• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2014 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5// Package internal contains support packages for oauth2 package.
6package internal
7
8import (
9	"encoding/json"
10	"fmt"
11	"io"
12	"io/ioutil"
13	"mime"
14	"net/http"
15	"net/url"
16	"strconv"
17	"strings"
18	"time"
19
20	"golang.org/x/net/context"
21	"golang.org/x/net/context/ctxhttp"
22)
23
24// Token represents the crendentials used to authorize
25// the requests to access protected resources on the OAuth 2.0
26// provider's backend.
27//
28// This type is a mirror of oauth2.Token and exists to break
29// an otherwise-circular dependency. Other internal packages
30// should convert this Token into an oauth2.Token before use.
31type Token struct {
32	// AccessToken is the token that authorizes and authenticates
33	// the requests.
34	AccessToken string
35
36	// TokenType is the type of token.
37	// The Type method returns either this or "Bearer", the default.
38	TokenType string
39
40	// RefreshToken is a token that's used by the application
41	// (as opposed to the user) to refresh the access token
42	// if it expires.
43	RefreshToken string
44
45	// Expiry is the optional expiration time of the access token.
46	//
47	// If zero, TokenSource implementations will reuse the same
48	// token forever and RefreshToken or equivalent
49	// mechanisms for that TokenSource will not be used.
50	Expiry time.Time
51
52	// Raw optionally contains extra metadata from the server
53	// when updating a token.
54	Raw interface{}
55}
56
57// tokenJSON is the struct representing the HTTP response from OAuth2
58// providers returning a token in JSON form.
59type tokenJSON struct {
60	AccessToken  string         `json:"access_token"`
61	TokenType    string         `json:"token_type"`
62	RefreshToken string         `json:"refresh_token"`
63	ExpiresIn    expirationTime `json:"expires_in"` // at least PayPal returns string, while most return number
64	Expires      expirationTime `json:"expires"`    // broken Facebook spelling of expires_in
65}
66
67func (e *tokenJSON) expiry() (t time.Time) {
68	if v := e.ExpiresIn; v != 0 {
69		return time.Now().Add(time.Duration(v) * time.Second)
70	}
71	if v := e.Expires; v != 0 {
72		return time.Now().Add(time.Duration(v) * time.Second)
73	}
74	return
75}
76
77type expirationTime int32
78
79func (e *expirationTime) UnmarshalJSON(b []byte) error {
80	var n json.Number
81	err := json.Unmarshal(b, &n)
82	if err != nil {
83		return err
84	}
85	i, err := n.Int64()
86	if err != nil {
87		return err
88	}
89	*e = expirationTime(i)
90	return nil
91}
92
93var brokenAuthHeaderProviders = []string{
94	"https://accounts.google.com/",
95	"https://api.codeswholesale.com/oauth/token",
96	"https://api.dropbox.com/",
97	"https://api.dropboxapi.com/",
98	"https://api.instagram.com/",
99	"https://api.netatmo.net/",
100	"https://api.odnoklassniki.ru/",
101	"https://api.pushbullet.com/",
102	"https://api.soundcloud.com/",
103	"https://api.twitch.tv/",
104	"https://app.box.com/",
105	"https://connect.stripe.com/",
106	"https://graph.facebook.com", // see https://github.com/golang/oauth2/issues/214
107	"https://login.microsoftonline.com/",
108	"https://login.salesforce.com/",
109	"https://login.windows.net",
110	"https://oauth.sandbox.trainingpeaks.com/",
111	"https://oauth.trainingpeaks.com/",
112	"https://oauth.vk.com/",
113	"https://openapi.baidu.com/",
114	"https://slack.com/",
115	"https://test-sandbox.auth.corp.google.com",
116	"https://test.salesforce.com/",
117	"https://user.gini.net/",
118	"https://www.douban.com/",
119	"https://www.googleapis.com/",
120	"https://www.linkedin.com/",
121	"https://www.strava.com/oauth/",
122	"https://www.wunderlist.com/oauth/",
123	"https://api.patreon.com/",
124	"https://sandbox.codeswholesale.com/oauth/token",
125	"https://api.sipgate.com/v1/authorization/oauth",
126}
127
128// brokenAuthHeaderDomains lists broken providers that issue dynamic endpoints.
129var brokenAuthHeaderDomains = []string{
130	".force.com",
131	".myshopify.com",
132	".okta.com",
133	".oktapreview.com",
134}
135
136func RegisterBrokenAuthHeaderProvider(tokenURL string) {
137	brokenAuthHeaderProviders = append(brokenAuthHeaderProviders, tokenURL)
138}
139
140// providerAuthHeaderWorks reports whether the OAuth2 server identified by the tokenURL
141// implements the OAuth2 spec correctly
142// See https://code.google.com/p/goauth2/issues/detail?id=31 for background.
143// In summary:
144// - Reddit only accepts client secret in the Authorization header
145// - Dropbox accepts either it in URL param or Auth header, but not both.
146// - Google only accepts URL param (not spec compliant?), not Auth header
147// - Stripe only accepts client secret in Auth header with Bearer method, not Basic
148func providerAuthHeaderWorks(tokenURL string) bool {
149	for _, s := range brokenAuthHeaderProviders {
150		if strings.HasPrefix(tokenURL, s) {
151			// Some sites fail to implement the OAuth2 spec fully.
152			return false
153		}
154	}
155
156	if u, err := url.Parse(tokenURL); err == nil {
157		for _, s := range brokenAuthHeaderDomains {
158			if strings.HasSuffix(u.Host, s) {
159				return false
160			}
161		}
162	}
163
164	// Assume the provider implements the spec properly
165	// otherwise. We can add more exceptions as they're
166	// discovered. We will _not_ be adding configurable hooks
167	// to this package to let users select server bugs.
168	return true
169}
170
171func RetrieveToken(ctx context.Context, clientID, clientSecret, tokenURL string, v url.Values) (*Token, error) {
172	hc, err := ContextClient(ctx)
173	if err != nil {
174		return nil, err
175	}
176	bustedAuth := !providerAuthHeaderWorks(tokenURL)
177	if bustedAuth {
178		if clientID != "" {
179			v.Set("client_id", clientID)
180		}
181		if clientSecret != "" {
182			v.Set("client_secret", clientSecret)
183		}
184	}
185	req, err := http.NewRequest("POST", tokenURL, strings.NewReader(v.Encode()))
186	if err != nil {
187		return nil, err
188	}
189	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
190	if !bustedAuth {
191		req.SetBasicAuth(clientID, clientSecret)
192	}
193	r, err := ctxhttp.Do(ctx, hc, req)
194	if err != nil {
195		return nil, err
196	}
197	defer r.Body.Close()
198	body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1<<20))
199	if err != nil {
200		return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err)
201	}
202	if code := r.StatusCode; code < 200 || code > 299 {
203		return nil, fmt.Errorf("oauth2: cannot fetch token: %v\nResponse: %s", r.Status, body)
204	}
205
206	var token *Token
207	content, _, _ := mime.ParseMediaType(r.Header.Get("Content-Type"))
208	switch content {
209	case "application/x-www-form-urlencoded", "text/plain":
210		vals, err := url.ParseQuery(string(body))
211		if err != nil {
212			return nil, err
213		}
214		token = &Token{
215			AccessToken:  vals.Get("access_token"),
216			TokenType:    vals.Get("token_type"),
217			RefreshToken: vals.Get("refresh_token"),
218			Raw:          vals,
219		}
220		e := vals.Get("expires_in")
221		if e == "" {
222			// TODO(jbd): Facebook's OAuth2 implementation is broken and
223			// returns expires_in field in expires. Remove the fallback to expires,
224			// when Facebook fixes their implementation.
225			e = vals.Get("expires")
226		}
227		expires, _ := strconv.Atoi(e)
228		if expires != 0 {
229			token.Expiry = time.Now().Add(time.Duration(expires) * time.Second)
230		}
231	default:
232		var tj tokenJSON
233		if err = json.Unmarshal(body, &tj); err != nil {
234			return nil, err
235		}
236		token = &Token{
237			AccessToken:  tj.AccessToken,
238			TokenType:    tj.TokenType,
239			RefreshToken: tj.RefreshToken,
240			Expiry:       tj.expiry(),
241			Raw:          make(map[string]interface{}),
242		}
243		json.Unmarshal(body, &token.Raw) // no error checks for optional fields
244	}
245	// Don't overwrite `RefreshToken` with an empty value
246	// if this was a token refreshing request.
247	if token.RefreshToken == "" {
248		token.RefreshToken = v.Get("refresh_token")
249	}
250	return token, nil
251}
252