forked from int128/oauth2dev
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathauthorization.go
More file actions
127 lines (111 loc) · 4.28 KB
/
authorization.go
File metadata and controls
127 lines (111 loc) · 4.28 KB
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
package oauth2dev
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
"golang.org/x/oauth2"
)
// AuthorizationResponse represents Device Authorization Response,
// described in https://www.rfc-editor.org/rfc/rfc8628#section-3.2
type AuthorizationResponse struct {
// The device verification code.
DeviceCode string `json:"device_code,omitempty"`
// The end-user verification code.
UserCode string `json:"user_code,omitempty"`
// The end-user verification URI on the authorization server.
VerificationURI string `json:"verification_uri,omitempty"`
// A verification URI that includes the "user_code" (or
// other information with the same function as the "user_code"),
// which is designed for non-textual transmission.
VerificationURIComplete string `json:"verification_uri_complete,omitempty"`
// The end-user verification URI on the authorization server.
// Some implementations return this field instead of verification_uri,
// such as https://developers.google.com/identity/protocols/oauth2/limited-input-device.
VerificationURL string `json:"verification_url,omitempty"`
// The lifetime in seconds of the "device_code" and "user_code".
ExpiresIn int `json:"expires_in,omitempty"`
// The minimum amount of time in seconds that the client
// SHOULD wait between polling requests to the token endpoint.
// If no value is provided, clients MUST use 5 as the default.
Interval int `json:"interval,omitempty"`
}
func (ar AuthorizationResponse) IntervalDuration() time.Duration {
return time.Duration(ar.Interval) * time.Second
}
// URL returns either of VerificationURIComplete, VerificationURI or VerificationURL.
func (ar AuthorizationResponse) URL() string {
if ar.VerificationURIComplete != "" {
return ar.VerificationURIComplete
}
if ar.VerificationURI != "" {
return ar.VerificationURI
}
if ar.VerificationURL != "" {
return ar.VerificationURL
}
return ""
}
// AuthorizationErrorResponse represents the error response,
// described in https://www.rfc-editor.org/rfc/rfc6749#section-5.2
type AuthorizationErrorResponse struct {
StatusCode int `json:"-"`
ErrorCode string `json:"error,omitempty"`
ErrorDescription string `json:"error_description,omitempty"`
ErrorURI string `json:"error_uri,omitempty"`
}
func (err AuthorizationErrorResponse) Error() string {
return fmt.Sprintf("authorization error response %d, %s (%s)", err.StatusCode, err.ErrorCode, err.ErrorDescription)
}
// RetrieveCode sends an authorization request to the authorization endpoint.
// If it received a successful response, it returns the AuthorizationResponse.
// If it received an error response JSON, it returns an AuthorizationErrorResponse.
// Otherwise, it returns an error wrapped with the cause.
func RetrieveCode(ctx context.Context, cfg oauth2.Config) (*AuthorizationResponse, error) {
// Device Authorization Request,
// described in https://www.rfc-editor.org/rfc/rfc8628#section-3.1
params := url.Values{"client_id": {cfg.ClientID}}
if len(cfg.Scopes) > 0 {
params.Set("scope", strings.Join(cfg.Scopes, " "))
}
if cfg.ClientSecret != "" {
params.Set("client_secret", cfg.ClientSecret)
}
req, err := http.NewRequestWithContext(ctx, "POST", cfg.Endpoint.AuthURL, strings.NewReader(params.Encode()))
if err != nil {
return nil, fmt.Errorf("unable to create a request: %w", err)
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
hc := contextHTTPClient(ctx)
resp, err := hc.Do(req)
if err != nil {
return nil, fmt.Errorf("unable to send the request: %w", err)
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != 200 {
b, _ := io.ReadAll(resp.Body)
var eresp AuthorizationErrorResponse
eresp.StatusCode = resp.StatusCode
d := json.NewDecoder(bytes.NewReader(b))
if err := d.Decode(&eresp); err != nil {
return nil, fmt.Errorf("authorization error response (status: %d, payload: %s)", resp.StatusCode, string(b))
}
return nil, eresp
}
d := json.NewDecoder(resp.Body)
var aresp AuthorizationResponse
if err := d.Decode(&aresp); err != nil {
return nil, fmt.Errorf("invalid response: %w", err)
}
if aresp.Interval == 0 {
// If no value is provided, clients MUST use 5 as the default.
// https://www.rfc-editor.org/rfc/rfc8628#section-3.2
aresp.Interval = 5
}
return &aresp, nil
}