From f0c4411b438f96e82047283d342ed98e2b0bd50a Mon Sep 17 00:00:00 2001 From: Halil Cankilic <141903706+Eiermitsucuk@users.noreply.github.com> Date: Tue, 31 Mar 2026 10:59:12 +0200 Subject: [PATCH] refactor: obo-exchange logics after moving them to library (#101) --- cmd/obo_exchange.go | 8 ++--- go.mod | 4 +-- go.sum | 8 ++--- ims/obo_exchange.go | 85 ++++++++++++--------------------------------- 4 files changed, 33 insertions(+), 72 deletions(-) diff --git a/cmd/obo_exchange.go b/cmd/obo_exchange.go index 12ef35d..c1b9eba 100644 --- a/cmd/obo_exchange.go +++ b/cmd/obo_exchange.go @@ -22,14 +22,14 @@ func oboExchangeCmd(imsConfig *ims.Config) *cobra.Command { Use: "on-behalf-of", Aliases: []string{"obo"}, Short: "On-Behalf-Of token exchange.", - Long: `On-Behalf-Of token exchange: exchange a user access token for a new token. Do NOT send OBO access tokens to frontend clients.`, + Long: `Token exchange using the RFC 8693 (urn:ietf:params:oauth:grant-type:token-exchange) grant.`, RunE: func(cmd *cobra.Command, args []string) error { cmd.SilenceUsage = true cmd.SilenceErrors = true resp, err := imsConfig.OBOExchange() if err != nil { - return fmt.Errorf("error during On-Behalf-Of exchange: %v", err) + return fmt.Errorf("error during On-Behalf-Of exchange: %w", err) } fmt.Println(resp.AccessToken) return nil @@ -39,8 +39,8 @@ func oboExchangeCmd(imsConfig *ims.Config) *cobra.Command { cmd.Flags().StringVarP(&imsConfig.ClientID, "clientID", "c", "", "IMS client ID.") cmd.Flags().StringVarP(&imsConfig.ClientSecret, "clientSecret", "p", "", "IMS client secret.") cmd.Flags().StringVarP(&imsConfig.AccessToken, "accessToken", "t", "", "User access token (subject token). Only access tokens are accepted.") - cmd.Flags().StringSliceVarP(&imsConfig.Scopes, "scopes", "s", []string{""}, - "Scopes to request. Must be within the client's configured scope boundary.") + cmd.Flags().StringSliceVarP(&imsConfig.Scopes, "scopes", "s", nil, + "Optional scopes to request; if omitted, none are sent. When set, must stay within the client's configured scope boundary.") return cmd } diff --git a/go.mod b/go.mod index daf77a1..20c309d 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ go 1.23.0 toolchain go1.26.1 require ( - github.com/adobe/ims-go v0.19.2 + github.com/adobe/ims-go v0.20.0 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/spf13/cobra v1.10.2 github.com/spf13/viper v1.21.0 @@ -24,7 +24,7 @@ require ( require ( github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect - github.com/golang-jwt/jwt/v5 v5.3.0 // indirect + github.com/golang-jwt/jwt/v5 v5.3.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect diff --git a/go.sum b/go.sum index 48f52ed..ce3ae82 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -github.com/adobe/ims-go v0.19.2 h1:PzomRLP/ZDE5myXCAT2ofs6v0mm0yy4vNEl6mKOYLEU= -github.com/adobe/ims-go v0.19.2/go.mod h1:fBMENOn081lCW9w+XDrG3RNkWzUPYzDAfGCZZGwSIKg= +github.com/adobe/ims-go v0.20.0 h1:M1OyF9xWLgsTVGbZ+c+G0EbPGpPhXQiPuLEdanNyaRo= +github.com/adobe/ims-go v0.20.0/go.mod h1:zGpx0ylsumBjkgd8fYgzJ8+Ci/zFABiBTAxbCscsyR8= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -9,8 +9,8 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= -github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= diff --git a/ims/obo_exchange.go b/ims/obo_exchange.go index c6b09de..ac1a529 100644 --- a/ims/obo_exchange.go +++ b/ims/obo_exchange.go @@ -8,99 +8,60 @@ // OF ANY KIND, either express or implied. See the License for the specific language // governing permissions and limitations under the License. -package ims - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - "net/url" - "strings" -) - // - Subject token restrictions: only user access tokens are accepted; ServiceTokens and // impersonation tokens must not be used as the subject token. // - Scope boundary: requested scopes cannot exceed the client's configured scopes. // - Audit trail: the full actor chain is preserved in the act claim of the issued token. // OBO uses token v4 and RFC 8693 grant type per IMS OBO documentation. -const defaultOBOGrantType = "urn:ietf:params:oauth:grant-type:token-exchange" + +package ims + +import ( + "fmt" + + "github.com/adobe/ims-go/ims" +) func (i Config) validateOBOExchangeConfig() error { switch { case i.URL == "": return fmt.Errorf("missing IMS base URL parameter") + case !validateURL(i.URL): + return fmt.Errorf("invalid IMS base URL parameter") case i.ClientID == "": return fmt.Errorf("missing client ID parameter") case i.ClientSecret == "": return fmt.Errorf("missing client secret parameter") case i.AccessToken == "": - return fmt.Errorf("missing access token parameter (only access tokens are accepted)") - case len(i.Scopes) == 0 || (len(i.Scopes) == 1 && i.Scopes[0] == ""): - return fmt.Errorf("scopes are required for On-Behalf-Of exchange") + return fmt.Errorf("missing access token parameter") default: return nil } } func (i Config) OBOExchange() (TokenInfo, error) { - if err := i.validateOBOExchangeConfig(); err != nil { - return TokenInfo{}, fmt.Errorf("invalid parameters for On-Behalf-Of exchange: %v", err) - } - httpClient, err := i.httpClient() - if err != nil { - return TokenInfo{}, fmt.Errorf("error creating the HTTP Client: %v", err) - } - - data := url.Values{} - data.Set("grant_type", defaultOBOGrantType) - data.Set("client_id", i.ClientID) - data.Set("client_secret", i.ClientSecret) - data.Set("subject_token", i.AccessToken) - data.Set("subject_token_type", "urn:ietf:params:oauth:token-type:access_token") - data.Set("requested_token_type", "urn:ietf:params:oauth:token-type:access_token") - data.Set("scope", strings.Join(i.Scopes, ",")) - - // OBO Token Exchange requires /ims/token/v4 (v3 does not support this grant type). - tokenURL := fmt.Sprintf("%s/ims/token/v4?client_id=%s", strings.TrimSuffix(i.URL, "/"), url.QueryEscape(i.ClientID)) - req, err := http.NewRequest(http.MethodPost, tokenURL, strings.NewReader(data.Encode())) - if err != nil { - return TokenInfo{}, fmt.Errorf("error creating On-Behalf-Of request: %v", err) + if err := i.validateOBOExchangeConfig(); err != nil { + return TokenInfo{}, fmt.Errorf("invalid parameters for On-Behalf-Of exchange: %w", err) } - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - resp, err := httpClient.Do(req) + c, err := i.newIMSClient() if err != nil { - return TokenInfo{}, fmt.Errorf("error during On-Behalf-Of exchange: %v", err) + return TokenInfo{}, fmt.Errorf("error creating the IMS client: %w", err) } - defer func() { _ = resp.Body.Close() }() - body, err := io.ReadAll(resp.Body) + r, err := c.OBOExchange(&ims.OBOExchangeRequest{ + ClientID: i.ClientID, + ClientSecret: i.ClientSecret, + SubjectToken: i.AccessToken, + Scopes: i.Scopes, + }) if err != nil { - return TokenInfo{}, fmt.Errorf("error reading On-Behalf-Of response: %v", err) - } - - if resp.StatusCode != http.StatusOK { - errMsg := fmt.Sprintf("On-Behalf-Of exchange failed (status %d): %s", resp.StatusCode, string(body)) - if resp.StatusCode == http.StatusBadRequest { - if strings.Contains(string(body), "invalid_scope") { - errMsg += " — IMS may be rejecting the subject token's scopes for this client. Ensure the client has Token exchange enabled and allowed scopes in the portal, or try a user token obtained with fewer scopes." - } - } - return TokenInfo{}, fmt.Errorf("%s", errMsg) - } - - var out struct { - AccessToken string `json:"access_token"` - ExpiresIn int `json:"expires_in"` - } - if err := json.Unmarshal(body, &out); err != nil { - return TokenInfo{}, fmt.Errorf("error decoding On-Behalf-Of response: %v", err) + return TokenInfo{}, fmt.Errorf("error during the On-Behalf-Of exchange: %w", err) } return TokenInfo{ - AccessToken: out.AccessToken, + AccessToken: r.AccessToken, }, nil }