-
-
Notifications
You must be signed in to change notification settings - Fork 284
feat: Enable OAuth 3LO support #877
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
ec9c285
to
323089d
Compare
cc: @ankitpokhrel for visibility. It'll be great if this can be reviewed/merged soon, so folks that can only authenticate through |
…figureServerMeta`
… transport in the client
…er, rather than their own JIRA server
fa19335
to
0ebaa17
Compare
@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) |
@christianarty Thanks for the PR! I'm currently away, I'll look into this in a few weeks. |
@ankitpokhrel Hey! Just wanted to see if you had a moment to take a peek at this PR! Would be appreciated thanks! |
Also added a new commit that expanded the default scopes since recently realized, with the oauth, for |
There was a problem hiding this 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.
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) | ||
} |
Copilot
AI
Oct 11, 2025
There was a problem hiding this comment.
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 |
Copilot
AI
Oct 11, 2025
There was a problem hiding this comment.
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.
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.
There was a problem hiding this 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:
- 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.
- The warning is shown every time when
oAuth
is not used. I think we only want to show warning if the type isoAuth
andapi_server
is not set.

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

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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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{ |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this 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:
- 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.
- The warning is shown every time when
oAuth
is not used. I think we only want to show warning if the type isoAuth
andapi_server
is not set.![]()
- Using
jira init
withAuthentication type: cloud
is failing with authentication err.![]()
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.
Summary
fixes #863
This PR allows for users to have another option (
oauth
) when generating their JIRAconfig.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.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
=> WORKSmake lint
=> WORKSmake ci
=> WORKS