github.com/timstclair/heapster@v0.20.0-alpha1/Godeps/_workspace/src/golang.org/x/oauth2/internal/token.go (about)

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