This repository was archived by the owner on Nov 30, 2019. It is now read-only.
forked from golang/oauth2
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathoauth2.go
More file actions
301 lines (277 loc) · 9.73 KB
/
oauth2.go
File metadata and controls
301 lines (277 loc) · 9.73 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
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
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
// Copyright 2014 The oauth2 Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package oauth2 provides support for making
// OAuth2 authorized and authenticated HTTP requests.
// It can additionally grant authorization with Bearer JWT.
package oauth2
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"mime"
"net/http"
"net/url"
"strconv"
"strings"
"time"
)
type tokenRespBody struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
RefreshToken string `json:"refresh_token"`
ExpiresIn int64 `json:"expires_in"` // in seconds
IdToken string `json:"id_token"`
}
// TokenFetcher refreshes or fetches a new access token from the
// provider. It should return an error if it's not capable of
// retrieving a token.
type TokenFetcher interface {
// FetchToken retrieves a new access token for the provider.
// If the implementation doesn't know how to retrieve a new token,
// it returns an error. The existing token may be nil.
FetchToken(existing *Token) (*Token, error)
}
// Options represents options to provide OAuth 2.0 client credentials
// and access level. A sample configuration:
type Options struct {
// ClientID is the OAuth client identifier used when communicating with
// the configured OAuth provider.
ClientID string `json:"client_id"`
// ClientSecret is the OAuth client secret used when communicating with
// the configured OAuth provider.
ClientSecret string `json:"client_secret"`
// RedirectURL is the URL to which the user will be returned after
// granting (or denying) access.
RedirectURL string `json:"redirect_url"`
// Scopes optionally specifies a list of requested permission scopes.
Scopes []string `json:"scopes,omitempty"`
}
// NewConfig creates a generic OAuth 2.0 configuration that talks
// to an OAuth 2.0 provider specified with authURL and tokenURL.
func NewConfig(opts *Options, authURL, tokenURL string) (*Config, error) {
aURL, err := url.Parse(authURL)
if err != nil {
return nil, err
}
tURL, err := url.Parse(tokenURL)
if err != nil {
return nil, err
}
if opts.ClientID == "" {
return nil, errors.New("oauth2: missing client ID")
}
return &Config{
opts: opts,
authURL: aURL,
tokenURL: tURL,
}, nil
}
// Config represents the configuration of an OAuth 2.0 consumer client.
type Config struct {
// Client is the HTTP client to be used to retrieve
// tokens from the OAuth 2.0 provider.
Client *http.Client
// Transport is the http.RoundTripper to be used
// to construct new oauth2.Transport instances from
// this configuration.
Transport http.RoundTripper
opts *Options
// AuthURL is the URL the user will be directed to
// in order to grant access.
authURL *url.URL
// TokenURL is the URL used to retrieve OAuth tokens.
tokenURL *url.URL
}
// AuthCodeURL returns a URL to OAuth 2.0 provider's consent page
// that asks for permissions for the required scopes explicitly.
//
// State is a token to protect the user from CSRF attacks. You must
// always provide a non-zero string and validate that it matches the
// the state query parameter on your redirect callback.
// See http://tools.ietf.org/html/rfc6749#section-10.12 for more info.
//
// Access type is an OAuth extension that gets sent as the
// "access_type" field in the URL from AuthCodeURL.
// It may be "online" (default) or "offline".
// If your application needs to refresh access tokens when the
// user is not present at the browser, then use offline. This
// will result in your application obtaining a refresh token
// the first time your application exchanges an authorization
// code for a user.
//
// Approval prompt indicates whether the user should be
// re-prompted for consent. If set to "auto" (default) the
// user will be prompted only if they haven't previously
// granted consent and the code can only be exchanged for an
// access token. If set to "force" the user will always be prompted,
// and the code can be exchanged for a refresh token.
func (c *Config) AuthCodeURL(state, accessType, prompt string) (authURL string) {
u := *c.authURL
v := url.Values{
"response_type": {"code"},
"client_id": {c.opts.ClientID},
"redirect_uri": condVal(c.opts.RedirectURL),
"scope": condVal(strings.Join(c.opts.Scopes, " ")),
"state": condVal(state),
"access_type": condVal(accessType),
"approval_prompt": condVal(prompt),
}
q := v.Encode()
if u.RawQuery == "" {
u.RawQuery = q
} else {
u.RawQuery += "&" + q
}
return u.String()
}
// NewTransport creates a new authorizable transport. It doesn't
// initialize the new transport with a token, so after creation,
// you need to set a valid token (or an expired token with a valid
// refresh token) in order to be able to do authorized requests.
func (c *Config) NewTransport() *Transport {
return NewTransport(c.transport(), c, nil)
}
// NewTransportWithCode exchanges the OAuth 2.0 authorization code with
// the provider to fetch a new access token (and refresh token). Once
// it successfully retrieves a new token, creates a new transport
// authorized with it.
func (c *Config) NewTransportWithCode(code string) (*Transport, error) {
token, err := c.Exchange(code)
if err != nil {
return nil, err
}
return NewTransport(c.transport(), c, token), nil
}
// FetchToken retrieves a new access token and updates the existing token
// with the newly fetched credentials. If existing token doesn't
// contain a refresh token, it returns an error.
func (c *Config) FetchToken(existing *Token) (*Token, error) {
if existing == nil || existing.RefreshToken == "" {
return nil, errors.New("oauth2: cannot fetch access token without refresh token")
}
return c.retrieveToken(url.Values{
"grant_type": {"refresh_token"},
"refresh_token": {existing.RefreshToken},
})
}
// Exchange exchanges the authorization code with the OAuth 2.0 provider
// to retrieve a new access token.
func (c *Config) Exchange(code string) (*Token, error) {
return c.retrieveToken(url.Values{
"grant_type": {"authorization_code"},
"code": {code},
"redirect_uri": condVal(c.opts.RedirectURL),
"scope": condVal(strings.Join(c.opts.Scopes, " ")),
})
}
func (c *Config) retrieveToken(v url.Values) (*Token, error) {
v.Set("client_id", c.opts.ClientID)
bustedAuth := !providerAuthHeaderWorks(c.tokenURL.String())
if bustedAuth && c.opts.ClientSecret != "" {
v.Set("client_secret", c.opts.ClientSecret)
}
req, err := http.NewRequest("POST", c.tokenURL.String(), strings.NewReader(v.Encode()))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
if !bustedAuth && c.opts.ClientSecret != "" {
req.SetBasicAuth(c.opts.ClientID, c.opts.ClientSecret)
}
r, err := c.client().Do(req)
if err != nil {
return nil, err
}
defer r.Body.Close()
body, err := ioutil.ReadAll(r.Body)
if err != nil {
return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err)
}
if c := r.StatusCode; c < 200 || c > 299 {
return nil, fmt.Errorf("oauth2: cannot fetch token: %v\nResponse: %s", r.Status, body)
}
resp := &tokenRespBody{}
content, _, _ := mime.ParseMediaType(r.Header.Get("Content-Type"))
switch content {
case "application/x-www-form-urlencoded", "text/plain":
vals, err := url.ParseQuery(string(body))
if err != nil {
return nil, err
}
resp.AccessToken = vals.Get("access_token")
resp.TokenType = vals.Get("token_type")
resp.RefreshToken = vals.Get("refresh_token")
resp.ExpiresIn, _ = strconv.ParseInt(vals.Get("expires_in"), 10, 64)
resp.IdToken = vals.Get("id_token")
default:
if err = json.Unmarshal(body, &resp); err != nil {
return nil, err
}
}
token := &Token{
AccessToken: resp.AccessToken,
TokenType: resp.TokenType,
RefreshToken: resp.RefreshToken,
}
// Don't overwrite `RefreshToken` with an empty value
// if this was a token refreshing request.
if resp.RefreshToken == "" {
token.RefreshToken = v.Get("refresh_token")
}
if resp.ExpiresIn == 0 {
token.Expiry = time.Time{}
} else {
token.Expiry = time.Now().Add(time.Duration(resp.ExpiresIn) * time.Second)
}
if resp.IdToken != "" {
if token.Extra == nil {
token.Extra = make(map[string]string)
}
token.Extra["id_token"] = resp.IdToken
}
return token, nil
}
func (c *Config) transport() http.RoundTripper {
if c.Transport != nil {
return c.Transport
}
return http.DefaultTransport
}
func (c *Config) client() *http.Client {
if c.Client != nil {
return c.Client
}
return http.DefaultClient
}
func condVal(v string) []string {
if v == "" {
return nil
}
return []string{v}
}
// providerAuthHeaderWorks reports whether the OAuth2 server identified by the tokenURL
// implements the OAuth2 spec correctly
// See https://code.google.com/p/goauth2/issues/detail?id=31 for background.
// In summary:
// - Reddit only accepts client secret in the Authorization header
// - Dropbox accepts either it in URL param or Auth header, but not both.
// - Google only accepts URL param (not spec compliant?), not Auth header
func providerAuthHeaderWorks(tokenURL string) bool {
if strings.HasPrefix(tokenURL, "https://accounts.google.com/") ||
strings.HasPrefix(tokenURL, "https://github.com/") ||
strings.HasPrefix(tokenURL, "https://api.instagram.com/") ||
strings.HasPrefix(tokenURL, "https://www.douban.com/") ||
strings.HasPrefix(tokenURL, "https://api.dropbox.com/") ||
strings.HasPrefix(tokenURL, "https://api.soundcloud.com/") ||
strings.HasPrefix(tokenURL, "https://www.linkedin.com/") {
// Some sites fail to implement the OAuth2 spec fully.
return false
}
// Assume the provider implements the spec properly
// otherwise. We can add more exceptions as they're
// discovered. We will _not_ be adding configurable hooks
// to this package to let users select server bugs.
return true
}