-
Notifications
You must be signed in to change notification settings - Fork 55
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat: device auth * fix:linter * feat: parse tenant from the token * fix: lint * fix: scope const * refactor: split auth flow (start + wait) * "fix: error handling"
- Loading branch information
Showing
4 changed files
with
244 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,165 @@ | ||
package auth | ||
|
||
import ( | ||
"context" | ||
"encoding/base64" | ||
"encoding/json" | ||
"errors" | ||
"fmt" | ||
"net/http" | ||
"net/url" | ||
"strings" | ||
"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" | ||
oauthTokenEndpoint = "https://auth0.auth0.com/oauth/token" | ||
// TODO(jfatta) extend the scope as we extend the CLI: | ||
scope = "openid read:roles" | ||
audiencePath = "/api/v2/" | ||
) | ||
|
||
type Authenticator struct { | ||
} | ||
|
||
type Result struct { | ||
Tenant string | ||
AccessToken string | ||
ExpiresIn int64 | ||
} | ||
|
||
type State struct { | ||
DeviceCode string `json:"device_code"` | ||
UserCode string `json:"user_code"` | ||
VerificationURI string `json:"verification_uri_complete"` | ||
ExpiresIn int `json:"expires_in"` | ||
Interval int `json:"interval"` | ||
} | ||
|
||
func (s *State) IntervalDuration() time.Duration { | ||
return time.Duration(s.Interval) * time.Second | ||
} | ||
|
||
func (a *Authenticator) Start(ctx context.Context) (State, error) { | ||
s, err := a.getDeviceCode(ctx) | ||
if err != nil { | ||
return State{}, fmt.Errorf("cannot get device code: %w", err) | ||
} | ||
return s, nil | ||
} | ||
|
||
func (a *Authenticator) Wait(ctx context.Context, state State) (Result, error) { | ||
t := time.NewTicker(state.IntervalDuration()) | ||
for { | ||
select { | ||
case <-ctx.Done(): | ||
return Result{}, ctx.Err() | ||
case <-t.C: | ||
data := url.Values{ | ||
"client_id": {clientID}, | ||
"grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, | ||
"device_code": {state.DeviceCode}, | ||
} | ||
r, err := http.PostForm(oauthTokenEndpoint, data) | ||
if err != nil { | ||
return Result{}, fmt.Errorf("cannot get device code: %w", err) | ||
} | ||
defer r.Body.Close() | ||
|
||
var res struct { | ||
AccessToken string `json:"access_token"` | ||
IDToken string `json:"id_token"` | ||
Scope string `json:"scope"` | ||
ExpiresIn int64 `json:"expires_in"` | ||
TokenType string `json:"token_type"` | ||
Error *string `json:"error,omitempty"` | ||
ErrorDescription string `json:"error_description,omitempty"` | ||
} | ||
|
||
err = json.NewDecoder(r.Body).Decode(&res) | ||
if err != nil { | ||
return Result{}, fmt.Errorf("cannot decode response: %w", err) | ||
} | ||
|
||
if res.Error != nil { | ||
if *res.Error == "authorization_pending" { | ||
continue | ||
} | ||
return Result{}, errors.New(res.ErrorDescription) | ||
} | ||
|
||
t, err := parseTenant(res.AccessToken) | ||
if err != nil { | ||
return Result{}, fmt.Errorf("cannot parse tenant from the given access token: %w", err) | ||
} | ||
return Result{ | ||
AccessToken: res.AccessToken, | ||
ExpiresIn: res.ExpiresIn, | ||
Tenant: t, | ||
}, nil | ||
} | ||
} | ||
} | ||
|
||
func (a *Authenticator) getDeviceCode(ctx context.Context) (State, error) { | ||
data := url.Values{ | ||
"client_id": {clientID}, | ||
"scope": {scope}, | ||
"audience": {"https://*.auth0.com/api/v2/"}, | ||
} | ||
r, err := http.PostForm(deviceCodeEndpoint, data) | ||
if err != nil { | ||
return State{}, fmt.Errorf("cannot get device code: %w", err) | ||
} | ||
defer r.Body.Close() | ||
var res State | ||
err = json.NewDecoder(r.Body).Decode(&res) | ||
if err != nil { | ||
return State{}, fmt.Errorf("cannot decode response: %w", err) | ||
} | ||
|
||
return res, nil | ||
} | ||
|
||
func parseTenant(accessToken string) (string, error) { | ||
parts := strings.Split(accessToken, ".") | ||
v, err := base64.RawURLEncoding.DecodeString(parts[1]) | ||
if err != nil { | ||
return "", err | ||
} | ||
var payload struct { | ||
AUDs []string `json:"aud"` | ||
} | ||
if err := json.Unmarshal([]byte(v), &payload); err != nil { | ||
return "", err | ||
} | ||
for _, aud := range payload.AUDs { | ||
u, err := url.Parse(aud) | ||
if err != nil { | ||
return "", err | ||
} | ||
if u.Path == audiencePath { | ||
parts := strings.Split(u.Host, ".") | ||
return parts[0], nil | ||
} | ||
} | ||
return "", fmt.Errorf("audience not found for %s", audiencePath) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
package cli | ||
|
||
import ( | ||
"fmt" | ||
|
||
"github.com/auth0/auth0-cli/internal/ansi" | ||
"github.com/auth0/auth0-cli/internal/auth" | ||
"github.com/auth0/auth0-cli/internal/open" | ||
"github.com/spf13/cobra" | ||
) | ||
|
||
func loginCmd(cli *cli) *cobra.Command { | ||
cmd := &cobra.Command{ | ||
Use: "login", | ||
Short: "authenticate the Auth0 CLI.", | ||
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 Auht0 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) | ||
} | ||
|
||
// TODO(jfatta): update the configuration with the token, tenant, audience, etc | ||
cli.renderer.Infof("Successfully logged in.") | ||
cli.renderer.Infof("Tenant: %s", res.Tenant) | ||
return nil | ||
}, | ||
} | ||
|
||
return cmd | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
package open | ||
|
||
import ( | ||
"os/exec" | ||
"runtime" | ||
) | ||
|
||
func URL(url string) error { | ||
var cmd string | ||
var args []string | ||
|
||
switch runtime.GOOS { | ||
case "windows": | ||
cmd = "cmd" | ||
args = []string{"/c", "start"} | ||
case "darwin": | ||
cmd = "open" | ||
default: // "linux", "freebsd", "openbsd", "netbsd" | ||
cmd = "xdg-open" | ||
} | ||
args = append(args, url) | ||
return exec.Command(cmd, args...).Start() | ||
} |