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

refactor: source auth vars from env #318

Merged
merged 11 commits into from
Jul 5, 2021
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
2 changes: 1 addition & 1 deletion internal/auth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
49 changes: 25 additions & 24 deletions internal/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -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.
Expand All @@ -64,8 +53,6 @@ type SecretStore interface {
Delete(namespace, key string) error
}

type Authenticator struct{}

type Result struct {
Tenant string
Domain string
Expand All @@ -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
}
Expand All @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand Down
9 changes: 5 additions & 4 deletions internal/auth/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 {
Expand Down
9 changes: 5 additions & 4 deletions internal/auth/token_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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 {
Expand Down
12 changes: 7 additions & 5 deletions internal/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
5 changes: 2 additions & 3 deletions internal/cli/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -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
})

Expand Down
20 changes: 20 additions & 0 deletions internal/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
Expand Down Expand Up @@ -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)

Expand Down