Skip to content

Commit

Permalink
feat: oauth2 client credentials support (#62)
Browse files Browse the repository at this point in the history
  • Loading branch information
le-yams authored Dec 16, 2023
1 parent 6d03bf4 commit 2ce2077
Show file tree
Hide file tree
Showing 3 changed files with 81 additions and 38 deletions.
34 changes: 33 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ func main() {
}
```

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

```golang
import (
Expand Down Expand Up @@ -182,6 +182,38 @@ func main() {
}
```

#### OAuth2 Client Credentials

```golang
import (
openfga "github.com/openfga/go-sdk"
. "github.com/openfga/go-sdk/client"
"github.com/openfga/go-sdk/credentials"
"os"
)

func main() {
fgaClient, err := NewSdkClient(&ClientConfiguration{
ApiUrl: os.Getenv("FGA_API_URL"), // required, e.g. https://api.fga.example
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
}
}
```


### Get your Store ID

Expand Down
68 changes: 36 additions & 32 deletions api_open_fga_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ func TestOpenFgaApiConfiguration(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.fga.example",
StoreId: "01GXSB9YR785C4FYS3C0RTG7B2",
Expand Down Expand Up @@ -172,23 +172,6 @@ func TestOpenFgaApiConfiguration(t *testing.T) {
t.Fatalf("Expected an error: client secret is required")
}

_, err = NewConfiguration(Configuration{
ApiHost: "https://api.fga.example",
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.fga.example",
StoreId: "01GXSB9YR785C4FYS3C0RTG7B2",
Expand Down Expand Up @@ -263,20 +246,8 @@ func TestOpenFgaApiConfiguration(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.fga.example",
StoreId: "01GXSB9YR785C4FYS3C0RTG7B2",
Credentials: &credentials.Credentials{
Method: credentials.CredentialsMethodClientCredentials,
Config: &credentials.Config{
ClientCredentialsClientId: "some-id",
ClientCredentialsClientSecret: "some-secret",
ClientCredentialsApiAudience: "some-audience",
ClientCredentialsApiTokenIssuer: "tokenissuer.fga.example",
},
},
})
clientCredentialsFirstRequestTest := func(t *testing.T, config Configuration) {
configuration, err := NewConfiguration(config)
if err != nil {
t.Fatalf("%v", err)
}
Expand Down Expand Up @@ -329,6 +300,39 @@ func TestOpenFgaApiConfiguration(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.fga.example",
StoreId: "01GXSB9YR785C4FYS3C0RTG7B2",
Credentials: &credentials.Credentials{
Method: credentials.CredentialsMethodClientCredentials,
Config: &credentials.Config{
ClientCredentialsClientId: "some-id",
ClientCredentialsClientSecret: "some-secret",
ClientCredentialsApiAudience: "some-audience",
ClientCredentialsApiTokenIssuer: "tokenissuer.fga.example",
},
},
})
})
t.Run("with OAuth2 configuration", func(t *testing.T) {
clientCredentialsFirstRequestTest(t, Configuration{
ApiHost: "api.fga.example",
StoreId: "01GXSB9YR785C4FYS3C0RTG7B2",
Credentials: &credentials.Credentials{
Method: credentials.CredentialsMethodClientCredentials,
Config: &credentials.Config{
ClientCredentialsClientId: "some-id",
ClientCredentialsClientSecret: "some-secret",
ClientCredentialsScopes: "scope1 scope2",
ClientCredentialsApiTokenIssuer: "tokenissuer.fga.example",
},
},
})
})
})

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

"github.com/openfga/go-sdk/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 2ce2077

Please sign in to comment.