Skip to content

feat(go-sdk): configurable client credentials token url #275

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}