Skip to content

Commit 4bb982e

Browse files
committed
(feat) Enable auto refreshing access tokens through the oauth2 custom transport in the client
1 parent 7b99937 commit 4bb982e

File tree

6 files changed

+485
-50
lines changed

6 files changed

+485
-50
lines changed

api/client.go

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,10 @@ func getAPIToken(config *jira.Config) string {
3232
}
3333

3434
// Try OAuth access token if available and valid
35-
if oauthToken := oauth.GetValidAccessToken(); oauthToken != "" {
36-
return oauthToken
35+
if oauth.HasOAuthCredentials() {
36+
tk, _ := oauth.LoadOAuth2TokenSource()
37+
token, _ := tk.Token()
38+
return token.AccessToken
3739
}
3840

3941
// Try netrc file
@@ -73,15 +75,26 @@ func Client(config jira.Config) *jira.Client {
7375
config.Insecure = &insecure
7476
}
7577

76-
// Get API token from various sources
77-
config.APIToken = getAPIToken(&config)
78-
79-
// If we have an OAuth token, set auth type to OAuth
80-
if oauthToken := oauth.GetValidAccessToken(); oauthToken != "" && config.APIToken == oauthToken {
81-
oauthAuthType := jira.AuthTypeOAuth
82-
config.AuthType = &oauthAuthType
78+
// Check if we have OAuth credentials and should use OAuth
79+
if oauth.HasOAuthCredentials() && config.AuthType != nil && *config.AuthType == jira.AuthTypeOAuth {
80+
// Try to create OAuth2 token source
81+
tokenSource, err := oauth.LoadOAuth2TokenSource()
82+
if err == nil {
83+
// We have valid OAuth credentials, use OAuth authentication
84+
// Pass the TokenSource to the client via a custom option
85+
jiraClient = jira.NewClient(
86+
config,
87+
jira.WithTimeout(clientTimeout),
88+
jira.WithInsecureTLS(*config.Insecure),
89+
jira.WithOAuth2TokenSource(tokenSource),
90+
)
91+
return jiraClient
92+
}
8393
}
8494

95+
// Get API token from various sources (fallback for non-OAuth auth)
96+
config.APIToken = getAPIToken(&config)
97+
8598
// MTLS
8699

87100
if config.MTLSConfig.CaCert == "" {

internal/cmd/root/root.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ func cmdRequireToken(cmd string) bool {
157157
}
158158

159159
func checkForJiraToken(server string, login string) {
160-
if oauthToken := oauth.GetValidAccessToken(); oauthToken != "" {
160+
if oauth.HasOAuthCredentials() {
161161
return
162162
}
163163

pkg/jira/client.go

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import (
1414
"os"
1515
"strings"
1616
"time"
17+
18+
"golang.org/x/oauth2"
1719
)
1820

1921
const (
@@ -116,14 +118,15 @@ type Config struct {
116118

117119
// Client is a jira client.
118120
type Client struct {
119-
transport http.RoundTripper
120-
insecure bool
121-
server string
122-
login string
123-
authType *AuthType
124-
token string
125-
timeout time.Duration
126-
debug bool
121+
transport http.RoundTripper
122+
insecure bool
123+
server string
124+
login string
125+
authType *AuthType
126+
token string
127+
timeout time.Duration
128+
debug bool
129+
tokenSource oauth2.TokenSource
127130
}
128131

129132
// ClientFunc decorates option for client.
@@ -142,8 +145,8 @@ func NewClient(c Config, opts ...ClientFunc) *Client {
142145
for _, opt := range opts {
143146
opt(&client)
144147
}
145-
146-
transport := &http.Transport{
148+
var transport http.RoundTripper
149+
transport = &http.Transport{
147150
Proxy: http.ProxyFromEnvironment,
148151
TLSClientConfig: &tls.Config{
149152
MinVersion: tls.VersionTLS12,
@@ -154,6 +157,15 @@ func NewClient(c Config, opts ...ClientFunc) *Client {
154157
}).DialContext,
155158
}
156159

160+
if c.AuthType != nil && *c.AuthType == AuthTypeOAuth && client.tokenSource != nil {
161+
// Use OAuth2 transport with automatic token refresh
162+
baseTransport := transport
163+
transport = &oauth2.Transport{
164+
Base: baseTransport,
165+
Source: oauth2.ReuseTokenSource(nil, client.tokenSource),
166+
}
167+
}
168+
157169
if c.AuthType != nil && *c.AuthType == AuthTypeMTLS {
158170
// Create a CA certificate pool and add cert.pem to it.
159171
caCert, err := os.ReadFile(c.MTLSConfig.CaCert)
@@ -170,9 +182,10 @@ func NewClient(c Config, opts ...ClientFunc) *Client {
170182
}
171183

172184
// Add the MTLS specific configuration.
173-
transport.TLSClientConfig.RootCAs = caCertPool
174-
transport.TLSClientConfig.Certificates = []tls.Certificate{cert}
175-
transport.TLSClientConfig.Renegotiation = tls.RenegotiateFreelyAsClient
185+
tlsConfig := transport.(*http.Transport).TLSClientConfig
186+
tlsConfig.RootCAs = caCertPool
187+
tlsConfig.Certificates = []tls.Certificate{cert}
188+
tlsConfig.Renegotiation = tls.RenegotiateFreelyAsClient
176189
}
177190

178191
client.transport = transport
@@ -194,6 +207,13 @@ func WithInsecureTLS(ins bool) ClientFunc {
194207
}
195208
}
196209

210+
// WithOAuth2TokenSource is a functional opt to attach OAuth2 token source to the client.
211+
func WithOAuth2TokenSource(tokenSource oauth2.TokenSource) ClientFunc {
212+
return func(c *Client) {
213+
c.tokenSource = tokenSource
214+
}
215+
}
216+
197217
// Get sends GET request to v3 version of the jira api.
198218
func (c *Client) Get(ctx context.Context, path string, headers Header) (*http.Response, error) {
199219
return c.request(ctx, http.MethodGet, c.server+baseURLv3+path, nil, headers)
@@ -280,7 +300,11 @@ func (c *Client) request(ctx context.Context, method, endpoint string, body []by
280300
req.Header.Add("Authorization", "Bearer "+c.token)
281301
}
282302
case string(AuthTypeOAuth):
283-
req.Header.Add("Authorization", "Bearer "+c.token)
303+
// OAuth authentication is handled by oauth2.Transport automatically
304+
// Only add manual auth header if we don't have a TokenSource (fallback mode)
305+
if c.tokenSource == nil && c.token != "" {
306+
req.Header.Add("Authorization", "Bearer "+c.token)
307+
}
284308
case string(AuthTypeBearer):
285309
req.Header.Add("Authorization", "Bearer "+c.token)
286310
case string(AuthTypeBasic):

pkg/oauth/oauth.go

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -51,30 +51,32 @@ type OAuthConfig struct {
5151
Scopes []string
5252
}
5353

54-
// OAuthSecrets holds all OAuth secrets in a single structure
55-
type OAuthSecrets struct {
56-
ClientSecret string `json:"client_secret"`
57-
AccessToken string `json:"access_token"`
58-
RefreshToken string `json:"refresh_token"`
59-
TokenType string `json:"token_type"`
60-
Expiry time.Time `json:"expiry"`
61-
}
62-
6354
// ConfigureTokenResponse holds the OAuth token response
6455
type ConfigureTokenResponse struct {
6556
AccessToken string
6657
RefreshToken string
6758
CloudID string
6859
}
6960

70-
// IsExpired checks if the access token is expired
71-
func (o *OAuthSecrets) IsExpired() bool {
72-
return time.Now().After(o.Expiry)
73-
}
61+
// GetOAuth2Config creates an OAuth2 config for the given client credentials
62+
func GetOAuth2Config(clientID, clientSecret, redirectURI string, scopes []string) *oauth2.Config {
63+
if scopes == nil {
64+
scopes = defaultScopes
65+
}
7466

75-
// IsValid checks if the OAuth secrets are valid and not expired
76-
func (o *OAuthSecrets) IsValid() bool {
77-
return o.AccessToken != "" && !o.IsExpired()
67+
if redirectURI == "" {
68+
redirectURI = defaultRedirectURI
69+
}
70+
return &oauth2.Config{
71+
ClientID: clientID,
72+
ClientSecret: clientSecret,
73+
RedirectURL: redirectURI,
74+
Scopes: scopes,
75+
Endpoint: oauth2.Endpoint{
76+
AuthURL: jiraAuthURL,
77+
TokenURL: jiraTokenURL,
78+
},
79+
}
7880
}
7981

8082
// Configure performs the complete OAuth flow and returns tokens
@@ -106,6 +108,7 @@ func Configure() (*ConfigureTokenResponse, error) {
106108

107109
// Store all OAuth secrets in a single JSON file
108110
oauthSecrets := &OAuthSecrets{
111+
ClientID: config.ClientID,
109112
ClientSecret: config.ClientSecret,
110113
AccessToken: tokens.AccessToken,
111114
RefreshToken: tokens.RefreshToken,
@@ -154,6 +157,12 @@ func GetValidAccessToken() string {
154157
return ""
155158
}
156159

160+
// HasOAuthCredentials checks if OAuth credentials are present
161+
func HasOAuthCredentials() bool {
162+
_, err := LoadOAuthSecrets()
163+
return err == nil
164+
}
165+
157166
// collectOAuthCredentials collects OAuth credentials from the user
158167
func collectOAuthCredentials() (*OAuthConfig, error) {
159168
var questions []*survey.Question
@@ -206,16 +215,7 @@ func performOAuthFlow(config *OAuthConfig) (*oauth2.Token, error) {
206215
defer s.Stop()
207216

208217
// OAuth2 configuration for JIRA
209-
oauthConfig := &oauth2.Config{
210-
ClientID: config.ClientID,
211-
ClientSecret: config.ClientSecret,
212-
RedirectURL: config.RedirectURI,
213-
Scopes: config.Scopes,
214-
Endpoint: oauth2.Endpoint{
215-
AuthURL: jiraAuthURL,
216-
TokenURL: jiraTokenURL,
217-
},
218-
}
218+
oauthConfig := GetOAuth2Config(config.ClientID, config.ClientSecret, config.RedirectURI, config.Scopes)
219219

220220
// Generate authorization URL with PKCE
221221
verifier := oauth2.GenerateVerifier()

0 commit comments

Comments
 (0)