Skip to content
Open
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
37 changes: 1 addition & 36 deletions internal/cli/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,12 +149,6 @@ func testLoginCmd(cli *cli) *cobra.Command {
return nil
}

if inputs.Audience != "" && (client.GetAppType() == appTypeNonInteractive) {
if err := checkClientIsAuthorizedForAPI(cmd.Context(), cli, client, inputs.Audience); err != nil {
return err
}
}

Comment on lines -152 to -157
Copy link
Copy Markdown
Author

@ttstarck ttstarck Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if inputs.Organization != "" {
if inputs.CustomParams != nil {
inputs.CustomParams["organization"] = inputs.Organization
Expand Down Expand Up @@ -262,7 +256,7 @@ func testTokenCmd(cli *cli) *cobra.Command {
cli.renderer.Warnf("Passed in scopes do not apply to Machine to Machine applications.\n")
}

tokenResponse, err = runClientCredentialsFlow(cmd.Context(), cli, client, inputs.Audience, cli.tenant)
tokenResponse, err = runClientCredentialsFlow(cmd.Context(), cli, client, inputs.Audience, cli.tenant, inputs.Organization)
if err != nil {
return fmt.Errorf(
"failed to log in with client credentials for client with ID %q: %w",
Expand Down Expand Up @@ -504,33 +498,4 @@ func (c *cli) pickTokenScopes(ctx context.Context, inputs *testCmdInputs) error
return survey.AskOne(scopesPrompt, &inputs.Scopes)
}

func checkClientIsAuthorizedForAPI(ctx context.Context, cli *cli, client *management.Client, audience string) error {
var list *management.ClientGrantList
if err := ansi.Waiting(func() (err error) {
list, err = cli.api.ClientGrant.List(
ctx,
management.Parameter("audience", audience),
management.Parameter("client_id", client.GetClientID()),
)
return err
}); err != nil {
return fmt.Errorf(
"failed to find client grants for API identifier %q and client ID %q: %w",
audience,
client.GetClientID(),
err,
)
}

if len(list.ClientGrants) < 1 {
return fmt.Errorf(
"the %s application is not authorized to request access tokens for this API %s.\n\n"+
"Run: 'auth0 apps open %s' to open the dashboard and authorize the application.",
ansi.Bold(client.GetName()),
ansi.Bold(audience),
client.GetClientID(),
)
}

return nil
}
50 changes: 47 additions & 3 deletions internal/cli/utils_shared.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,16 @@ func BuildOauthTokenURL(domain string) string {
return u.String()
}

func BuildOauthTokenParams(clientID, clientSecret, audience string) url.Values {
func BuildOauthTokenParams(clientID, clientSecret, audience, organization string) url.Values {
q := url.Values{
"audience": {audience},
"client_id": {clientID},
"client_secret": {clientSecret},
"grant_type": {"client_credentials"},
}
if organization != "" {
q.Set("organization", organization)
}
return q
}

Expand All @@ -64,13 +67,14 @@ func runClientCredentialsFlow(
client *management.Client,
audience string,
tenantDomain string,
organization string,
) (*authutil.TokenResponse, error) {
if err := checkClientIsAuthorizedForAPI(ctx, cli, client, audience); err != nil {
if err := checkClientIsAuthorizedForAPI(ctx, cli, client, audience, organization); err != nil {
return nil, err
}

tokenURL := BuildOauthTokenURL(tenantDomain)
payload := BuildOauthTokenParams(client.GetClientID(), client.GetClientSecret(), audience)
payload := BuildOauthTokenParams(client.GetClientID(), client.GetClientSecret(), audience, organization)

var tokenResponse *authutil.TokenResponse
err := ansi.Spinner("Waiting for token", func() error {
Expand All @@ -92,6 +96,46 @@ func runClientCredentialsFlow(
return tokenResponse, err
}

func checkClientIsAuthorizedForAPI(ctx context.Context, cli *cli, client *management.Client, audience, organization string) error {
var list *management.ClientGrantList
if err := ansi.Waiting(func() (err error) {
list, err = cli.api.ClientGrant.List(
ctx,
management.Parameter("audience", audience),
management.Parameter("client_id", client.GetClientID()),
)
return err
}); err != nil {
return fmt.Errorf(
"failed to find client grants for API identifier %q and client ID %q: %w",
audience,
client.GetClientID(),
err,
)
}

if len(list.ClientGrants) < 1 {
return fmt.Errorf(
"the %s application is not authorized to request access tokens for this API %s.\n\n"+
"Run: 'auth0 apps open %s' to open the dashboard and authorize the application.",
ansi.Bold(client.GetName()),
ansi.Bold(audience),
client.GetClientID(),
)
}

grant := list.ClientGrants[0]
if grant.GetOrganizationUsage() == "require" && organization == "" {
return fmt.Errorf(
"the client grant for %s requires an organization.\n\n"+
"Use the --organization flag to specify one.",
ansi.Bold(audience),
)
}

return nil
}

// runLoginFlowPreflightChecks checks if we need to make any updates
// to the client being tested in order to log in successfully.
// If so, it asks the user to confirm whether to proceed.
Expand Down
90 changes: 89 additions & 1 deletion internal/cli/utils_shared_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,96 @@ func TestBuildOauthTokenURL(t *testing.T) {
}

func TestBuildOauthTokenParams(t *testing.T) {
params := BuildOauthTokenParams("some-client-id", "some-client-secret", "https://cli-demo.auth0.us.auth0.com/api/v2/")
params := BuildOauthTokenParams("some-client-id", "some-client-secret", "https://cli-demo.auth0.us.auth0.com/api/v2/", "")
assert.Equal(t, "audience=https%3A%2F%2Fcli-demo.auth0.us.auth0.com%2Fapi%2Fv2%2F&client_id=some-client-id&client_secret=some-client-secret&grant_type=client_credentials", params.Encode())

params = BuildOauthTokenParams("some-client-id", "some-client-secret", "https://cli-demo.auth0.us.auth0.com/api/v2/", "org_abc123")
assert.Equal(t, "audience=https%3A%2F%2Fcli-demo.auth0.us.auth0.com%2Fapi%2Fv2%2F&client_id=some-client-id&client_secret=some-client-secret&grant_type=client_credentials&organization=org_abc123", params.Encode())
}

func TestCheckClientIsAuthorizedForAPI(t *testing.T) {
const audience = "https://cli-demo.us.auth0.com/api/v2/"

client := &management.Client{
ClientID: auth0.String("some-client-id"),
Name: auth0.String("some-client-name"),
}

tests := []struct {
name string
organization string
grantList *management.ClientGrantList
apiError error
expectedError string
}{
{
name: "no grant exists",
organization: "",
grantList: &management.ClientGrantList{},
expectedError: "the some-client-name application is not authorized to request access tokens for this API " +
audience,
},
{
name: "api error",
organization: "",
apiError: errors.New("unexpected error"),
expectedError: "failed to find client grants for API identifier " +
"\"" + audience + "\" and client ID \"some-client-id\": unexpected error",
},
{
name: "grant exists, no org required",
organization: "",
grantList: &management.ClientGrantList{
ClientGrants: []*management.ClientGrant{
{OrganizationUsage: auth0.String("allow")},
},
},
},
{
name: "grant requires org, org provided",
organization: "org_abc123",
grantList: &management.ClientGrantList{
ClientGrants: []*management.ClientGrant{
{OrganizationUsage: auth0.String("require")},
},
},
},
{
name: "grant requires org, no org provided",
organization: "",
grantList: &management.ClientGrantList{
ClientGrants: []*management.ClientGrant{
{OrganizationUsage: auth0.String("require")},
},
},
expectedError: "the client grant for " + audience + " requires an organization.\n\n" +
"Use the --organization flag to specify one.",
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()

clientGrantAPI := mock.NewMockClientGrantAPI(ctrl)
clientGrantAPI.EXPECT().
List(gomock.Any(), gomock.Any()).
Return(test.grantList, test.apiError)

cli := &cli{
api: &auth0.API{ClientGrant: clientGrantAPI},
}

err := checkClientIsAuthorizedForAPI(context.Background(), cli, client, audience, test.organization)

if test.expectedError != "" {
assert.ErrorContains(t, err, test.expectedError)
} else {
assert.NoError(t, err)
}
})
}
}

func TestHasLocalCallbackURL(t *testing.T) {
Expand Down