Skip to content

Conversation

christianarty
Copy link

@christianarty christianarty commented Jul 17, 2025

Summary

fixes #863

This PR allows for users to have another option (oauth) when generating their JIRA config.yml for the Cloud installation.

Details

This PR implements JIRA's 3LO OAuth solution for users to obtain a JIRA access token.
Each consumer of jira-cli will need to create a JIRA App with the specific scopes in order to connect it properly with their JIRA cloud instance.

The oauth secret will be stored in the .config/.jira directory, where the tokens will be automatically regenerated when it expires and the newly generated tokens will be cached to the oauth secret file.

How to create a JIRA App properly:

See this discussion post here: #879 (comment)

Known Limitations/Issues

Note

This limitation has also been noted in the README under the Known Issues section.

Ideally, for OAuth, we would have one single distributed app that can be installed in multiple different JIRA cloud instances. However, The 3LO doesn't support Proof Key for Code Exchange (PKCE). Without this support, we would have to share the single distrubuted app's client secret with all the consumers. See these links for more info:

As noted in the forum above, a workaround would be that each consumer has to create their own JIRA app and use that app's client ID and secret in the jira-cli client app.

  • This basically acts like a proxy to funnel requests into your JIRA cloud instance

Testing Done

  • make deps install => WORKS
  • ~/go/bin/jira issue create -tTask -s"TEST TICKET" -l"testing" --template ~/jira/task.tmpl -a$(~/go/bin/jira me) => WORKS (created a ticket, and proper link)
  • make test => WORKS
  • make lint => WORKS
  • make ci => WORKS

@christianarty christianarty marked this pull request as ready for review July 17, 2025 12:45
@shadyabhi
Copy link

cc: @ankitpokhrel for visibility. It'll be great if this can be reviewed/merged soon, so folks that can only authenticate through OAuth 3LO can start using this tool again.

@christianarty
Copy link
Author

@ankitpokhrel Could you take a look at this PR when you get a chance? I think this is a nice feature add (and personally been using it for months now without hiccup)

@ankitpokhrel
Copy link
Owner

@christianarty Thanks for the PR! I'm currently away, I'll look into this in a few weeks.

@christianarty
Copy link
Author

@ankitpokhrel Hey! Just wanted to see if you had a moment to take a peek at this PR! Would be appreciated thanks!

@christianarty
Copy link
Author

christianarty commented Oct 8, 2025

Also added a new commit that expanded the default scopes since recently realized, with the oauth, for jira sprint list to work properly needed extra scopes: https://developer.atlassian.com/cloud/jira/software/rest/api-group-board/#api-rest-agile-1-0-board-boardid-sprint-sprintid-issue-get
https://developer.atlassian.com/cloud/jira/software/rest/api-group-sprint/#api-rest-agile-1-0-sprint-sprintid-issue-post

@ankitpokhrel ankitpokhrel requested a review from Copilot October 11, 2025 09:06
Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds OAuth 3LO (3-legged OAuth) authentication support to the JIRA CLI, allowing users to authenticate with Atlassian Jira Cloud using OAuth instead of API tokens. The implementation includes a complete OAuth flow with automatic token refresh, secure credential storage, and cloud ID retrieval for proper API access.

  • Implements OAuth 3LO authentication flow with automatic browser-based authorization
  • Adds secure file-based storage for OAuth tokens and client credentials
  • Integrates OAuth authentication into existing JIRA client with automatic token refresh

Reviewed Changes

Copilot reviewed 14 out of 15 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
pkg/oauth/oauth.go Core OAuth implementation with flow management and cloud ID retrieval
pkg/oauth/tokens.go Token management with automatic refresh and persistent storage
pkg/oauth/oauth_test.go Comprehensive test suite for OAuth functionality
pkg/utils/storage.go Generic file storage interface with JSON serialization helpers
pkg/utils/storage_test.go Test coverage for storage functionality
pkg/jira/client.go Integration of OAuth transport into JIRA client
internal/config/generator.go Configuration generator updates for OAuth setup
api/client.go Client factory updates to support OAuth token sources
pkg/jira/types.go New authentication type constants
pkg/jira/cloud_id.go Cloud ID retrieval functionality
README.md Documentation updates for OAuth authentication

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Comment on lines +283 to +287
ctx, cancel := context.WithTimeout(context.Background(), serverShutdownTimeout)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
fmt.Printf("Warning: failed to shutdown server: %v\n", err)
}
Copy link

Copilot AI Oct 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The server shutdown logic is duplicated in three places (lines 283-287, 303-307, 312-316). Consider extracting this into a helper function to reduce code duplication.

Copilot uses AI. Check for mistakes.

transport.TLSClientConfig.RootCAs = caCertPool
transport.TLSClientConfig.Certificates = []tls.Certificate{cert}
transport.TLSClientConfig.Renegotiation = tls.RenegotiateFreelyAsClient
tlsConfig := transport.(*http.Transport).TLSClientConfig
Copy link

Copilot AI Oct 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This type assertion will panic if the transport is not an *http.Transport. After adding OAuth support, the transport could be an *oauth2.Transport, making this assertion unsafe. Add a type check before the assertion.

Suggested change
tlsConfig := transport.(*http.Transport).TLSClientConfig
httpTransport, ok := transport.(*http.Transport)
if !ok {
log.Fatal("transport is not an *http.Transport; cannot configure mTLS")
}
tlsConfig := httpTransport.TLSClientConfig

Copilot uses AI. Check for mistakes.

Copy link
Owner

@ankitpokhrel ankitpokhrel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @christianarty, first of all, amazing work and sorry for jumping in a bit late!

I ran some tests, and most things are working great. I noticed a few things tho:

  1. Listing epic items returns 401. Perhaps we are missing some permissions?
➔ jira epic list ABC-3

jira: Received unexpected response '401 Unauthorized'.
Please check the parameters you supplied and try again.
  1. The warning is shown every time when oAuth is not used. I think we only want to show warning if the type is oAuth and api_server is not set.
Image
  1. Using jira init with Authentication type: cloud is failing with authentication err.
Image

Could you please help check?

(Feel free to ignore any copilot comment if that doesn't make sense)

Thank you again for great work! 🎉

}
// Return the first cloud ID found
if len(out) > 1 {
return "", ErrMultipleCloudIDs
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wondering if the user be able to override the cloud ID if the API returns more than one?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can add in a env var that basically checks to see if the cloud id is there

authType := c.usrCfg.AuthType

if c.usrCfg.AuthType == "" {
qs := &survey.Select{
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shall we also allow user to set client id and secret using env like JIRA_OAUTH_CLIENT_ID or similar? (could be something for later)

Expiry: tokens.Expiry,
}

if err := utils.SaveJSON(secretStorage, oauthSecretsFile, oauthSecrets); err != nil {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should use OS Keyring for secrets if that's available. If a fallback to plain text is required, I think the user should be warned clearly along with the saved location.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So that i understand correctly, you saying that the secret should be read/written to the OS keyring if available, and if it fails to write to keyring it should fallback to the json.

It could do a warn log that these secrets are located in X directory in plaintext with only owner read/write, so something like:

"[warning] Keyring unavailable, using plaintext JSON storage (owner read/write only)"

The only thing i could see as a minor annoyance is that the user would need to basically always (or whenever the new token is requested) allow jira-cli to access the keychain when prompted.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds correct!

The only thing i could see as a minor annoyance is that the user would need to basically always (or whenever the new token is requested) allow jira-cli to access the keychain when prompted.

I think it’s best to prioritize the most secure option when available. If the keychain prompt is too much of an annoyance we can let the user override the behavior with env. Overriding would mean they understand it’s a less secure choice.

Copy link
Author

@christianarty christianarty left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @christianarty, first of all, amazing work and sorry for jumping in a bit late!

I ran some tests, and most things are working great. I noticed a few things tho:

  1. Listing epic items returns 401. Perhaps we are missing some permissions?
➔ jira epic list ABC-3

jira: Received unexpected response '401 Unauthorized'.
Please check the parameters you supplied and try again.
  1. The warning is shown every time when oAuth is not used. I think we only want to show warning if the type is oAuth and api_server is not set.
Image
  1. Using jira init with Authentication type: cloud is failing with authentication err.
Image

Could you please help check?

(Feel free to ignore any copilot comment if that doesn't make sense)

Thank you again for great work! 🎉

Thanks for the information! Will address them.

Listing epic items returns 401. Perhaps we are missing some permissions?
After checking this url here for the endpoint we are hitting: https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-search/#api-rest-api-3-search-jql-get

from my own mental model, the Classic scope should be sufficient, but then again the JIRA API is okay at best, so i added in all the other granular scopes that is stated there in the defaultScopes field, and will update the discussion post regarding it. With adding in all those scopes, it works as intended.

The warning is shown every time when oAuth is not used. I think we only want to show warning if the type is oAuth and api_server is not set.

I'll take a look at this in a bit to see what small thing is off

Using jira init with Authentication type: cloud is failing with authentication err.

I just fixed the logic error for when we checkJiraAPIToken basically if you already did oauth, it thinks it should use the oauth token. However i think the JIRA api has changed and tbh, i think we should migrate to use the v3 of the myself API: https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-myself/#api-rest-api-3-myself-get

I am going to address these comments over the next couple days and when its ready to re-review i'll re-request a review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add new JIRA_AUTH_TYPE OAuth 2.0 3LO

3 participants