1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
|
// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package auth
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"time"
)
const (
waitThresholdInSeconds = 3
// SecretsNamespace namespace used to set/get values from the keychain
SecretsNamespace = "vespa-cli"
)
var requiredScopes = []string{"openid", "offline_access"}
type Authenticator struct {
Audience string
ClientID string
DeviceCodeEndpoint string
OauthTokenEndpoint string
}
// SecretStore provides access to stored sensitive data.
type SecretStore interface {
// Get gets the secret
Get(namespace, key string) (string, error)
// Delete removes the secret
Delete(namespace, key string) error
}
type Result struct {
RefreshToken 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"`
}
// RequiredScopes returns the scopes used for login.
func RequiredScopes() []string { return requiredScopes }
func (s *State) IntervalDuration() time.Duration {
return time.Duration(s.Interval+waitThresholdInSeconds) * 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 {
return State{}, fmt.Errorf("cannot get device code: %w", err)
}
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 {
select {
case <-ctx.Done():
return Result{}, ctx.Err()
case <-t.C:
data := url.Values{
"client_id": {a.ClientID},
"grant_type": {"urn:ietf:params:oauth:grant-type:device_code"},
"device_code": {state.DeviceCode},
}
r, err := http.PostForm(a.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"`
RefreshToken string `json:"refresh_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)
}
return Result{
RefreshToken: res.RefreshToken,
AccessToken: res.AccessToken,
ExpiresIn: res.ExpiresIn,
}, nil
}
}
}
func (a *Authenticator) getDeviceCode(ctx context.Context) (State, error) {
data := url.Values{
"client_id": {a.ClientID},
"scope": {strings.Join(requiredScopes, " ")},
"audience": {a.Audience},
}
r, err := http.PostForm(a.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
}
|