Skip to content

Commit

Permalink
feat(go-sdk): configurable client credentials token url (#275)
Browse files Browse the repository at this point in the history
  • Loading branch information
rhamzeh authored Jan 15, 2024
2 parents 511fee3 + cb0be93 commit a1a9eec
Show file tree
Hide file tree
Showing 2 changed files with 67 additions and 45 deletions.
74 changes: 43 additions & 31 deletions config/clients/go/template/api_test.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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"`
Expand All @@ -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)
}
Expand All @@ -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}}",
Expand Down
38 changes: 24 additions & 14 deletions config/clients/go/template/credentials.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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{
Expand All @@ -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
}

0 comments on commit a1a9eec

Please sign in to comment.