From cb0be930b9741ab29b50f5c79ea0ec9f3540cdb6 Mon Sep 17 00:00:00 2001 From: Yann D'Isanto Date: Fri, 12 Jan 2024 18:18:52 +0100 Subject: [PATCH] feat(go-sdk): configurable client credentials token url --- config/clients/go/template/api_test.mustache | 74 +++++++++++-------- .../clients/go/template/credentials.mustache | 38 ++++++---- 2 files changed, 67 insertions(+), 45 deletions(-) diff --git a/config/clients/go/template/api_test.mustache b/config/clients/go/template/api_test.mustache index f5d8397c..4ddc5574 100644 --- a/config/clients/go/template/api_test.mustache +++ b/config/clients/go/template/api_test.mustache @@ -235,7 +235,7 @@ func Test{{appShortName}}ApiConfiguration(t *testing.T) { } }) - clientCredentialsFirstRequestTest := func(t *testing.T, config Configuration) { + clientCredentialsFirstRequestTest := func(t *testing.T, config Configuration, expectedTokenEndpoint string) { configuration, err := NewConfiguration(config) if err != nil { t.Fatalf("%v", err) @@ -264,7 +264,7 @@ func Test{{appShortName}}ApiConfiguration(t *testing.T) { }, ) - httpmock.RegisterResponder("POST", fmt.Sprintf("https://%s/oauth/token", configuration.Credentials.Config.ClientCredentialsApiTokenIssuer), + httpmock.RegisterResponder("POST", expectedTokenEndpoint, func(req *http.Request) (*http.Response, error) { resp, err := httpmock.NewJsonResponse(200, struct { AccessToken string `json:"access_token"` @@ -281,7 +281,7 @@ func Test{{appShortName}}ApiConfiguration(t *testing.T) { } info := httpmock.GetCallCountInfo() - numCalls := info[fmt.Sprintf("POST https://%s/oauth/token", configuration.Credentials.Config.ClientCredentialsApiTokenIssuer)] + numCalls := info[fmt.Sprintf("POST %s", expectedTokenEndpoint)] if numCalls != 1 { t.Fatalf("Expected call to get access token to be made exactly once, saw: %d", numCalls) } @@ -291,39 +291,51 @@ func Test{{appShortName}}ApiConfiguration(t *testing.T) { } } - t.Run("should issue a network call to get the token at the first request if client id is provided", func(t *testing.T) { - t.Run("with Auth0 configuration", func(t *testing.T) { - clientCredentialsFirstRequestTest(t, Configuration{ - ApiHost: "api.{{sampleApiDomain}}", - StoreId: "01GXSB9YR785C4FYS3C0RTG7B2", - Credentials: &credentials.Credentials{ - Method: credentials.CredentialsMethodClientCredentials, - Config: &credentials.Config{ - ClientCredentialsClientId: "some-id", - ClientCredentialsClientSecret: "some-secret", - ClientCredentialsApiAudience: "some-audience", - ClientCredentialsApiTokenIssuer: "tokenissuer.{{sampleApiDomain}}", + tokenIssuers := map[string]string{ + "issuer.fga.example": "https://issuer.fga.example/oauth/token", + "https://issuer.fga.example": "https://issuer.fga.example/oauth/token", + "https://issuer.fga.example/": "https://issuer.fga.example/oauth/token", + "https://issuer.fga.example:8080": "https://issuer.fga.example:8080/oauth/token", + "https://issuer.fga.example:8080/": "https://issuer.fga.example:8080/oauth/token", + "issuer.fga.example/some_endpoint": "https://issuer.fga.example/some_endpoint", + "https://issuer.fga.example/some_endpoint": "https://issuer.fga.example/some_endpoint", + "https://issuer.fga.example:8080/some_endpoint": "https://issuer.fga.example:8080/some_endpoint", + } + + for tokenIssuer, expectedTokenURL := range tokenIssuers { + t.Run("should issue a network call to get the token at the first request if client id is provided", func(t *testing.T) { + t.Run("with Auth0 configuration", func(t *testing.T) { + clientCredentialsFirstRequestTest(t, Configuration{ + ApiUrl: "http://api.{{sampleApiDomain}}", + StoreId: "01GXSB9YR785C4FYS3C0RTG7B2", + Credentials: &credentials.Credentials{ + Method: credentials.CredentialsMethodClientCredentials, + Config: &credentials.Config{ + ClientCredentialsClientId: "some-id", + ClientCredentialsClientSecret: "some-secret", + ClientCredentialsApiAudience: "some-audience", + ClientCredentialsApiTokenIssuer: tokenIssuer, + }, }, - }, + }, expectedTokenURL) }) - }) - t.Run("with OAuth2 configuration", func(t *testing.T) { - clientCredentialsFirstRequestTest(t, Configuration{ - ApiHost: "api.{{sampleApiDomain}}", - StoreId: "01GXSB9YR785C4FYS3C0RTG7B2", - Credentials: &credentials.Credentials{ - Method: credentials.CredentialsMethodClientCredentials, - Config: &credentials.Config{ - ClientCredentialsClientId: "some-id", - ClientCredentialsClientSecret: "some-secret", - ClientCredentialsScopes: "scope1 scope2", - ClientCredentialsApiTokenIssuer: "tokenissuer.{{sampleApiDomain}}", + t.Run("with OAuth2 configuration", func(t *testing.T) { + clientCredentialsFirstRequestTest(t, Configuration{ + ApiUrl: "http://api.{{sampleApiDomain}}", + StoreId: "01GXSB9YR785C4FYS3C0RTG7B2", + Credentials: &credentials.Credentials{ + Method: credentials.CredentialsMethodClientCredentials, + Config: &credentials.Config{ + ClientCredentialsClientId: "some-id", + ClientCredentialsClientSecret: "some-secret", + ClientCredentialsScopes: "scope1 scope2", + ClientCredentialsApiTokenIssuer: tokenIssuer, + }, }, - }, + }, expectedTokenURL) }) }) - }) - + } t.Run("should not issue a network call to get the token at the first request if the clientId is not provided", func(t *testing.T) { configuration, err := NewConfiguration(Configuration{ ApiHost: "api.{{sampleApiDomain}}", diff --git a/config/clients/go/template/credentials.mustache b/config/clients/go/template/credentials.mustache index ee1cf76a..1e76865a 100644 --- a/config/clients/go/template/credentials.mustache +++ b/config/clients/go/template/credentials.mustache @@ -39,16 +39,6 @@ type Credentials struct { Config *Config `json:"config,omitempty"` } -func isWellFormedUri(uriString string) bool { - uri, err := url.Parse(uriString) - - if (err != nil) || (uri.Scheme != "http" && uri.Scheme != "https") || ((uri.Scheme + "://" + uri.Host) != uriString) { - return false - } - - return true -} - func NewCredentials(config Credentials) (*Credentials, error) { creds := &Credentials{ Method: config.Method, @@ -79,9 +69,11 @@ func (c *Credentials) ValidateCredentialsConfig() error { conf.ClientCredentialsApiTokenIssuer == "" { return fmt.Errorf("all of CredentialsConfig.ClientId, CredentialsConfig.ClientSecret and CredentialsConfig.ApiTokenIssuer are required when CredentialsMethod is CredentialsMethodClientCredentials (%s)", c.Method) } - if !isWellFormedUri("https://" + conf.ClientCredentialsApiTokenIssuer) { - return fmt.Errorf("CredentialsConfig.ApiTokenIssuer (%s) is in an invalid format", "https://"+conf.ClientCredentialsApiTokenIssuer) - } + tokenURL, err := buildApiTokenURL(conf.ClientCredentialsApiTokenIssuer) + if err != nil { + return err + } + conf.ClientCredentialsApiTokenIssuer = tokenURL } return nil @@ -114,7 +106,7 @@ func (c *Credentials) GetHttpClientAndHeaderOverrides() (*http.Client, []*Header ccConfig := clientcredentials.Config{ ClientID: c.Config.ClientCredentialsClientId, ClientSecret: c.Config.ClientCredentialsClientSecret, - TokenURL: fmt.Sprintf("https://%s/oauth/token", c.Config.ClientCredentialsApiTokenIssuer), + TokenURL: c.Config.ClientCredentialsApiTokenIssuer, } if c.Config.ClientCredentialsApiAudience != "" { ccConfig.EndpointParams = map[string][]string{ @@ -137,3 +129,21 @@ func (c *Credentials) GetHttpClientAndHeaderOverrides() (*http.Client, []*Header return client, headers } + +var defaultTokenEndpointPath = "oauth/token" + +func buildApiTokenURL(issuer string) (string, error) { + u, err := url.Parse(issuer) + if err != nil { + return "", err + } + if u.Scheme == "" { + u, _ = url.Parse(fmt.Sprintf("https://%s", issuer)) + } else if u.Scheme != "http" && u.Scheme != "https" { + return "", fmt.Errorf("invalid issuer scheme '%s' (must be http or https)", u.Scheme) + } + if u.Path == "" || u.Path == "/" { + u.Path = defaultTokenEndpointPath + } + return u.String(), nil +}