mirror of
https://github.com/golang/oauth2.git
synced 2025-07-21 00:00:09 +08:00
Go treats json numbers as float64 not int. Previously json response expiry information was ignored since it was expected to be an int.
380 lines
12 KiB
Go
380 lines
12 KiB
Go
// 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 (
|
|
"crypto/rsa"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"mime"
|
|
"time"
|
|
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// Option represents a function that applies some state to
|
|
// an Options object.
|
|
type Option func(*Options) error
|
|
|
|
// Client requires the OAuth 2.0 client credentials. You need to provide
|
|
// the client identifier and optionally the client secret that are
|
|
// assigned to your application by the OAuth 2.0 provider.
|
|
func Client(id, secret string) Option {
|
|
return func(opts *Options) error {
|
|
opts.ClientID = id
|
|
opts.ClientSecret = secret
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// RedirectURL requires the URL to which the user will be returned after
|
|
// granting (or denying) access.
|
|
func RedirectURL(url string) Option {
|
|
return func(opts *Options) error {
|
|
opts.RedirectURL = url
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// Scope requires a list of requested permission scopes.
|
|
// It is optinal to specify scopes.
|
|
func Scope(scopes ...string) Option {
|
|
return func(o *Options) error {
|
|
o.Scopes = scopes
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// Endpoint requires OAuth 2.0 provider's authorization and token endpoints.
|
|
func Endpoint(authURL, tokenURL string) Option {
|
|
return func(o *Options) error {
|
|
au, err := url.Parse(authURL)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tu, err := url.Parse(tokenURL)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
o.TokenFetcherFunc = makeThreeLeggedFetcher(o)
|
|
o.AuthURL = au
|
|
o.TokenURL = tu
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// HTTPClient allows you to provide a custom http.Client to be
|
|
// used to retrieve tokens from the OAuth 2.0 provider.
|
|
func HTTPClient(c *http.Client) Option {
|
|
return func(o *Options) error {
|
|
o.Client = c
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// RoundTripper allows you to provide a custom http.RoundTripper
|
|
// to be used to construct new oauth2.Transport instances.
|
|
// If none is provided a default RoundTripper will be used.
|
|
func RoundTripper(tr http.RoundTripper) Option {
|
|
return func(o *Options) error {
|
|
o.Transport = tr
|
|
return nil
|
|
}
|
|
}
|
|
|
|
type Flow struct {
|
|
opts Options
|
|
}
|
|
|
|
// New initiates a new flow. It determines the type of the OAuth 2.0
|
|
// (2-legged, 3-legged or custom) by looking at the provided options.
|
|
// If the flow type cannot determined automatically, an error is returned.
|
|
func New(options ...Option) (*Flow, error) {
|
|
f := &Flow{}
|
|
for _, opt := range options {
|
|
if err := opt(&f.opts); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
switch {
|
|
case f.opts.TokenFetcherFunc != nil:
|
|
return f, nil
|
|
case f.opts.AUD != nil:
|
|
// TODO(jbd): Assert required JWT params.
|
|
f.opts.TokenFetcherFunc = makeTwoLeggedFetcher(&f.opts)
|
|
return f, nil
|
|
case f.opts.AuthURL != nil && f.opts.TokenURL != nil:
|
|
// TODO(jbd): Assert required OAuth2 params.
|
|
f.opts.TokenFetcherFunc = makeThreeLeggedFetcher(&f.opts)
|
|
return f, nil
|
|
default:
|
|
return nil, errors.New("oauth2: missing endpoints, can't determine how to fetch tokens")
|
|
}
|
|
}
|
|
|
|
// 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 (f *Flow) AuthCodeURL(state, accessType, prompt string) string {
|
|
u := f.opts.AuthURL
|
|
v := url.Values{
|
|
"response_type": {"code"},
|
|
"client_id": {f.opts.ClientID},
|
|
"redirect_uri": condVal(f.opts.RedirectURL),
|
|
"scope": condVal(strings.Join(f.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()
|
|
}
|
|
|
|
// exchange exchanges the authorization code with the OAuth 2.0 provider
|
|
// to retrieve a new access token.
|
|
func (f *Flow) exchange(code string) (*Token, error) {
|
|
return retrieveToken(&f.opts, url.Values{
|
|
"grant_type": {"authorization_code"},
|
|
"code": {code},
|
|
"redirect_uri": condVal(f.opts.RedirectURL),
|
|
"scope": condVal(strings.Join(f.opts.Scopes, " ")),
|
|
})
|
|
}
|
|
|
|
// NewTransportFromCode exchanges the code to retrieve a new access token
|
|
// and returns an authorized and authenticated Transport.
|
|
func (f *Flow) NewTransportFromCode(code string) (*Transport, error) {
|
|
token, err := f.exchange(code)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return f.NewTransportFromToken(token), nil
|
|
}
|
|
|
|
// NewTransportFromToken returns a new Transport that is authorized
|
|
// and authenticated with the provided token.
|
|
func (f *Flow) NewTransportFromToken(t *Token) *Transport {
|
|
tr := f.opts.Transport
|
|
if tr == nil {
|
|
tr = http.DefaultTransport
|
|
}
|
|
return newTransport(tr, f.opts.TokenFetcherFunc, t)
|
|
}
|
|
|
|
// NewTransport returns a Transport.
|
|
func (f *Flow) NewTransport() *Transport {
|
|
return f.NewTransportFromToken(nil)
|
|
}
|
|
|
|
func makeThreeLeggedFetcher(o *Options) func(t *Token) (*Token, error) {
|
|
return func(t *Token) (*Token, error) {
|
|
if t == nil || t.RefreshToken == "" {
|
|
return nil, errors.New("oauth2: cannot fetch access token without refresh token")
|
|
}
|
|
return retrieveToken(o, url.Values{
|
|
"grant_type": {"refresh_token"},
|
|
"refresh_token": {t.RefreshToken},
|
|
})
|
|
}
|
|
}
|
|
|
|
// Options represents an object to keep the state of the OAuth 2.0 flow.
|
|
type Options struct {
|
|
// ClientID is the OAuth client identifier used when communicating with
|
|
// the configured OAuth provider.
|
|
ClientID string
|
|
|
|
// ClientSecret is the OAuth client secret used when communicating with
|
|
// the configured OAuth provider.
|
|
ClientSecret string
|
|
|
|
// RedirectURL is the URL to which the user will be returned after
|
|
// granting (or denying) access.
|
|
RedirectURL string
|
|
|
|
// Email is the OAuth client identifier used when communicating with
|
|
// the configured OAuth provider.
|
|
Email string
|
|
|
|
// PrivateKey contains the contents of an RSA private key or the
|
|
// contents of a PEM file that contains a private key. The provided
|
|
// private key is used to sign JWT payloads.
|
|
// PEM containers with a passphrase are not supported.
|
|
// Use the following command to convert a PKCS 12 file into a PEM.
|
|
//
|
|
// $ openssl pkcs12 -in key.p12 -out key.pem -nodes
|
|
//
|
|
PrivateKey *rsa.PrivateKey
|
|
|
|
// Scopes identify the level of access being requested.
|
|
Subject string
|
|
|
|
// Scopes optionally specifies a list of requested permission scopes.
|
|
Scopes []string
|
|
|
|
// AuthURL represents the authorization endpoint of the OAuth 2.0 provider.
|
|
AuthURL *url.URL
|
|
|
|
// TokenURL represents the token endpoint of the OAuth 2.0 provider.
|
|
TokenURL *url.URL
|
|
|
|
// AUD represents the token endpoint required to complete the 2-legged JWT flow.
|
|
AUD *url.URL
|
|
|
|
TokenFetcherFunc func(t *Token) (*Token, error)
|
|
|
|
Transport http.RoundTripper
|
|
Client *http.Client
|
|
}
|
|
|
|
func retrieveToken(o *Options, v url.Values) (*Token, error) {
|
|
v.Set("client_id", o.ClientID)
|
|
bustedAuth := !providerAuthHeaderWorks(o.TokenURL.String())
|
|
if bustedAuth && o.ClientSecret != "" {
|
|
v.Set("client_secret", o.ClientSecret)
|
|
}
|
|
req, err := http.NewRequest("POST", o.TokenURL.String(), strings.NewReader(v.Encode()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
if !bustedAuth && o.ClientSecret != "" {
|
|
req.SetBasicAuth(o.ClientID, o.ClientSecret)
|
|
}
|
|
c := o.Client
|
|
if c == nil {
|
|
c = &http.Client{}
|
|
}
|
|
r, err := c.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 code := r.StatusCode; code < 200 || code > 299 {
|
|
return nil, fmt.Errorf("oauth2: cannot fetch token: %v\nResponse: %s", r.Status, body)
|
|
}
|
|
|
|
token := &Token{}
|
|
expires := int(0)
|
|
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
|
|
}
|
|
token.AccessToken = vals.Get("access_token")
|
|
token.TokenType = vals.Get("token_type")
|
|
token.RefreshToken = vals.Get("refresh_token")
|
|
token.raw = vals
|
|
e := vals.Get("expires_in")
|
|
if e == "" {
|
|
// TODO(jbd): Facebook's OAuth2 implementation is broken and
|
|
// returns expires_in field in expires. Remove the fallback to expires,
|
|
// when Facebook fixes their implementation.
|
|
e = vals.Get("expires")
|
|
}
|
|
expires, _ = strconv.Atoi(e)
|
|
default:
|
|
b := make(map[string]interface{})
|
|
if err = json.Unmarshal(body, &b); err != nil {
|
|
return nil, err
|
|
}
|
|
token.AccessToken, _ = b["access_token"].(string)
|
|
token.TokenType, _ = b["token_type"].(string)
|
|
token.RefreshToken, _ = b["refresh_token"].(string)
|
|
token.raw = b
|
|
e, ok := b["expires_in"].(float64)
|
|
if !ok {
|
|
// TODO(jbd): Facebook's OAuth2 implementation is broken and
|
|
// returns expires_in field in expires. Remove the fallback to expires,
|
|
// when Facebook fixes their implementation.
|
|
e, _ = b["expires"].(float64)
|
|
}
|
|
expires = int(e)
|
|
}
|
|
// Don't overwrite `RefreshToken` with an empty value
|
|
// if this was a token refreshing request.
|
|
if token.RefreshToken == "" {
|
|
token.RefreshToken = v.Get("refresh_token")
|
|
}
|
|
if expires == 0 {
|
|
token.Expiry = time.Time{}
|
|
} else {
|
|
token.Expiry = time.Now().Add(time.Duration(expires) * time.Second)
|
|
}
|
|
return token, nil
|
|
}
|
|
|
|
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
|
|
}
|