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

feat: renew expired access token #112

Merged
merged 2 commits into from
Feb 26, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
20 changes: 4 additions & 16 deletions internal/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,6 @@ import (
"time"
)

// 1st request
// curl --request POST \
// --url 'https://auth0.auth0.com/oauth/device/code' \
// --header 'content-type: application/x-www-form-urlencoded' \
// --data 'client_id=2iZo3Uczt5LFHacKdM0zzgUO2eG2uDjT' \
// --data 'scope=openid read:roles' \
// --data audience=https://\*.auth0.com/api/v2/

// polling request
// curl --request POST \
// --url 'https://auth0.auth0.com/oauth/token' \
// --header 'content-type: application/x-www-form-urlencoded' \
// --data grant_type=urn:ietf:params:oauth:grant-type:device_code \
// --data device_code=9GtgUcsGKzXkU-i70RN74baY \
// --data 'client_id=2iZo3Uczt5LFHacKdM0zzgUO2eG2uDjT'

const (
clientID = "2iZo3Uczt5LFHacKdM0zzgUO2eG2uDjT"
deviceCodeEndpoint = "https://auth0.auth0.com/oauth/device/code"
Expand Down Expand Up @@ -65,6 +49,9 @@ func (s *State) IntervalDuration() time.Duration {
return time.Duration(s.Interval) * time.Second
}

// 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) {
s, err := a.getDeviceCode(ctx)
if err != nil {
Expand All @@ -73,6 +60,7 @@ func (a *Authenticator) Start(ctx context.Context) (State, error) {
return s, 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())
for {
Expand Down
26 changes: 22 additions & 4 deletions internal/cli/cli.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cli

import (
"context"
"encoding/json"
"errors"
"fmt"
Expand All @@ -21,7 +22,8 @@ import (
)

const (
userAgent = "Auth0 CLI"
userAgent = "Auth0 CLI"
accessTokenExpThreshold = 5 // minutes
Copy link
Contributor

Choose a reason for hiding this comment

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

What do you think about doing 5 * time.Minute here and in the caller below doing int(accessTokenExpThreshold.Minutes()) ?

Copy link
Contributor Author

@jfatta jfatta Feb 26, 2021

Choose a reason for hiding this comment

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

yes! I always mess with it because time.* feels invalid for constants to me (which is not the case)

)

// config defines the exact set of tenants, access tokens, which only exists
Expand Down Expand Up @@ -87,7 +89,7 @@ func (c *cli) isLoggedIn() bool {
//
// 1. A tenant is found.
// 2. The tenant has an access token.
func (c *cli) setup() error {
func (c *cli) setup(ctx context.Context) error {
if err := c.init(); err != nil {
return err
}
Expand All @@ -100,7 +102,19 @@ func (c *cli) setup() error {
if t.AccessToken == "" {
return errUnauthenticated

} else if t.AccessToken != "" {
}

// check if the stored access token is expired:
if isExpired(t.ExpiresAt, accessTokenExpThreshold) {
// ask and guide the user through the login process:
err := RunLogin(ctx, c, true)
if err != nil {
return err
}
}

// continue with the command setup:
if t.AccessToken != "" {
m, err := management.New(t.Domain,
management.WithStaticToken(t.AccessToken),
management.WithDebug(c.debug),
Expand All @@ -115,6 +129,11 @@ func (c *cli) setup() error {
return err
}

// isExpired is true if now() + a threshold in minutes is after the given date
func isExpired(t time.Time, threshold time.Duration) bool {
return time.Now().Add(time.Minute * threshold).After(t)
}

// getTenant fetches the default tenant configured (or the tenant specified via
// the --tenant flag).
func (c *cli) getTenant() (tenant, error) {
Expand Down Expand Up @@ -201,7 +220,6 @@ func (c *cli) init() error {
c.renderer.Tenant = c.tenant

cobra.EnableCommandSorting = false

})

// Determine what the desired output format is.
Expand Down
87 changes: 50 additions & 37 deletions internal/cli/login.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cli

import (
"context"
"fmt"
"time"

Expand All @@ -17,45 +18,57 @@ func loginCmd(cli *cli) *cobra.Command {
Long: "sign in to your Auth0 account and authorize the CLI to access the API",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()

cli.renderer.Heading("✪ Welcome to the Auth0 CLI 🎊.")
cli.renderer.Infof("To set it up, you will need to sign in to your Auth0 account and authorize the CLI to access the API.")
cli.renderer.Infof("If you don't have an account, please go to https://auth0.com/signup, otherwise continue in the browser.\n\n")

a := &auth.Authenticator{}
state, err := a.Start(ctx)
if err != nil {
return fmt.Errorf("could not start the authentication process: %w.", err)
}

cli.renderer.Infof("Your pairing code is: %s\n", ansi.Bold(state.UserCode))
cli.renderer.Infof("This pairing code verifies your authentication with Auth0.")
cli.renderer.Infof("Press Enter to open the browser (^C to quit)")
fmt.Scanln()

err = open.URL(state.VerificationURI)
if err != nil {
cli.renderer.Warnf("Couldn't open the URL, please do it manually: %s.", state.VerificationURI)
}

res, err := a.Wait(ctx, state)
if err != nil {
return fmt.Errorf("login error: %w", err)
}

cli.renderer.Infof("Successfully logged in.")
cli.renderer.Infof("Tenant: %s", res.Tenant)

return cli.addTenant(tenant{
Name: res.Tenant,
Domain: res.Domain,
AccessToken: res.AccessToken,
ExpiresAt: time.Now().Add(
time.Duration(res.ExpiresIn) * time.Second,
),
})
return RunLogin(ctx, cli, false)
},
}

return cmd
}

// RunLogin runs the login flow guiding the user through the process
// by showing the login instructions, opening the browser.
// Use `expired` to run the login from other commands setup:
// this will only affect the messages.
func RunLogin(ctx context.Context, cli *cli, expired bool) error {
if expired {
cli.renderer.Warnf("Your session expired. Please sign in to re-authorize the CLI.")
} else {
cli.renderer.Heading("✪ Welcome to the Auth0 CLI 🎊.")
cli.renderer.Infof("To set it up, you will need to sign in to your Auth0 account and authorize the CLI to access the API.")
cli.renderer.Infof("If you don't have an account, please go to https://auth0.com/signup, otherwise continue in the browser.\n\n")
}

a := &auth.Authenticator{}
state, err := a.Start(ctx)
if err != nil {
return fmt.Errorf("could not start the authentication process: %w.", err)
}

cli.renderer.Infof("Your pairing code is: %s\n", ansi.Bold(state.UserCode))
cli.renderer.Infof("This pairing code verifies your authentication with Auth0.")
cli.renderer.Infof("Press Enter to open the browser (^C to quit)")
fmt.Scanln()

err = open.URL(state.VerificationURI)
if err != nil {
cli.renderer.Warnf("Couldn't open the URL, please do it manually: %s.", state.VerificationURI)
}

res, err := a.Wait(ctx, state)
if err != nil {
return fmt.Errorf("login error: %w", err)
}

cli.renderer.Infof("Successfully logged in.")
cli.renderer.Infof("Tenant: %s\n", res.Tenant)

return cli.addTenant(tenant{
Name: res.Tenant,
Domain: res.Domain,
AccessToken: res.AccessToken,
ExpiresAt: time.Now().Add(
time.Duration(res.ExpiresIn) * time.Second,
),
})

}
2 changes: 1 addition & 1 deletion internal/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func Execute() {
// Initialize everything once. Later callers can then
// freely assume that config is fully primed and ready
// to go.
return cli.setup()
return cli.setup(cmd.Context())
},
}

Expand Down