Skip to content

Commit

Permalink
feat(go-sdk): oauth2 client credentials support (#256)
Browse files Browse the repository at this point in the history
  • Loading branch information
rhamzeh authored Dec 15, 2023
2 parents ce7d2d6 + 917f815 commit 8bbf9d6
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 39 deletions.
34 changes: 33 additions & 1 deletion config/clients/go/template/README_initializing.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func main() {
}
```

#### Client Credentials
#### Auth0 Client Credentials

```golang
import (
Expand Down Expand Up @@ -82,3 +82,35 @@ func main() {
}
}
```

#### OAuth2 Client Credentials

```golang
import (
{{packageName}} "{{gitHost}}/{{gitUserId}}/{{gitRepoId}}"
. "{{gitHost}}/{{gitUserId}}/{{gitRepoId}}/client"
"{{gitHost}}/{{gitUserId}}/{{gitRepoId}}/credentials"
"os"
)

func main() {
fgaClient, err := NewSdkClient(&ClientConfiguration{
ApiUrl: os.Getenv("FGA_API_URL"), // required, e.g. https://api.{{sampleApiDomain}}
StoreId: os.Getenv("FGA_STORE_ID"), // not needed when calling `CreateStore` or `ListStores`
AuthorizationModelId: os.Getenv("FGA_AUTHORIZATION_MODEL_ID"), // optional, recommended to be set for production
Credentials: &credentials.Credentials{
Method: credentials.CredentialsMethodClientCredentials,
Config: &credentials.Config{
ClientCredentialsClientId: os.Getenv("FGA_CLIENT_ID"),
ClientCredentialsClientSecret: os.Getenv("FGA_CLIENT_SECRET"),
ClientCredentialsScopes: os.Getenv("FGA_API_SCOPES"), // optional space separated scopes
ClientCredentialsApiTokenIssuer: os.Getenv("FGA_API_TOKEN_ISSUER"),
},
},
})

if err != nil {
// .. Handle error
}
}
```
70 changes: 37 additions & 33 deletions config/clients/go/template/api_test.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ func Test{{appShortName}}ApiConfiguration(t *testing.T) {
}
})

t.Run("In ClientCredentials method, providing no client id, secret, audience or issuer should error", func(t *testing.T) {
t.Run("In ClientCredentials method, providing no client id, secret or issuer should error", func(t *testing.T) {
_, err := NewConfiguration(Configuration{
ApiHost: "https://api.{{sampleApiDomain}}",
StoreId: "01GXSB9YR785C4FYS3C0RTG7B2",
Expand Down Expand Up @@ -161,23 +161,6 @@ func Test{{appShortName}}ApiConfiguration(t *testing.T) {
t.Fatalf("Expected an error: client secret is required")
}

_, err = NewConfiguration(Configuration{
ApiHost: "https://api.{{sampleApiDomain}}",
StoreId: "01GXSB9YR785C4FYS3C0RTG7B2",
Credentials: &credentials.Credentials{
Method: credentials.CredentialsMethodApiToken,
Config: &credentials.Config{
ClientCredentialsClientId: "some-id",
ClientCredentialsClientSecret: "some-secret",
ClientCredentialsApiTokenIssuer: "some-issuer",
},
},
})

if err == nil {
t.Fatalf("Expected an error: api audience is required")
}

_, err = NewConfiguration(Configuration{
ApiHost: "https://api.{{sampleApiDomain}}",
StoreId: "01GXSB9YR785C4FYS3C0RTG7B2",
Expand Down Expand Up @@ -252,20 +235,8 @@ 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) {
configuration, err := NewConfiguration(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}}",
},
},
})
clientCredentialsFirstRequestTest := func(t *testing.T, config Configuration) {
configuration, err := NewConfiguration(config)
if err != nil {
t.Fatalf("%v", err)
}
Expand Down Expand Up @@ -318,7 +289,40 @@ func Test{{appShortName}}ApiConfiguration(t *testing.T) {
if numCalls != 1 {
t.Fatalf("Expected call to get authorization models to be made exactly once, saw: %d", numCalls)
}
})
}

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}}",
},
},
})
})
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("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{
Expand Down
17 changes: 12 additions & 5 deletions config/clients/go/template/credentials.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"net/http"
"net/url"
"strings"

"{{gitHost}}/{{gitUserId}}/{{gitRepoId}}/oauth2/clientcredentials"
)
Expand All @@ -30,6 +31,7 @@ type Config struct {
ClientCredentialsApiAudience string `json:"apiAudience,omitempty"`
ClientCredentialsClientId string `json:"clientId,omitempty"`
ClientCredentialsClientSecret string `json:"clientSecret,omitempty"`
ClientCredentialsScopes string `json:"scopes,omitempty"`
}

type Credentials struct {
Expand Down Expand Up @@ -74,9 +76,8 @@ func (c *Credentials) ValidateCredentialsConfig() error {
if conf == nil ||
conf.ClientCredentialsClientId == "" ||
conf.ClientCredentialsClientSecret == "" ||
conf.ClientCredentialsApiTokenIssuer == "" ||
conf.ClientCredentialsApiAudience == "" {
return fmt.Errorf("all of CredentialsConfig.ClientId, CredentialsConfig.ClientSecret, CredentialsConfig.ApiAudience and CredentialsConfig.ApiTokenIssuer are required when CredentialsMethod is CredentialsMethodClientCredentials (%s)", c.Method)
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)
Expand Down Expand Up @@ -114,9 +115,15 @@ func (c *Credentials) GetHttpClientAndHeaderOverrides() (*http.Client, []*Header
ClientID: c.Config.ClientCredentialsClientId,
ClientSecret: c.Config.ClientCredentialsClientSecret,
TokenURL: fmt.Sprintf("https://%s/oauth/token", c.Config.ClientCredentialsApiTokenIssuer),
EndpointParams: map[string][]string{
}
if c.Config.ClientCredentialsApiAudience != "" {
ccConfig.EndpointParams = map[string][]string{
"audience": {c.Config.ClientCredentialsApiAudience},
},
}
}
if c.Config.ClientCredentialsScopes != "" {
scopes := strings.Split(strings.TrimSpace(c.Config.ClientCredentialsScopes), " ")
ccConfig.Scopes = append(ccConfig.Scopes, scopes...)
}
client = ccConfig.Client(context.Background())
case CredentialsMethodApiToken:
Expand Down

0 comments on commit 8bbf9d6

Please sign in to comment.