Skip to content
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

DXCDT-296: Supporting additional scopes when authenticating as user #561

Merged
merged 21 commits into from
Dec 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
253f880
Merge branch 'v1' of https://github.com/auth0/auth0-cli into v1
willvedd Dec 5, 2022
4cea50f
Merge branch 'v1' of https://github.com/auth0/auth0-cli into v1
willvedd Dec 13, 2022
2cb6f19
Adding additional scopes support via --scopes flag
willvedd Dec 15, 2022
06723ec
Adding additional scopes support via --scopes flag
willvedd Dec 15, 2022
73ea1c9
Removing logging
willvedd Dec 15, 2022
d55fd0d
Uncommenting scope, removing Start function
willvedd Dec 15, 2022
1aef346
Merge branch 'v1' into DXCDT-296-dynamic-scoping
willvedd Dec 15, 2022
b96c834
Condensing error to single line
willvedd Dec 15, 2022
c3541b0
Merge branch 'DXCDT-296-dynamic-scoping' of https://github.com/auth0/…
willvedd Dec 15, 2022
607a197
Fixing linting errors
willvedd Dec 15, 2022
e8df004
Merge branch 'v1' into DXCDT-296-dynamic-scoping
willvedd Dec 15, 2022
7d58fa9
Changing test
willvedd Dec 15, 2022
cb0e08f
Merge branch 'DXCDT-296-dynamic-scoping' of https://github.com/auth0/…
willvedd Dec 15, 2022
a2f5860
Updating docs
willvedd Dec 15, 2022
9be2ed0
Unpluralizing text, setting nil default value
willvedd Dec 15, 2022
d5caf06
Fixing bad help text
willvedd Dec 15, 2022
976f9a2
Merge branch 'v1' into DXCDT-296-dynamic-scoping
Widcket Dec 16, 2022
e23b9b2
Tiny refactors on the login cmd
sergiught Dec 16, 2022
d823184
Merge branch 'DXCDT-296-dynamic-scoping' of https://github.com/auth0/…
willvedd Dec 16, 2022
03bea62
Fixing linting error
willvedd Dec 16, 2022
cc62501
Update internal/auth/auth.go
sergiught Dec 16, 2022
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
2 changes: 2 additions & 0 deletions docs/auth0_login.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ auth0 login [flags]
```
auth0 login
auth0 login --domain <tenant-domain> --client-id <client-id> --client-secret <client-secret>
auth0 login --scopes "read:client_grants,create:client_grants"
```

### Options
Expand All @@ -27,6 +28,7 @@ auth0 login --domain <tenant-domain> --client-id <client-id> --client-secret <cl
--client-secret string Client secret of the application when authenticating via client credentials.
--domain string Tenant domain of the application when authenticating via client credentials.
-h, --help help for login
--scopes strings Additional scopes to request when authenticating via device code flow. By default, only scopes for first-class functions are requested. Primarily useful when using the api command to execute arbitrary Management API requests.
Copy link
Contributor

Choose a reason for hiding this comment

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

Any way we can support the use case mentioned in #556 (only X scopes when scripting)? Note that as we add support for more and more resources, the number of scopes requested initially will keep going up. This is probably fine for human usage, but for machine usage we'll probably want to find an alternative.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Absolutely. However, that is a separate task. This PR only pertains to scoping with respect to device code flow, whereas #556 is concerned with client credentials. I think that work is better suited for its own PR.

```

### Options inherited from parent commands
Expand Down
14 changes: 0 additions & 14 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -206,21 +206,8 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.3.0 h1:VWL6FNY2bEEmsGVKabSlHu5Irp34xmMRoqb/9lF9lxk=
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.3.0 h1:6l90koy8/LaBLmLu8jpHeHexzMwEita0zFfYlggy2F8=
golang.org/x/oauth2 v0.3.0/go.mod h1:rQrIauxkUhJ6CuwEXwymO2/eh4xz2ZWF1nBkcxS+tGk=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
Expand Down Expand Up @@ -248,7 +235,6 @@ golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI=
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
Expand Down
89 changes: 26 additions & 63 deletions internal/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,50 +25,23 @@ const (

var requiredScopes = []string{
"openid",
"offline_access", // This is used to retrieve a refresh token.
"create:clients", "read:clients", "update:clients", "delete:clients",
"read:client_keys",
"create:client_grants", "read:client_grants", "update:client_grants", "delete:client_grants",
"create:resource_servers", "read:resource_servers", "update:resource_servers", "delete:resource_servers",
"create:connections", "read:connections", "update:connections", "delete:connections",
"create:users", "read:users", "update:users", "delete:users",
"create:roles", "read:roles", "update:roles", "delete:roles",
"create:actions", "read:actions", "update:actions", "delete:actions",
"read:triggers", "update:triggers",
"create:rules", "read:rules", "update:rules", "delete:rules",
"read:rules_configs", "update:rules_configs", "delete:rules_configs",
"create:hooks", "read:hooks", "update:hooks", "delete:hooks",
"read:attack_protection", "update:attack_protection",
"create:organizations", "read:organizations", "update:organizations", "delete:organizations",
"create:organization_members", "read:organization_members", "delete:organization_members",
"create:organization_connections", "read:organization_connections", "update:organization_connections", "delete:organization_connections",
"create:organization_member_roles", "read:organization_member_roles", "delete:organization_member_roles",
"create:organization_invitations", "read:organization_invitations", "delete:organization_invitations",
"read:prompts", "update:prompts",
"read:branding", "update:branding", "delete:branding",
"create:custom_domains", "read:custom_domains", "update:custom_domains", "delete:custom_domains",
"create:email_provider", "read:email_provider", "update:email_provider", "delete:email_provider",
"create:email_templates", "read:email_templates", "update:email_templates",
"read:tenant_settings", "update:tenant_settings",
"offline_access", // for retrieving refresh token
"create:clients", "delete:clients", "read:clients", "update:clients",
"create:resource_servers", "delete:resource_servers", "read:resource_servers", "update:resource_servers",
"create:roles", "delete:roles", "read:roles", "update:roles",
"create:rules", "delete:rules", "read:rules", "update:rules",
"create:users", "delete:users", "read:users", "update:users",
"read:branding", "update:branding",
"read:email_templates", "update:email_templates",
"read:connections", "update:connections",
"read:client_keys", "read:logs", "read:tenant_settings",
"read:custom_domains", "create:custom_domains", "update:custom_domains", "delete:custom_domains",
"read:anomaly_blocks", "delete:anomaly_blocks",
"create:log_streams", "read:log_streams", "update:log_streams", "delete:log_streams",
"read:stats",
"read:insights",
"read:logs",
"create:shields", "read:shields", "update:shields", "delete:shields",
"create:users_app_metadata", "read:users_app_metadata", "update:users_app_metadata", "delete:users_app_metadata",
"create:user_custom_blocks", "read:user_custom_blocks", "delete:user_custom_blocks",
"create:user_tickets",
"blacklist:tokens",
"read:grants", "delete:grants",
"read:mfa_policies", "update:mfa_policies",
"read:guardian_factors", "update:guardian_factors",
"read:guardian_enrollments", "delete:guardian_enrollments",
"create:guardian_enrollment_tickets",
"read:user_idp_tokens",
"create:passwords_checking_job", "delete:passwords_checking_job",
"read:limits", "update:limits",
"read:entitlements",
"create:log_streams", "delete:log_streams", "read:log_streams", "update:log_streams",
"create:actions", "delete:actions", "read:actions", "update:actions",
"create:organizations", "delete:organizations", "read:organizations", "update:organizations", "read:organization_members", "read:organization_member_roles",
"read:prompts", "update:prompts",
"read:attack_protection", "update:attack_protection",
}

// Authenticator is used to facilitate the login process.
Expand Down Expand Up @@ -123,7 +96,7 @@ func (s *State) IntervalDuration() time.Duration {
return time.Duration(s.Interval+waitThresholdInSeconds) * time.Second
}

// New returns a new instance of Authenticator using Auth0 Public Cloud default values
// New returns a new instance of Authenticator using Auth0 Public Cloud default values.
func New() *Authenticator {
return &Authenticator{
Audience: "https://*.auth0.com/api/v2/",
Expand All @@ -133,18 +106,6 @@ func New() *Authenticator {
}
}

// Start kicks-off the device authentication flow by requesting
// a device code from Auth0. The returned state contains the
// URI for the next step of the flow.
func (a *Authenticator) Start(ctx context.Context) (State, error) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

A bit of a refactor here. Removing the Start function because it only exists as a function wrapper for getDeviceCode. Further, the name wasn't descriptive of what the function did.

state, err := a.getDeviceCode(ctx)
if err != nil {
return State{}, fmt.Errorf("failed to get the device code: %w", err)
}

return state, nil
}

// Wait waits until the user is logged in on the browser.
func (a *Authenticator) Wait(ctx context.Context, state State) (Result, error) {
t := time.NewTicker(state.IntervalDuration())
Expand Down Expand Up @@ -205,10 +166,13 @@ func (a *Authenticator) Wait(ctx context.Context, state State) (Result, error) {
}
}

func (a *Authenticator) getDeviceCode(ctx context.Context) (State, error) {
// GetDeviceCode kicks-off the device authentication flow by requesting
// a device code from Auth0. The returned state contains the
// URI for the next step of the flow.
func (a *Authenticator) GetDeviceCode(ctx context.Context, additionalScopes []string) (State, error) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The only functional change here is the addition of the additionalScopes argument, which enables the passing of additional scopes when requesting a grant.

data := url.Values{
"client_id": []string{a.ClientID},
"scope": []string{strings.Join(requiredScopes, " ")},
"scope": []string{strings.Join(append(requiredScopes, additionalScopes...), " ")},
"audience": []string{a.Audience},
}

Expand All @@ -230,7 +194,7 @@ func (a *Authenticator) getDeviceCode(ctx context.Context) (State, error) {
}
defer response.Body.Close()

if response.StatusCode < http.StatusOK || response.StatusCode >= http.StatusBadRequest {
if response.StatusCode != http.StatusOK {
bodyBytes, err := io.ReadAll(response.Body)
if err != nil {
return State{}, fmt.Errorf(
Expand All @@ -243,8 +207,7 @@ func (a *Authenticator) getDeviceCode(ctx context.Context) (State, error) {
}

var state State
err = json.NewDecoder(response.Body).Decode(&state)
if err != nil {
if err = json.NewDecoder(response.Body).Decode(&state); err != nil {
return State{}, fmt.Errorf("failed to decode the response: %w", err)
}

Expand Down Expand Up @@ -277,14 +240,14 @@ func parseTenant(accessToken string) (tenant, domain string, err error) {
return "", "", fmt.Errorf("audience not found for %s", audiencePath)
}

// ClientCredentials encapsulates all data to facilitate access token creation with client credentials (client ID and client secret)
// ClientCredentials encapsulates all data to facilitate access token creation with client credentials (client ID and client secret).
type ClientCredentials struct {
ClientID string
ClientSecret string
Domain string
}

// GetAccessTokenFromClientCreds generates an access token from client credentials
// GetAccessTokenFromClientCreds generates an access token from client credentials.
func GetAccessTokenFromClientCreds(ctx context.Context, args ClientCredentials) (Result, error) {
u, err := url.Parse("https://" + args.Domain)
if err != nil {
Expand Down
36 changes: 9 additions & 27 deletions internal/auth/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,15 @@ func TestRequiredScopes(t *testing.T) {
t.Run("Verify CRUD scopes", func(t *testing.T) {
crudResources := []string{
"clients",
"client_grants",
"connections",
"log_streams",
"resource_servers",
"roles",
"rules",
"users",
"custom_domains",
"log_streams",
"actions",
"hooks",
"organizations",
"organization_connections",
"custom_domains",
"email_provider",
"shields",
"users_app_metadata",
}
crudPrefixes := []string{"create:", "delete:", "read:", "update:"}

Expand All @@ -37,27 +31,15 @@ func TestRequiredScopes(t *testing.T) {

t.Run("Verify special scopes", func(t *testing.T) {
list := []string{
"read:branding", "update:branding", "delete:branding",
"read:triggers", "update:triggers",
"read:client_keys",
"read:logs",
"read:tenant_settings", "update:tenant_settings",
"read:branding", "update:branding",
"read:connections", "update:connections",
"read:email_templates", "update:email_templates",
"read:custom_domains", "create:custom_domains", "update:custom_domains", "delete:custom_domains",
"read:client_keys", "read:logs", "read:tenant_settings",
"read:anomaly_blocks", "delete:anomaly_blocks",
"read:attack_protection", "update:attack_protection",
"read:organization_members", "read:organization_member_roles",
"read:prompts", "update:prompts",
"read:stats",
"read:insights",
"create:user_tickets",
"blacklist:tokens",
"read:grants", "delete:grants",
"read:mfa_policies", "update:mfa_policies",
"read:guardian_factors", "update:guardian_factors",
"read:guardian_enrollments", "delete:guardian_enrollments",
"create:guardian_enrollment_tickets",
"read:user_idp_tokens",
"create:passwords_checking_job", "delete:passwords_checking_job",
"read:limits", "update:limits",
"read:entitlements",
"read:attack_protection", "update:attack_protection",
}

for _, v := range list {
Expand Down
35 changes: 35 additions & 0 deletions internal/cli/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,10 @@ func apiCmdRun(cli *cli, inputs *apiCmdInputs) func(cmd *cobra.Command, args []s
}
defer response.Body.Close()

if err := isInsufficientScopeError(response); err != nil {
return err
}

rawBodyJSON, err := io.ReadAll(response.Body)
if err != nil {
return err
Expand Down Expand Up @@ -257,3 +261,34 @@ func (i *apiCmdInputs) parseRaw(args []string) {

i.RawURI = args[lenArgs-1]
}

func isInsufficientScopeError(r *http.Response) error {
if r.StatusCode != 403 {
return nil
}

type ErrorBody struct {
ErrorCode string `json:"errorCode"`
Message string `json:"message"`
}

var body ErrorBody
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
return nil
}

if body.ErrorCode != "insufficient_scope" {
return nil
}

missingScopes := strings.Split(body.Message, "Insufficient scope, expected any of: ")[1]
recommendedScopeToAdd := strings.Split(missingScopes, ",")[0]

return fmt.Errorf(
"request failed because access token lacks scope: %s.\n "+
"If authenticated via client credentials, add this scope to the designated client. "+
"If authenticated as a user, request this scope during login by running `auth0 login --scopes %s`.",
recommendedScopeToAdd,
recommendedScopeToAdd,
)
}
73 changes: 73 additions & 0 deletions internal/cli/api_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package cli

import (
"bytes"
"io"
"net/http"
"testing"

Expand Down Expand Up @@ -85,3 +87,74 @@ func TestAPICmdInputs_FromArgs(t *testing.T) {
})
}
}

func TestAPICmd_IsInsufficientScopeError(t *testing.T) {
var testCases = []struct {
name string
inputStatusCode int
inputResponseBody string
expectedError string
}{
{
name: "it does not detect 404 error",
inputStatusCode: 404,
inputResponseBody: `{
"statusCode": 404,
"error": "Not Found",
"message": "Not Found"
}`,
expectedError: "",
},
{
name: "it does not detect a 200 HTTP response",
inputStatusCode: 200,
inputResponseBody: `{
"allowed_logout_urls": [],
"change_password": {
"enabled": true,
"html": "<html>LOL</html>"
},
"default_audience": "",
}`,
expectedError: "",
},
{
name: "it correctly detects an insufficient scope error",
inputStatusCode: 403,
inputResponseBody: `{
"statusCode": 403,
"error": "Forbidden",
"message": "Insufficient scope, expected any of: create:client_grants",
"errorCode": "insufficient_scope"
}`,
expectedError: "request failed because access token lacks scope: create:client_grants.\n If authenticated via client credentials, add this scope to the designated client. If authenticated as a user, request this scope during login by running `auth0 login --scopes create:client_grants`.",
},
{
name: "it correctly detects an insufficient scope error with multiple scope",
inputStatusCode: 403,
inputResponseBody: `{
"statusCode": 403,
"error": "Forbidden",
"message": "Insufficient scope, expected any of: read:clients, read:client_summary",
"errorCode": "insufficient_scope"
}`,
expectedError: "request failed because access token lacks scope: read:clients.\n If authenticated via client credentials, add this scope to the designated client. If authenticated as a user, request this scope during login by running `auth0 login --scopes read:clients`.",
},
}

for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
input := http.Response{
Body: io.NopCloser(bytes.NewReader([]byte(testCase.inputResponseBody))),
StatusCode: testCase.inputStatusCode,
}

err := isInsufficientScopeError(&input)
if testCase.expectedError == "" {
assert.NoError(t, err)
} else {
assert.EqualError(t, err, testCase.expectedError)
}
})
}
}
Loading