diff --git a/README.md b/README.md index d3efbf47a..e2fa7031e 100644 --- a/README.md +++ b/README.md @@ -220,6 +220,17 @@ Failed Login hello 7 minutes ago N/A my awesome app userid: auth0|QXV0aDAgaXMgaGlyaW5nISBhdXRoMC5jb20vY2FyZWVycyAK ``` +## Customization + +The authenticator of the CLI defaults to the default Auth0 cloud `auth0.auth0.com`. This can be customized for personalized cloud offerings by setting the following env variables: + +``` + AUTH0_AUDIENCE - The audience of the Auth0 Management API (System API) to use. + AUTH0_CLIENT_ID - Client ID of an application configured with the Device Code grant type. + AUTH0_DEVICE_CODE_ENDPOINT - Device Authorization URL + AUTH0_OAUTH_TOKEN_ENDPOINT - OAuth Token URL +``` + ## Anonymous Analytics By default, the CLI tracks some anonymous usage events. This helps us understand how the CLI is being used, so we can continue to improve it. You can opt-out by setting the environment variable `AUTH0_CLI_ANALYTICS` to `false`. diff --git a/internal/auth/README.md b/internal/auth/README.md index 9fbcc1185..a9b6a31b3 100644 --- a/internal/auth/README.md +++ b/internal/auth/README.md @@ -7,4 +7,4 @@ The CLI authentication follows this approach: 1. The refresh token is stored at the OS keychain (supports macOS, Linux, and Windows thanks to https://github.com/zalando/go-keyring). 1. During regular commands initialization, the access token is used to instantiate an Auth0 API client. - If the token is expired according to the value stored on the configuration file, a new one is requested using the refresh token. - - In case of any error, the interactive login flow is triggered. + - In case of any error, the interactive login flow is triggered. \ No newline at end of file diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 5edfec358..add4744e4 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -14,12 +14,8 @@ import ( ) const ( - clientID = "2iZo3Uczt5LFHacKdM0zzgUO2eG2uDjT" - deviceCodeEndpoint = "https://auth0.auth0.com/oauth/device/code" - oauthTokenEndpoint = "https://auth0.auth0.com/oauth/token" audiencePath = "/api/v2/" waitThresholdInSeconds = 3 - // namespace used to set/get values from the keychain SecretsNamespace = "auth0-cli" ) @@ -35,25 +31,18 @@ var requiredScopes = []string{ "read:branding", "update:branding", "read:email_templates", "update:email_templates", "read:connections", "update:connections", - "read:client_keys", "read:logs", "read:tenant_settings", + "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", "delete:log_streams", "read:log_streams", "update:log_streams", "create:actions", "delete:actions", "read:actions", "update:actions", } -// RequiredScopes returns the scopes used for login. -func RequiredScopes() []string { return requiredScopes } - -// RequiredScopesMin returns minimum scopes used for login in integration tests. -func RequiredScopesMin() []string { - min := []string{} - for _, s := range requiredScopes { - if s != "offline_access" && s != "openid" { - min = append(min, s) - } - } - return min +type Authenticator struct { + Audience string + ClientID string + DeviceCodeEndpoint string + OauthTokenEndpoint string } // SecretStore provides access to stored sensitive data. @@ -64,8 +53,6 @@ type SecretStore interface { Delete(namespace, key string) error } -type Authenticator struct{} - type Result struct { Tenant string Domain string @@ -82,6 +69,20 @@ type State struct { Interval int `json:"interval"` } +// RequiredScopes returns the scopes used for login. +func RequiredScopes() []string { return requiredScopes } + +// RequiredScopesMin returns minimum scopes used for login in integration tests. +func RequiredScopesMin() []string { + min := []string{} + for _, s := range requiredScopes { + if s != "offline_access" && s != "openid" { + min = append(min, s) + } + } + return min +} + func (s *State) IntervalDuration() time.Duration { return time.Duration(s.Interval+waitThresholdInSeconds) * time.Second } @@ -106,11 +107,11 @@ func (a *Authenticator) Wait(ctx context.Context, state State) (Result, error) { return Result{}, ctx.Err() case <-t.C: data := url.Values{ - "client_id": {clientID}, + "client_id": {a.ClientID}, "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, "device_code": {state.DeviceCode}, } - r, err := http.PostForm(oauthTokenEndpoint, data) + r, err := http.PostForm(a.OauthTokenEndpoint, data) if err != nil { return Result{}, fmt.Errorf("cannot get device code: %w", err) } @@ -157,11 +158,11 @@ func (a *Authenticator) Wait(ctx context.Context, state State) (Result, error) { func (a *Authenticator) getDeviceCode(ctx context.Context) (State, error) { data := url.Values{ - "client_id": {clientID}, + "client_id": {a.ClientID}, "scope": {strings.Join(requiredScopes, " ")}, - "audience": {"https://*.auth0.com/api/v2/"}, + "audience": {a.Audience}, } - r, err := http.PostForm(deviceCodeEndpoint, data) + r, err := http.PostForm(a.DeviceCodeEndpoint, data) if err != nil { return State{}, fmt.Errorf("cannot get device code: %w", err) } diff --git a/internal/auth/token.go b/internal/auth/token.go index 66424a3e6..313c0ef93 100644 --- a/internal/auth/token.go +++ b/internal/auth/token.go @@ -18,8 +18,9 @@ type TokenResponse struct { } type TokenRetriever struct { - Secrets SecretStore - Client *http.Client + Authenticator *Authenticator + Secrets SecretStore + Client *http.Client } // Delete deletes the given tenant from the secrets storage. @@ -39,9 +40,9 @@ func (t *TokenRetriever) Refresh(ctx context.Context, tenant string) (TokenRespo return TokenResponse{}, errors.New("cannot use the stored refresh token: the token is empty") } // get access token: - r, err := t.Client.PostForm(oauthTokenEndpoint, url.Values{ + r, err := t.Client.PostForm(t.Authenticator.OauthTokenEndpoint, url.Values{ "grant_type": {"refresh_token"}, - "client_id": {clientID}, + "client_id": {t.Authenticator.ClientID}, "refresh_token": {refreshToken}, }) if err != nil { diff --git a/internal/auth/token_test.go b/internal/auth/token_test.go index 3ab991ec4..1dc994e21 100644 --- a/internal/auth/token_test.go +++ b/internal/auth/token_test.go @@ -46,8 +46,9 @@ func TestTokenRetriever_Refresh(t *testing.T) { client := &http.Client{Transport: transport} tr := &TokenRetriever{ - Secrets: secretsMock, - Client: client, + Authenticator: &Authenticator{"https://test.com/api/v2/", "client-id", "https://test.com/oauth/device/code", "https://test.com/token"}, + Secrets: secretsMock, + Client: client, } got, err := tr.Refresh(context.Background(), "mytenant") @@ -72,13 +73,13 @@ func TestTokenRetriever_Refresh(t *testing.T) { t.Fatal(err) } - if want, got := "https://auth0.auth0.com/oauth/token", req.URL.String(); want != got { + if want, got := "https://test.com/token", req.URL.String(); want != got { t.Fatalf("wanted request URL: %v, got: %v", want, got) } if want, got := "refresh_token", req.Form["grant_type"][0]; want != got { t.Fatalf("wanted grant_type: %v, got: %v", want, got) } - if want, got := "2iZo3Uczt5LFHacKdM0zzgUO2eG2uDjT", req.Form["client_id"][0]; want != got { + if want, got := "client-id", req.Form["client_id"][0]; want != got { t.Fatalf("wanted grant_type: %v, got: %v", want, got) } if want, got := "refresh-token-here", req.Form["refresh_token"][0]; want != got { diff --git a/internal/cli/cli.go b/internal/cli/cli.go index 4600efe0d..3eeb2e0de 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -73,9 +73,10 @@ var errUnauthenticated = errors.New("Not logged in. Try 'auth0 login'.") // type cli struct { // core primitives exposed to command builders. - api *auth0.API - renderer *display.Renderer - tracker *analytics.Tracker + api *auth0.API + authenticator *auth.Authenticator + renderer *display.Renderer + tracker *analytics.Tracker // set of flags which are user specified. debug bool tenant string @@ -161,8 +162,9 @@ func (c *cli) prepareTenant(ctx context.Context) (tenant, error) { // check if the stored access token is expired: // use the refresh token to get a new access token: tr := &auth.TokenRetriever{ - Secrets: &auth.Keyring{}, - Client: http.DefaultClient, + Authenticator: c.authenticator, + Secrets: &auth.Keyring{}, + Client: http.DefaultClient, } res, err := tr.Refresh(ctx, t.Domain) diff --git a/internal/cli/login.go b/internal/cli/login.go index 4cb457e27..397b8ab16 100644 --- a/internal/cli/login.go +++ b/internal/cli/login.go @@ -43,8 +43,7 @@ func RunLogin(ctx context.Context, cli *cli, expired bool) (tenant, error) { fmt.Print("If you don't have an account, please go to https://auth0.com/signup\n\n") } - a := &auth.Authenticator{} - state, err := a.Start(ctx) + state, err := cli.authenticator.Start(ctx) if err != nil { return tenant{}, fmt.Errorf("Could not start the authentication process: %w.", err) } @@ -60,7 +59,7 @@ func RunLogin(ctx context.Context, cli *cli, expired bool) (tenant, error) { var res auth.Result err = ansi.Spinner("Waiting for login to complete in browser", func() error { - res, err = a.Wait(ctx, state) + res, err = cli.authenticator.Wait(ctx, state) return err }) diff --git a/internal/cli/root.go b/internal/cli/root.go index d5675cf15..b13bf386e 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -9,14 +9,24 @@ import ( "github.com/auth0/auth0-cli/internal/analytics" "github.com/auth0/auth0-cli/internal/ansi" + "github.com/auth0/auth0-cli/internal/auth" "github.com/auth0/auth0-cli/internal/buildinfo" "github.com/auth0/auth0-cli/internal/display" "github.com/auth0/auth0-cli/internal/instrumentation" + "github.com/joeshaw/envdecode" "github.com/spf13/cobra" ) const rootShort = "Supercharge your development workflow." +// authCfg defines the configurable auth context the cli will run in. +var authCfg struct { + Audience string `env:"AUTH0_AUDIENCE,default=https://*.auth0.com/api/v2/"` + ClientID string `env:"AUTH0_CLIENT_ID,default=2iZo3Uczt5LFHacKdM0zzgUO2eG2uDjT"` + DeviceCodeEndpoint string `env:"AUTH0_DEVICE_CODE_ENDPOINT,default=https://auth0.auth0.com/oauth/device/code"` + OauthTokenEndpoint string `env:"AUTH0_OAUTH_TOKEN_ENDPOINT,default=https://auth0.auth0.com/oauth/token"` +} + // Execute is the primary entrypoint of the CLI app. func Execute() { // cfg contains tenant related information, e.g. `travel0-dev`, @@ -83,6 +93,16 @@ func buildRootCmd(cli *cli) *cobra.Command { Long: rootShort + "\n" + getLogin(cli), Version: buildinfo.GetVersionWithCommit(), PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + if err := envdecode.StrictDecode(&authCfg); err != nil { + return fmt.Errorf("could not decode env: %w", err) + } + + cli.authenticator = &auth.Authenticator{ + Audience: authCfg.Audience, + ClientID: authCfg.ClientID, + DeviceCodeEndpoint: authCfg.DeviceCodeEndpoint, + OauthTokenEndpoint: authCfg.OauthTokenEndpoint, + } ansi.DisableColors = cli.noColor prepareInteractivity(cmd)