/
device_flow.go
174 lines (150 loc) · 5.52 KB
/
device_flow.go
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
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
// Package device facilitates performing OAuth Device Authorization Flow for client applications
// such as CLIs that can not receive redirects from a web site.
//
// First, RequestCode should be used to obtain a CodeResponse.
//
// Next, the user will need to navigate to VerificationURI in their web browser on any device and fill
// in the UserCode.
//
// While the user is completing the web flow, the application should invoke PollToken, which blocks
// the goroutine until the user has authorized the app on the server.
//
// https://docs.github.com/en/free-pro-team@latest/developers/apps/authorizing-oauth-apps#device-flow
package device
import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/cli/oauth/api"
)
var (
// ErrUnsupported is thrown when the server does not implement Device flow.
ErrUnsupported = errors.New("device flow not supported")
// ErrTimeout is thrown when polling the server for the granted token has timed out.
ErrTimeout = errors.New("authentication timed out")
)
type httpClient interface {
PostForm(string, url.Values) (*http.Response, error)
}
// CodeResponse holds information about the authorization-in-progress.
type CodeResponse struct {
// The user verification code is displayed on the device so the user can enter the code in a browser.
UserCode string
// The verification URL where users need to enter the UserCode.
VerificationURI string
// The optional verification URL that includes the UserCode.
VerificationURIComplete string
// The device verification code is 40 characters and used to verify the device.
DeviceCode string
// The number of seconds before the DeviceCode and UserCode expire.
ExpiresIn int
// The minimum number of seconds that must pass before you can make a new access token request to
// complete the device authorization.
Interval int
}
// RequestCode initiates the authorization flow by requesting a code from uri.
func RequestCode(c httpClient, uri string, clientID string, scopes []string) (*CodeResponse, error) {
resp, err := api.PostForm(c, uri, url.Values{
"client_id": {clientID},
"scope": {strings.Join(scopes, " ")},
})
if err != nil {
return nil, err
}
verificationURI := resp.Get("verification_uri")
if verificationURI == "" {
// Google's "OAuth 2.0 for TV and Limited-Input Device Applications" uses `verification_url`.
verificationURI = resp.Get("verification_url")
}
if resp.StatusCode == 401 || resp.StatusCode == 403 || resp.StatusCode == 404 || resp.StatusCode == 422 ||
(resp.StatusCode == 200 && verificationURI == "") ||
(resp.StatusCode == 400 && resp.Get("error") == "unauthorized_client") {
return nil, ErrUnsupported
}
if resp.StatusCode != 200 {
return nil, resp.Err()
}
intervalSeconds, err := strconv.Atoi(resp.Get("interval"))
if err != nil {
return nil, fmt.Errorf("could not parse interval=%q as integer: %w", resp.Get("interval"), err)
}
expiresIn, err := strconv.Atoi(resp.Get("expires_in"))
if err != nil {
return nil, fmt.Errorf("could not parse expires_in=%q as integer: %w", resp.Get("expires_in"), err)
}
return &CodeResponse{
DeviceCode: resp.Get("device_code"),
UserCode: resp.Get("user_code"),
VerificationURI: verificationURI,
VerificationURIComplete: resp.Get("verification_uri_complete"),
Interval: intervalSeconds,
ExpiresIn: expiresIn,
}, nil
}
const defaultGrantType = "urn:ietf:params:oauth:grant-type:device_code"
// PollToken polls the server at pollURL until an access token is granted or denied.
//
// Deprecated: use Wait.
func PollToken(c httpClient, pollURL string, clientID string, code *CodeResponse) (*api.AccessToken, error) {
return Wait(context.Background(), c, pollURL, WaitOptions{
ClientID: clientID,
DeviceCode: code,
})
}
// WaitOptions specifies parameters to poll the server with until authentication completes.
type WaitOptions struct {
// ClientID is the app client ID value.
ClientID string
// ClientSecret is the app client secret value. Optional: only pass if the server requires it.
ClientSecret string
// DeviceCode is the value obtained from RequestCode.
DeviceCode *CodeResponse
// GrantType overrides the default value specified by OAuth 2.0 Device Code. Optional.
GrantType string
newPoller pollerFactory
}
// Wait polls the server at uri until authorization completes.
func Wait(ctx context.Context, c httpClient, uri string, opts WaitOptions) (*api.AccessToken, error) {
checkInterval := time.Duration(opts.DeviceCode.Interval) * time.Second
expiresIn := time.Duration(opts.DeviceCode.ExpiresIn) * time.Second
grantType := opts.GrantType
if opts.GrantType == "" {
grantType = defaultGrantType
}
makePoller := opts.newPoller
if makePoller == nil {
makePoller = newPoller
}
_, poll := makePoller(ctx, checkInterval, expiresIn)
for {
if err := poll.Wait(); err != nil {
return nil, err
}
values := url.Values{
"client_id": {opts.ClientID},
"device_code": {opts.DeviceCode.DeviceCode},
"grant_type": {grantType},
}
// Google's "OAuth 2.0 for TV and Limited-Input Device Applications" requires `client_secret`.
if opts.ClientSecret != "" {
values.Add("client_secret", opts.ClientSecret)
}
// TODO: pass tctx down to the HTTP layer
resp, err := api.PostForm(c, uri, values)
if err != nil {
return nil, err
}
var apiError *api.Error
token, err := resp.AccessToken()
if err == nil {
return token, nil
} else if !(errors.As(err, &apiError) && apiError.Code == "authorization_pending") {
return nil, err
}
}
}