oauth2/oauth2.go

303 lines
9.1 KiB
Go
Raw Normal View History

// 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.
2014-05-13 14:06:46 -04:00
2014-05-05 17:54:23 -04:00
// 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"
"io/ioutil"
"mime"
"net/http"
"net/url"
2014-08-12 22:50:34 -04:00
"strconv"
2014-05-05 17:54:23 -04:00
"strings"
"time"
)
type tokenRespBody struct {
2014-08-12 22:50:34 -04:00
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"`
2014-05-05 17:54:23 -04:00
}
// TokenFetcher refreshes or fetches a new access token from the
2014-05-05 17:54:23 -04:00
// 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.
2014-05-05 17:54:23 -04:00
// 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)
2014-05-05 17:54:23 -04:00
}
// Options represents options to provide OAuth 2.0 client credentials
// and access level. A sample configuration:
//
// opts := &oauth2.Options{
// ClientID: "<clientID>",
// ClientSecret: "ad4364309eff",
// RedirectURL: "https://homepage/oauth2callback",
// Scopes: []string{"scope1", "scope2"},
// AccessType: "offline", // retrieves a refresh token
// }
//
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"`
2014-08-13 16:40:18 -04:00
// Scopes optionally specifies a list of requested permission scopes.
Scopes []string `json:"scopes,omitempty"`
2014-05-05 17:54:23 -04:00
2014-08-12 22:50:34 -04:00
// AccessType is an OAuth extension that gets sent as the
// "access_type" field in the URL from AuthCodeURL.
// See https://developers.google.com/accounts/docs/OAuth2WebServer.
// It may be "online" (the 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.
2014-08-13 02:42:30 -04:00
AccessType string `json:"access_type,omitempty"`
2014-05-05 17:54:23 -04:00
// ApprovalPrompt 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.
2014-08-13 02:42:30 -04:00
ApprovalPrompt string `json:"-"`
2014-05-05 17:54:23 -04:00
}
// NewConfig creates a generic OAuth 2.0 configuration that talks
// to an OAuth 2.0 provider specified with authURL and tokenURL.
2014-07-21 00:07:57 -04:00
func NewConfig(opts *Options, authURL, tokenURL string) (*Config, error) {
2014-07-21 00:08:11 -04:00
aURL, err := url.Parse(authURL)
if err != nil {
2014-07-21 00:07:57 -04:00
return nil, err
}
2014-07-21 00:08:11 -04:00
tURL, err := url.Parse(tokenURL)
if err != nil {
2014-07-21 00:07:57 -04:00
return nil, err
2014-05-05 17:54:23 -04:00
}
conf := &Config{
Client: http.DefaultClient,
Transport: http.DefaultTransport,
opts: opts,
authURL: aURL,
tokenURL: tURL,
}
if err = conf.validate(); err != nil {
2014-05-05 17:54:23 -04:00
return nil, err
}
2014-07-21 00:07:57 -04:00
return conf, nil
2014-05-05 17:54:23 -04:00
}
// 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
2014-05-05 17:54:23 -04:00
opts *Options
// AuthURL is the URL the user will be directed to
// in order to grant access.
authURL *url.URL
2014-05-05 17:54:23 -04:00
// TokenURL is the URL used to retrieve OAuth tokens.
tokenURL *url.URL
2014-05-05 17:54:23 -04:00
}
// AuthCodeURL returns a URL to OAuth 2.0 provider's consent page
// that asks for permissions for the required scopes explicitly.
func (c *Config) AuthCodeURL(state string) (authURL string) {
2014-07-21 00:08:11 -04:00
u := *c.authURL
vals := url.Values{
"response_type": {"code"},
"client_id": {c.opts.ClientID},
"scope": {strings.Join(c.opts.Scopes, " ")},
"state": {state},
}
if c.opts.AccessType != "" {
vals.Set("access_type", c.opts.AccessType)
}
if c.opts.ApprovalPrompt != "" {
vals.Set("approval_prompt", c.opts.ApprovalPrompt)
}
if c.opts.RedirectURL != "" {
vals.Set("redirect_uri", c.opts.RedirectURL)
}
q := vals.Encode()
2014-05-05 17:54:23 -04:00
if u.RawQuery == "" {
u.RawQuery = q
} else {
u.RawQuery += "&" + q
}
return u.String()
2014-05-05 17:54:23 -04:00
}
// 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.
//
// Example:
// t, _ := c.NewTransport()
// t.SetToken(validToken)
//
func (c *Config) NewTransport() *Transport {
return NewTransport(c.transport(), c, nil)
2014-05-05 17:54:23 -04:00
}
// NewTransportWithCode exchanges the OAuth 2.0 authorization code with
2014-05-05 17:54:23 -04:00
// the provider to fetch a new access token (and refresh token). Once
// it succesffully retrieves a new token, creates a new transport
// authorized with it.
func (c *Config) NewTransportWithCode(code string) (*Transport, error) {
token, err := c.Exchange(code)
2014-05-05 17:54:23 -04:00
if err != nil {
return nil, err
}
return NewTransport(c.transport(), c, token), nil
2014-05-05 17:54:23 -04:00
}
// FetchToken retrieves a new access token and updates the existing token
2014-05-05 17:54:23 -04:00
// 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) {
2014-05-05 17:54:23 -04:00
if existing == nil || existing.RefreshToken == "" {
return nil, errors.New("cannot fetch access token without refresh token.")
}
return c.retrieveToken(url.Values{
2014-05-05 17:54:23 -04:00
"grant_type": {"refresh_token"},
"client_secret": {c.opts.ClientSecret},
2014-05-05 17:54:23 -04:00
"refresh_token": {existing.RefreshToken},
})
}
// Checks if all required configuration fields have non-zero values.
func (c *Config) validate() error {
2014-05-05 17:54:23 -04:00
if c.opts.ClientID == "" {
return errors.New("oauth2: missing client ID")
2014-05-05 17:54:23 -04:00
}
if c.opts.ClientSecret == "" {
return errors.New("oauth2: missing client secret")
2014-05-05 17:54:23 -04:00
}
return nil
}
// Exchange exchanges the authorization code with the OAuth 2.0 provider
2014-06-26 20:27:53 -04:00
// to retrieve a new access token.
func (c *Config) Exchange(code string) (*Token, error) {
vals := url.Values{
2014-08-12 22:50:34 -04:00
"grant_type": {"authorization_code"},
"client_secret": {c.opts.ClientSecret},
"code": {code},
}
if len(c.opts.Scopes) != 0 {
vals.Set("scope", strings.Join(c.opts.Scopes, " "))
}
if c.opts.RedirectURL != "" {
vals.Set("redirect_uri", c.opts.RedirectURL)
}
return c.retrieveToken(vals)
2014-06-26 20:27:53 -04:00
}
func (c *Config) retrieveToken(v url.Values) (*Token, error) {
2014-05-05 17:54:23 -04:00
v.Set("client_id", c.opts.ClientID)
2014-08-12 22:50:34 -04:00
// Note that we're not setting v's client_secret to t.ClientSecret, due
// to https://code.google.com/p/goauth2/issues/detail?id=31
// Reddit only accepts client_secret in Authorization header.
// Dropbox accepts either, but not both.
// The spec requires servers to always support the Authorization header,
// so that's all we use.
r, err := c.client().PostForm(c.tokenURL.String(), v)
2014-05-05 17:54:23 -04:00
if err != nil {
return nil, err
2014-05-05 17:54:23 -04:00
}
defer r.Body.Close()
if r.StatusCode != 200 {
// TODO(jbd): Add status code or error message
return nil, errors.New("error during updating token")
2014-05-05 17:54:23 -04:00
}
resp := &tokenRespBody{}
content, _, _ := mime.ParseMediaType(r.Header.Get("Content-Type"))
switch content {
case "application/x-www-form-urlencoded", "text/plain":
body, err := ioutil.ReadAll(r.Body)
if err != nil {
return nil, err
2014-05-05 17:54:23 -04:00
}
vals, err := url.ParseQuery(string(body))
if err != nil {
return nil, err
2014-05-05 17:54:23 -04:00
}
resp.AccessToken = vals.Get("access_token")
resp.TokenType = vals.Get("token_type")
resp.RefreshToken = vals.Get("refresh_token")
2014-08-12 22:50:34 -04:00
resp.ExpiresIn, _ = strconv.ParseInt(vals.Get("expires_in"), 10, 64)
2014-05-05 17:54:23 -04:00
resp.IdToken = vals.Get("id_token")
default:
if err = json.NewDecoder(r.Body).Decode(&resp); err != nil {
return nil, err
2014-05-05 17:54:23 -04:00
}
}
token := &Token{
AccessToken: resp.AccessToken,
TokenType: resp.TokenType,
RefreshToken: resp.RefreshToken,
}
2014-05-05 17:54:23 -04:00
// Don't overwrite `RefreshToken` with an empty value
// if this was a token refreshing request.
if resp.RefreshToken == "" {
token.RefreshToken = v.Get("refresh_token")
2014-05-05 17:54:23 -04:00
}
if resp.ExpiresIn == 0 {
token.Expiry = time.Time{}
2014-05-05 17:54:23 -04:00
} else {
token.Expiry = time.Now().Add(time.Duration(resp.ExpiresIn) * time.Second)
2014-05-05 17:54:23 -04:00
}
if resp.IdToken != "" {
if token.Extra == nil {
token.Extra = make(map[string]string)
2014-05-05 17:54:23 -04:00
}
token.Extra["id_token"] = resp.IdToken
2014-05-05 17:54:23 -04:00
}
return token, nil
2014-05-05 17:54:23 -04:00
}
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
}