Skip to content

Commit

Permalink
Add support for using Organizations with Client Grants (#309)
Browse files Browse the repository at this point in the history
Co-authored-by: Rita Zerrizuela <[email protected]>
  • Loading branch information
ewanharris and Widcket authored Nov 9, 2023
1 parent 065cdb3 commit 03f7feb
Show file tree
Hide file tree
Showing 12 changed files with 982 additions and 6 deletions.
4 changes: 4 additions & 0 deletions authentication/oauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,10 @@ func (o *OAuth) LoginWithClientCredentials(ctx context.Context, body oauth.Login
"audience": []string{body.Audience},
}

if body.Organization != "" {
data.Set("organization", body.Organization)
}

err = o.addClientAuthentication(body.ClientAuthentication, data, true)

if err != nil {
Expand Down
2 changes: 2 additions & 0 deletions authentication/oauth/oauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ type LoginWithClientCredentialsRequest struct {
Audience string
// Extra parameters to be merged into the request body. Values set here will override any existing values.
ExtraParameters map[string]string
// And organization name or ID. When included, the access token will include the `org_id` or `org_name` claim.
Organization string
}

// RefreshTokenRequest defines the request body for logging in with Authorization Code grant.
Expand Down
16 changes: 16 additions & 0 deletions authentication/oauth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,22 @@ func TestLoginWithClientCredentials(t *testing.T) {

assert.ErrorContains(t, err, "Unsupported client assertion algorithm \"invalid-alg\" provided")
})

t.Run("Should support passing an organization", func(t *testing.T) {
configureHTTPTestRecordings(t)

tokenSet, err := authAPI.OAuth.LoginWithClientCredentials(context.Background(), oauth.LoginWithClientCredentialsRequest{
ClientAuthentication: oauth.ClientAuthentication{
ClientSecret: clientSecret,
},
Audience: "my-api",
Organization: "org_test",
}, oauth.IDTokenValidationOptions{})

assert.NoError(t, err)
assert.NotEmpty(t, tokenSet.AccessToken)
assert.Equal(t, "Bearer", tokenSet.TokenType)
})
}

func TestRefreshToken(t *testing.T) {
Expand Down
19 changes: 18 additions & 1 deletion management/client_grant.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,15 @@ type ClientGrant struct {
// The audience.
Audience *string `json:"audience,omitempty"`

Scope []string `json:"scope"`
Scope []string `json:"scope,omitempty"`

// If enabled, any organization can be used with this grant.
// If disabled (default), the grant must be explicitly assigned to the desired organizations.
AllowAnyOrganization *bool `json:"allow_any_organization,omitempty"`

// Defines whether organizations can be used with client credentials exchanges for this grant.
// Can be one of `deny`, `allow`, or `require`. Defaults to `deny` when not defined.
OrganizationUsage *string `json:"organization_usage,omitempty"`
}

// ClientGrantList is a list of ClientGrants.
Expand Down Expand Up @@ -87,3 +95,12 @@ func (m *ClientGrantManager) List(ctx context.Context, opts ...RequestOption) (g
err = m.management.Request(ctx, "GET", m.management.URI("client-grants"), &gs, applyListDefaults(opts))
return
}

// Organizations lists client grants associated to an organization.
//
// This method forces the `include_totals=true` and defaults to `per_page=50` if
// not provided.
func (m *ClientGrantManager) Organizations(ctx context.Context, id string, opts ...RequestOption) (o *OrganizationList, err error) {
err = m.management.Request(ctx, "GET", m.management.URI("client-grants", id, "organizations"), &o, applyListDefaults(opts))
return
}
31 changes: 26 additions & 5 deletions management/client_grant_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/auth0/go-auth0"
)

func TestClientGrantManager_Create(t *testing.T) {
Expand All @@ -31,7 +33,7 @@ func TestClientGrantManager_Create(t *testing.T) {
func TestClientGrantManager_Read(t *testing.T) {
configureHTTPTestRecordings(t)

expectedClientGrant := givenAClientGrant(t)
expectedClientGrant := givenAClientGrant(t, false)

actualClientGrant, err := api.ClientGrant.Read(context.Background(), expectedClientGrant.GetID())

Expand All @@ -44,7 +46,7 @@ func TestClientGrantManager_Read(t *testing.T) {
func TestClientGrantManager_Update(t *testing.T) {
configureHTTPTestRecordings(t)

expectedClientGrant := givenAClientGrant(t)
expectedClientGrant := givenAClientGrant(t, false)

clientGrantID := expectedClientGrant.GetID()

Expand All @@ -64,7 +66,7 @@ func TestClientGrantManager_Update(t *testing.T) {
func TestClientGrantManager_Delete(t *testing.T) {
configureHTTPTestRecordings(t)

expectedClientGrant := givenAClientGrant(t)
expectedClientGrant := givenAClientGrant(t, false)

err := api.ClientGrant.Delete(context.Background(), expectedClientGrant.GetID())
assert.NoError(t, err)
Expand All @@ -77,7 +79,7 @@ func TestClientGrantManager_Delete(t *testing.T) {
func TestClientGrantManager_List(t *testing.T) {
configureHTTPTestRecordings(t)

expectedClientGrant := givenAClientGrant(t)
expectedClientGrant := givenAClientGrant(t, false)

clientGrantList, err := api.ClientGrant.List(
context.Background(),
Expand All @@ -88,7 +90,21 @@ func TestClientGrantManager_List(t *testing.T) {
assert.Equal(t, len(clientGrantList.ClientGrants), 1)
}

func givenAClientGrant(t *testing.T) (clientGrant *ClientGrant) {
func TestClientGrantManager_Organizations(t *testing.T) {
configureHTTPTestRecordings(t)

org := givenAnOrganization(t)
clientGrant := givenAClientGrant(t, true)

err := api.Organization.AssociateClientGrant(context.Background(), org.GetID(), clientGrant.GetID())
require.NoError(t, err)

associatedOrg, err := api.ClientGrant.Organizations(context.Background(), clientGrant.GetID())
require.NoError(t, err)
assert.Equal(t, org.GetID(), associatedOrg.Organizations[0].GetID())
}

func givenAClientGrant(t *testing.T, allowOrganizations bool) (clientGrant *ClientGrant) {
t.Helper()

client := givenAClient(t)
Expand All @@ -100,6 +116,11 @@ func givenAClientGrant(t *testing.T) (clientGrant *ClientGrant) {
Scope: []string{"create:resource"},
}

if allowOrganizations {
clientGrant.AllowAnyOrganization = auth0.Bool(true)
clientGrant.OrganizationUsage = auth0.String("allow")
}

err := api.ClientGrant.Create(context.Background(), clientGrant)
require.NoError(t, err)

Expand Down
16 changes: 16 additions & 0 deletions management/management.gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions management/management.gen_test.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 23 additions & 0 deletions management/organization.go
Original file line number Diff line number Diff line change
Expand Up @@ -379,3 +379,26 @@ func (m *OrganizationManager) DeleteMemberRoles(ctx context.Context, id string,
err = m.management.Request(ctx, "DELETE", m.management.URI("organizations", id, "members", memberID, "roles"), &body, opts...)
return
}

// ClientGrants retrieves the client grants assigned to an organization.
func (m *OrganizationManager) ClientGrants(ctx context.Context, id string, opts ...RequestOption) (g *ClientGrantList, err error) {
err = m.management.Request(ctx, "GET", m.management.URI("organizations", id, "client-grants"), &g, applyListDefaults(opts))
return
}

// AssociateClientGrant assigns a client grant to an organization.
func (m *OrganizationManager) AssociateClientGrant(ctx context.Context, id string, grantID string, opts ...RequestOption) (err error) {
body := struct {
GrantID string `json:"grant_id"`
}{
GrantID: grantID,
}
err = m.management.Request(ctx, "POST", m.management.URI("organizations", id, "client-grants"), &body, applyListDefaults(opts))
return
}

// RemoveClientGrant removes a client grant from an organization.
func (m *OrganizationManager) RemoveClientGrant(ctx context.Context, id string, grantID string, opts ...RequestOption) (err error) {
err = m.management.Request(ctx, "DELETE", m.management.URI("organizations", id, "client-grants", grantID), nil, applyListDefaults(opts))
return
}
22 changes: 22 additions & 0 deletions management/organization_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,28 @@ func TestOrganizationManager_MemberRoles(t *testing.T) {
assert.Len(t, roles.Roles, 0)
}

func TestOrganizationManager_ClientGrants(t *testing.T) {
configureHTTPTestRecordings(t)

org := givenAnOrganization(t)
clientGrant := givenAClientGrant(t, true)

err := api.Organization.AssociateClientGrant(context.Background(), org.GetID(), clientGrant.GetID())
require.NoError(t, err)

associatedGrants, err := api.Organization.ClientGrants(context.Background(), org.GetID())
require.NoError(t, err)
assert.Len(t, associatedGrants.ClientGrants, 1)
assert.Equal(t, clientGrant.GetID(), associatedGrants.ClientGrants[0].GetID())

err = api.Organization.RemoveClientGrant(context.Background(), org.GetID(), clientGrant.GetID())
require.NoError(t, err)

associatedGrants, err = api.Organization.ClientGrants(context.Background(), org.GetID())
require.NoError(t, err)
assert.Len(t, associatedGrants.ClientGrants, 0)
}

func givenAnOrganization(t *testing.T) *Organization {
org := &Organization{
Name: auth0.String(fmt.Sprintf("test-organization%v", rand.Intn(999))),
Expand Down
Loading

0 comments on commit 03f7feb

Please sign in to comment.