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
Merged
Show file tree
Hide file tree
Changes from 7 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
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 the CLI uses defaults to the default Auth0 cloud offering of `auth0.auth0.com`. This can be customized for personalized cloud offerings by setting the following env variables:
as-herzog marked this conversation as resolved.
Show resolved Hide resolved

```
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
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ require (
github.com/google/go-cmp v0.5.5
github.com/google/uuid v1.2.0
github.com/guiguan/caster v0.0.0-20191104051807-3736c4464f38
github.com/joeshaw/envdecode v0.0.0-20200121155833-099f1fc765bd
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/klauspost/compress v1.11.9 // indirect
github.com/klauspost/pgzip v1.2.5 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,8 @@ github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/
github.com/iris-contrib/jade v1.1.3/go.mod h1:H/geBymxJhShH5kecoiOCSssPX7QWYH7UaeZTSWddIk=
github.com/iris-contrib/pongo2 v0.0.1/go.mod h1:Ssh+00+3GAZqSQb30AvBRNxBx7rf0GqwkjqxNd0u65g=
github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrOcOqfqxa4hXw=
github.com/joeshaw/envdecode v0.0.0-20200121155833-099f1fc765bd h1:nIzoSW6OhhppWLm4yqBwZsKJlAayUu5FGozhrF3ETSM=
github.com/joeshaw/envdecode v0.0.0-20200121155833-099f1fc765bd/go.mod h1:MEQrHur0g8VplbLOv5vXmDzacSaH9Z7XhcgsSh1xciU=
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
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 @@ -34,25 +30,18 @@ var requiredScopes = []string{
"create:users", "delete:users", "read:users", "update:users",
"read:branding", "update:branding",
"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 @@ -63,8 +52,6 @@ type SecretStore interface {
Delete(namespace, key string) error
}

type Authenticator struct{}

type Result struct {
Tenant string
Domain string
Expand All @@ -81,6 +68,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 @@ -105,11 +106,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 @@ -156,11 +157,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
5 changes: 3 additions & 2 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://*.auth0.com/api/v2/", "2iZo3Uczt5LFHacKdM0zzgUO2eG2uDjT", "https://auth0.auth0.com/oauth/device/code", "https://auth0.auth0.com/oauth/token"},
as-herzog marked this conversation as resolved.
Show resolved Hide resolved
Secrets: secretsMock,
Client: client,
}

got, err := tr.Refresh(context.Background(), "mytenant")
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
22 changes: 21 additions & 1 deletion internal/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,22 @@ 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"
)

// 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 All @@ -35,6 +45,16 @@ func Execute() {
Long: "Supercharge your development workflow.\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 Expand Up @@ -148,7 +168,7 @@ func Execute() {
// for most of the architectures there's no requirements:
ansi.InitConsole()

cancelCtx := contextWithCancel()
cancelCtx := contextWithCancel()
if err := rootCmd.ExecuteContext(cancelCtx); err != nil {
cli.renderer.Heading("error")
cli.renderer.Errorf(err.Error())
Expand Down
5 changes: 5 additions & 0 deletions vendor/github.com/joeshaw/envdecode/.travis.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 21 additions & 0 deletions vendor/github.com/joeshaw/envdecode/LICENSE

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

89 changes: 89 additions & 0 deletions vendor/github.com/joeshaw/envdecode/README.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading