Skip to content

Commit

Permalink
feat: store and use refresh token (#141)
Browse files Browse the repository at this point in the history
* feat: save refresh token into kc

* WIP

* wip

* feat: refresh token

* cleanup parse payload

* cleanup tenant

* fix login vs refresh flow

* document refresh

* docs: auth readme

* Update README.md
  • Loading branch information
jfatta authored Mar 6, 2021
1 parent 40e6cf5 commit c9d1f92
Show file tree
Hide file tree
Showing 81 changed files with 8,000 additions and 7 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ require (
github.com/stretchr/testify v1.7.0
github.com/tidwall/pretty v1.1.0
github.com/ulikunitz/xz v0.5.10 // indirect
github.com/zalando/go-keyring v0.1.1
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 // indirect
golang.org/x/sys v0.0.0-20210305023407-0d6cb8bd5a4b // indirect
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d
Expand Down
7 changes: 7 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/danieljoos/wincred v1.1.0 h1:3RNcEpBg4IhIChZdFRSdlQt1QjCp1sMAPIrOnm7Yf8g=
github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7hqDjlFjiygg=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Expand Down Expand Up @@ -106,6 +108,8 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/goccy/go-json v0.4.7 h1:xGUjaNfhpqhKAV2LoyNXihFLZ8ABSST8B+W+duHqkPI=
github.com/goccy/go-json v0.4.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/godbus/dbus/v5 v5.0.3 h1:ZqHaoEF7TBzh4jzPmqVhE/5A1z9of6orkAe5uHoAeME=
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
Expand Down Expand Up @@ -318,6 +322,7 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
Expand All @@ -342,6 +347,8 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/zalando/go-keyring v0.1.1 h1:w2V9lcx/Uj4l+dzAf1m9s+DJ1O8ROkEHnynonHjTcYE=
github.com/zalando/go-keyring v0.1.1/go.mod h1:OIC+OZ28XbmwFxU/Rp9V7eKzZjamBJwRzC8UFJH9+L8=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
Expand Down
10 changes: 10 additions & 0 deletions internal/auth/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Auth package

The CLI authentication follows this approach:

1. `$ auth0 login` uses **Auth0 Device Flow** to get an `acccess token` and a `refresh token` for the selected tenant.
1. The access token is stored at the configuration file.
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.
21 changes: 21 additions & 0 deletions internal/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,29 @@ const (
deviceCodeEndpoint = "https://auth0.auth0.com/oauth/device/code"
oauthTokenEndpoint = "https://auth0.auth0.com/oauth/token"
audiencePath = "/api/v2/"

secretsNamespace = "auth0-cli"
)

var requiredScopes = []string{
"openid",
"offline_access", // <-- to get a refresh token.
"create:actions", "delete:actions", "read:actions", "update:actions",
"create:clients", "delete:clients", "read:clients", "update:clients",
"create:resource_servers", "delete:resource_servers", "read:resource_servers", "update:resource_servers",
"read:client_keys", "read:logs",
}

// SecretStore provides secure storage for sensitive data
type SecretStore interface {
// Set sets the secret
Set(namespace, key, value string) error
// Get gets the secret
Get(namespace, key string) (string, error)
}

type Authenticator struct {
Secrets SecretStore
}

type Result struct {
Expand Down Expand Up @@ -82,6 +94,7 @@ func (a *Authenticator) Wait(ctx context.Context, state State) (Result, error) {
var res struct {
AccessToken string `json:"access_token"`
IDToken string `json:"id_token"`
RefreshToken string `json:"refresh_token"`
Scope string `json:"scope"`
ExpiresIn int64 `json:"expires_in"`
TokenType string `json:"token_type"`
Expand All @@ -105,6 +118,13 @@ func (a *Authenticator) Wait(ctx context.Context, state State) (Result, error) {
if err != nil {
return Result{}, fmt.Errorf("cannot parse tenant from the given access token: %w", err)
}

// store the refresh token
err = a.Secrets.Set(secretsNamespace, ten, res.RefreshToken)
if err != nil {
return Result{}, fmt.Errorf("cannot store refresh token: %w", err)
}

return Result{
AccessToken: res.AccessToken,
ExpiresIn: res.ExpiresIn,
Expand Down Expand Up @@ -147,6 +167,7 @@ func parseTenant(accessToken string) (tenant, domain string, err error) {
if err := json.Unmarshal([]byte(v), &payload); err != nil {
return "", "", err
}

for _, aud := range payload.AUDs {
u, err := url.Parse(aud)
if err != nil {
Expand Down
15 changes: 15 additions & 0 deletions internal/auth/secrets.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package auth

import "github.com/zalando/go-keyring"

type Keyring struct{}

// Set sets the given key/value pair with the given namespace.
func (k *Keyring) Set(namespace, key, value string) error {
return keyring.Set(namespace, key, value)
}

// Get gets a value for the given namespace and key.
func (k *Keyring) Get(namespace, key string) (string, error) {
return keyring.Get(namespace, key)
}
60 changes: 60 additions & 0 deletions internal/auth/token.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package auth

import (
"context"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
)

type TokenResponse struct {
AccessToken string `json:"access_token"`
IDToken string `json:"id_token"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
}

type TokenRetriever struct {
Secrets SecretStore
Client *http.Client
}

// Refresh gets a new access token from the provided refresh token,
// The request is used the default client_id and endpoint for device authentication.
func (t *TokenRetriever) Refresh(ctx context.Context, tenant string) (TokenResponse, error) {
// get stored refresh token:
refreshToken, err := t.Secrets.Get(secretsNamespace, tenant)
if err != nil {
return TokenResponse{}, fmt.Errorf("cannot get the stored refresh token: %w", err)
}
if refreshToken == "" {
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{
"grant_type": {"refresh_token"},
"client_id": {clientID},
"refresh_token": {refreshToken},
})
if err != nil {
return TokenResponse{}, fmt.Errorf("cannot get a new access token from the refresh token: %w", err)
}

defer r.Body.Close()
if r.StatusCode != http.StatusOK {
b, _ := ioutil.ReadAll(r.Body)
bodyStr := string(b)
return TokenResponse{}, fmt.Errorf("cannot get a new access token from the refresh token: %s", bodyStr)
}

var res TokenResponse
err = json.NewDecoder(r.Body).Decode(&res)
if err != nil {
return TokenResponse{}, fmt.Errorf("cannot decode response: %w", err)
}

return res, nil
}
30 changes: 26 additions & 4 deletions internal/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"io/ioutil"
"net/http"
"os"
"path"
"path/filepath"
Expand All @@ -14,6 +15,7 @@ import (
"time"

"github.com/auth0/auth0-cli/internal/ansi"
"github.com/auth0/auth0-cli/internal/auth"
"github.com/auth0/auth0-cli/internal/auth0"
"github.com/auth0/auth0-cli/internal/display"
"github.com/lestrrat-go/jwx/jwt"
Expand Down Expand Up @@ -118,15 +120,35 @@ func (c *cli) setup(ctx context.Context) error {

if t.AccessToken == "" {
return errUnauthenticated

}

// 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)
// use the refresh token to get a new access token:
tr := &auth.TokenRetriever{
Secrets: &auth.Keyring{},
Client: http.DefaultClient,
}

res, err := tr.Refresh(ctx, t.Name)
if err != nil {
return err
// ask and guide the user through the login process:
c.renderer.Errorf("failed to renew access token, %s", err)
err = RunLogin(ctx, c, true)
if err != nil {
return err
}
} else {
// persist the updated tenant with renewed access token
t.AccessToken = res.AccessToken
t.ExpiresAt = time.Now().Add(
time.Duration(res.ExpiresIn) * time.Second,
)

err = c.addTenant(t)
if err != nil {
return err
}
}
}

Expand Down
5 changes: 2 additions & 3 deletions internal/cli/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,14 @@ func loginCmd(cli *cli) *cobra.Command {
// 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.")
cli.renderer.Warnf("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{}
a := &auth.Authenticator{Secrets: &auth.Keyring{}}
state, err := a.Start(ctx)
if err != nil {
return fmt.Errorf("could not start the authentication process: %w.", err)
Expand Down Expand Up @@ -70,5 +70,4 @@ func RunLogin(ctx context.Context, cli *cli, expired bool) error {
time.Duration(res.ExpiresIn) * time.Second,
),
})

}

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

23 changes: 23 additions & 0 deletions vendor/github.com/danieljoos/wincred/.gitignore

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/danieljoos/wincred/LICENSE

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

Loading

0 comments on commit c9d1f92

Please sign in to comment.