Skip to content

Commit

Permalink
refactor: source auth vars from env (#318)
Browse files Browse the repository at this point in the history
* pick up auth flow from env vars

* refactor: move around authenticator init

* fix: remove func

* test: fix

* docs: add to readme

* style: revert formatting changes

* refactor: address comments

* Update README.md

Co-authored-by: Jorge L. Fatta <[email protected]>

* test: use dummy authenticator

Co-authored-by: Jorge L. Fatta <[email protected]>
Co-authored-by: Rita Zerrizuela <[email protected]>
  • Loading branch information
3 people authored Jul 5, 2021
1 parent 3040a25 commit 85d22b7
Show file tree
Hide file tree
Showing 8 changed files with 76 additions and 41 deletions.
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

0 comments on commit 85d22b7

Please sign in to comment.