Skip to content

Commit fa29458

Browse files
committed
Squashed commit of the following:
commit 6c5e40d Author: M Hickford <[email protected]> Date: Mon Nov 14 05:43:06 2022 +0000 more error handling fix commit bc6a2d2 Author: M Hickford <[email protected]> Date: Fri Nov 11 19:24:52 2022 +0000 fix error handling commit 547b7ab Author: Beyang Liu <[email protected]> Date: Sun Oct 11 12:23:44 2020 -0700 oauth2: return error, error_description, and error_uri when error field is present in token response commit 13a8e35 Author: M Hickford <[email protected]> Date: Fri Nov 11 15:36:51 2022 +0000 Fix for GitHub commit 4d789d2 Author: Marcos Lilljedahl <[email protected]> Date: Mon Jan 7 21:56:42 2019 +0800 oauth2: add device flow support Signed-off-by: Marcos Lilljedahl <[email protected]>
1 parent 800ea71 commit fa29458

File tree

4 files changed

+161
-4
lines changed

4 files changed

+161
-4
lines changed

deviceauth.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package oauth2
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"io"
8+
"io/ioutil"
9+
"net/http"
10+
"net/url"
11+
"strings"
12+
13+
"golang.org/x/net/context/ctxhttp"
14+
"golang.org/x/oauth2/internal"
15+
)
16+
17+
const (
18+
errAuthorizationPending = "authorization_pending"
19+
errSlowDown = "slow_down"
20+
errAccessDenied = "access_denied"
21+
errExpiredToken = "expired_token"
22+
)
23+
24+
type DeviceAuth struct {
25+
DeviceCode string `json:"device_code"`
26+
UserCode string `json:"user_code"`
27+
VerificationURI string `json:"verification_uri,verification_url"`
28+
VerificationURIComplete string `json:"verification_uri_complete,omitempty"`
29+
ExpiresIn int `json:"expires_in"`
30+
Interval int `json:"interval,omitempty"`
31+
raw map[string]interface{}
32+
}
33+
34+
func retrieveDeviceAuth(ctx context.Context, c *Config, v url.Values) (*DeviceAuth, error) {
35+
req, err := http.NewRequest("POST", c.Endpoint.DeviceAuthURL, strings.NewReader(v.Encode()))
36+
if err != nil {
37+
return nil, err
38+
}
39+
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
40+
req.Header.Set("Accept", "application/json")
41+
42+
r, err := ctxhttp.Do(ctx, nil, req)
43+
if err != nil {
44+
return nil, err
45+
}
46+
47+
body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1<<20))
48+
if err != nil {
49+
return nil, fmt.Errorf("oauth2: cannot auth device: %v", err)
50+
}
51+
if code := r.StatusCode; code < 200 || code > 299 {
52+
return nil, &RetrieveError{
53+
Response: r,
54+
Body: body,
55+
}
56+
}
57+
58+
da := &DeviceAuth{}
59+
err = json.Unmarshal(body, &da)
60+
if err != nil {
61+
return nil, fmt.Errorf("unmarshal %s", err)
62+
}
63+
64+
_ = json.Unmarshal(body, &da.raw)
65+
66+
// Azure AD supplies verification_url instead of verification_uri
67+
if da.VerificationURI == "" {
68+
da.VerificationURI, _ = da.raw["verification_url"].(string)
69+
}
70+
71+
return da, nil
72+
}
73+
74+
func parseError(err error) string {
75+
e, ok := err.(*RetrieveError)
76+
if ok {
77+
eResp := make(map[string]string)
78+
_ = json.Unmarshal(e.Body, &eResp)
79+
return eResp["error"]
80+
}
81+
e2, ok := err.(*internal.TokenError)
82+
if ok {
83+
return e2.Err
84+
}
85+
return ""
86+
}

endpoints/endpoints.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,9 @@ var Fitbit = oauth2.Endpoint{
5555

5656
// GitHub is the endpoint for Github.
5757
var GitHub = oauth2.Endpoint{
58-
AuthURL: "https://github.com/login/oauth/authorize",
59-
TokenURL: "https://github.com/login/oauth/access_token",
58+
AuthURL: "https://github.com/login/oauth/authorize",
59+
TokenURL: "https://github.com/login/oauth/access_token",
60+
DeviceAuthURL: "https://github.com/login/device/code",
6061
}
6162

6263
// GitLab is the endpoint for GitLab.

internal/token.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,3 +323,13 @@ type RetrieveError struct {
323323
func (r *RetrieveError) Error() string {
324324
return fmt.Sprintf("oauth2: cannot fetch token: %v\nResponse: %s", r.Response.Status, r.Body)
325325
}
326+
327+
type TokenError struct {
328+
Err string
329+
ErrorDescription string
330+
ErrorURI string
331+
}
332+
333+
func (t *TokenError) Error() string {
334+
return fmt.Sprintf("oauth2: error in token fetch response: %s\nerror_description: %s\nerror_uri: %s", t.Err, t.ErrorDescription, t.ErrorURI)
335+
}

oauth2.go

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"net/url"
1717
"strings"
1818
"sync"
19+
"time"
1920

2021
"golang.org/x/oauth2/internal"
2122
)
@@ -70,8 +71,9 @@ type TokenSource interface {
7071
// Endpoint represents an OAuth 2.0 provider's authorization and token
7172
// endpoint URLs.
7273
type Endpoint struct {
73-
AuthURL string
74-
TokenURL string
74+
AuthURL string
75+
DeviceAuthURL string
76+
TokenURL string
7577

7678
// AuthStyle optionally specifies how the endpoint wants the
7779
// client ID & client secret sent. The zero value means to
@@ -224,6 +226,64 @@ func (c *Config) Exchange(ctx context.Context, code string, opts ...AuthCodeOpti
224226
return retrieveToken(ctx, c, v)
225227
}
226228

229+
// AuthDevice returns a device auth struct which contains a device code
230+
// and authorization information provided for users to enter on another device.
231+
func (c *Config) AuthDevice(ctx context.Context, opts ...AuthCodeOption) (*DeviceAuth, error) {
232+
v := url.Values{
233+
"client_id": {c.ClientID},
234+
}
235+
if len(c.Scopes) > 0 {
236+
v.Set("scope", strings.Join(c.Scopes, " "))
237+
}
238+
for _, opt := range opts {
239+
opt.setValue(v)
240+
}
241+
return retrieveDeviceAuth(ctx, c, v)
242+
}
243+
244+
// Poll does a polling to exchange an device code for a token.
245+
func (c *Config) Poll(ctx context.Context, da *DeviceAuth, opts ...AuthCodeOption) (*Token, error) {
246+
v := url.Values{
247+
"client_id": {c.ClientID},
248+
"grant_type": {"urn:ietf:params:oauth:grant-type:device_code"},
249+
"device_code": {da.DeviceCode},
250+
}
251+
if len(c.Scopes) > 0 {
252+
v.Set("scope", strings.Join(c.Scopes, " "))
253+
}
254+
for _, opt := range opts {
255+
opt.setValue(v)
256+
}
257+
258+
// If no interval was provided, the client MUST use a reasonable default polling interval.
259+
// See https://tools.ietf.org/html/draft-ietf-oauth-device-flow-07#section-3.5
260+
interval := da.Interval
261+
if interval == 0 {
262+
interval = 5
263+
}
264+
265+
for {
266+
time.Sleep(time.Duration(interval) * time.Second)
267+
268+
tok, err := retrieveToken(ctx, c, v)
269+
if err == nil {
270+
return tok, nil
271+
}
272+
273+
errTyp := parseError(err)
274+
switch errTyp {
275+
case errSlowDown:
276+
interval += 5
277+
case errAuthorizationPending:
278+
// Do nothing.
279+
case errAccessDenied, errExpiredToken:
280+
fallthrough
281+
default:
282+
return tok, err
283+
}
284+
}
285+
}
286+
227287
// Client returns an HTTP client using the provided token.
228288
// The token will auto-refresh as necessary. The underlying
229289
// HTTP transport will be obtained using the provided context.

0 commit comments

Comments
 (0)