@@ -45,6 +45,36 @@ const (
4545 refreshTokenTimeout = 10 * time .Second
4646)
4747
48+ // OAuth endpoint paths
49+ const (
50+ endpointDeviceCode = "/oauth/device/code"
51+ endpointToken = "/oauth/token"
52+ endpointTokenInfo = "/oauth/tokeninfo"
53+ )
54+
55+ // Device authorization error codes per RFC 8628
56+ const (
57+ oauthErrAuthorizationPending = "authorization_pending"
58+ oauthErrSlowDown = "slow_down"
59+ oauthErrExpiredToken = "expired_token"
60+ oauthErrAccessDenied = "access_denied"
61+ )
62+
63+ // Common OAuth 2.0 token endpoint error codes (e.g., RFC 6749)
64+ const (
65+ oauthErrInvalidGrant = "invalid_grant"
66+ oauthErrInvalidToken = "invalid_token"
67+ )
68+
69+ // tokenResponse is the common structure for OAuth token endpoint responses.
70+ type tokenResponse struct {
71+ AccessToken string `json:"access_token"`
72+ RefreshToken string `json:"refresh_token"`
73+ TokenType string `json:"token_type"`
74+ ExpiresIn int `json:"expires_in"`
75+ Scope string `json:"scope"`
76+ }
77+
4878func init () {
4979 // Load .env file if exists (ignore error if not found)
5080 _ = godotenv .Load ()
@@ -361,7 +391,7 @@ func requestDeviceCode(ctx context.Context) (*oauth2.DeviceAuthResponse, error)
361391 req , err := http .NewRequestWithContext (
362392 reqCtx ,
363393 http .MethodPost ,
364- serverURL + "/oauth/device/code" ,
394+ serverURL + endpointDeviceCode ,
365395 strings .NewReader (data .Encode ()),
366396 )
367397 if err != nil {
@@ -418,8 +448,8 @@ func performDeviceFlow(ctx context.Context, d tui.Displayer) (*TokenStorage, err
418448 config := & oauth2.Config {
419449 ClientID : clientID ,
420450 Endpoint : oauth2.Endpoint {
421- DeviceAuthURL : serverURL + "/oauth/device/code" ,
422- TokenURL : serverURL + "/oauth/token" ,
451+ DeviceAuthURL : serverURL + endpointDeviceCode ,
452+ TokenURL : serverURL + endpointToken ,
423453 },
424454 Scopes : []string {"read" , "write" },
425455 }
@@ -505,11 +535,11 @@ func pollForTokenWithProgress(
505535 var errResp ErrorResponse
506536 if jsonErr := json .Unmarshal (oauthErr .Body , & errResp ); jsonErr == nil {
507537 switch errResp .Error {
508- case "authorization_pending" :
538+ case oauthErrAuthorizationPending :
509539 // User hasn't authorized yet, continue polling
510540 continue
511541
512- case "slow_down" :
542+ case oauthErrSlowDown :
513543 // Server requests slower polling - increase interval
514544 backoffMultiplier *= 1.5
515545 pollInterval = min (
@@ -520,10 +550,10 @@ func pollForTokenWithProgress(
520550 d .PollSlowDown (pollInterval )
521551 continue
522552
523- case "expired_token" :
553+ case oauthErrExpiredToken :
524554 return nil , errors .New ("device code expired, please restart the flow" )
525555
526- case "access_denied" :
556+ case oauthErrAccessDenied :
527557 return nil , errors .New ("user denied authorization" )
528558
529559 default :
@@ -590,14 +620,7 @@ func exchangeDeviceCode(
590620 }
591621
592622 // Parse successful token response
593- var tokenResp struct {
594- AccessToken string `json:"access_token"`
595- RefreshToken string `json:"refresh_token"`
596- TokenType string `json:"token_type"`
597- ExpiresIn int `json:"expires_in"`
598- Scope string `json:"scope"`
599- }
600-
623+ var tokenResp tokenResponse
601624 if err := json .Unmarshal (body , & tokenResp ); err != nil {
602625 return nil , fmt .Errorf ("failed to parse token response: %w" , err )
603626 }
@@ -627,7 +650,7 @@ func verifyToken(ctx context.Context, accessToken string, d tui.Displayer) error
627650 defer cancel ()
628651
629652 req , err := http .NewRequestWithContext (
630- reqCtx , http .MethodGet , serverURL + "/oauth/tokeninfo" , nil ,
653+ reqCtx , http .MethodGet , serverURL + endpointTokenInfo , nil ,
631654 )
632655 if err != nil {
633656 return fmt .Errorf ("failed to create request: %w" , err )
@@ -765,7 +788,7 @@ func refreshAccessToken(
765788 req , err := http .NewRequestWithContext (
766789 reqCtx ,
767790 http .MethodPost ,
768- serverURL + "/oauth/token" ,
791+ serverURL + endpointToken ,
769792 strings .NewReader (data .Encode ()),
770793 )
771794 if err != nil {
@@ -789,7 +812,7 @@ func refreshAccessToken(
789812 var errResp ErrorResponse
790813 if err := json .Unmarshal (body , & errResp ); err == nil {
791814 // Check if refresh token is expired or invalid
792- if errResp .Error == "invalid_grant" || errResp .Error == "invalid_token" {
815+ if errResp .Error == oauthErrInvalidGrant || errResp .Error == oauthErrInvalidToken {
793816 return nil , ErrRefreshTokenExpired
794817 }
795818 return nil , fmt .Errorf ("%s: %s" , errResp .Error , errResp .ErrorDescription )
@@ -798,13 +821,7 @@ func refreshAccessToken(
798821 }
799822
800823 // Parse token response
801- var tokenResp struct {
802- AccessToken string `json:"access_token"`
803- RefreshToken string `json:"refresh_token"`
804- TokenType string `json:"token_type"`
805- ExpiresIn int `json:"expires_in"`
806- }
807-
824+ var tokenResp tokenResponse
808825 if err := json .Unmarshal (body , & tokenResp ); err != nil {
809826 return nil , fmt .Errorf ("failed to parse token response: %w" , err )
810827 }
@@ -850,7 +867,7 @@ func makeAPICallWithAutoRefresh(ctx context.Context, storage *TokenStorage, d tu
850867 defer cancel ()
851868
852869 req , err := http .NewRequestWithContext (
853- reqCtx , http .MethodGet , serverURL + "/oauth/tokeninfo" , nil ,
870+ reqCtx , http .MethodGet , serverURL + endpointTokenInfo , nil ,
854871 )
855872 if err != nil {
856873 return fmt .Errorf ("failed to create request: %w" , err )
@@ -889,7 +906,7 @@ func makeAPICallWithAutoRefresh(ctx context.Context, storage *TokenStorage, d tu
889906 defer retryCancel ()
890907
891908 req , err = http .NewRequestWithContext (
892- retryCtx , http .MethodGet , serverURL + "/oauth/tokeninfo" , nil ,
909+ retryCtx , http .MethodGet , serverURL + endpointTokenInfo , nil ,
893910 )
894911 if err != nil {
895912 return fmt .Errorf ("failed to create retry request: %w" , err )
0 commit comments