// 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" ) // Cacher implementations read and write OAuth 2.0 tokens from a cache. type Cacher interface { // Read reads the token from the cache. // If the read is successful, it should return the token and a nil error. // The returned tokens may be expired tokens. // If there is no token in the cache, it should return a nil token and a nil error. // It should return a non-nil error when an unrecoverable failure occurs. Read() (*Token, error) // Write writes the token to the cache. Write(*Token) } // 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.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 } } // Cache requires a Cacher implementation. It will initially read // the token if the transport is initialized with NewTransportFromCache // and will write the refreshed tokens back to the cache. func Cache(c Cacher) Option { return func(o *Options) error { o.Cache = c 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 the required JWT params. f.opts.TokenFetcherFunc = makeTwoLeggedFetcher(&f.opts) return f, nil case f.opts.AuthURL != nil && f.opts.TokenURL != nil: // TODO(jbd): Assert the 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, " ")), }) } // NewTransportFromCache reads the token from the cache and returns // a Transport that is authorized and the authenticated // by the returned token. func (f *Flow) NewTransportFromCache() (*Transport, error) { if f.opts.Cache == nil { return nil, errors.New("oauth2: no cache is set") } tok, err := f.opts.Cache.Read() if err != nil { return nil, err } if tok == nil { return nil, nil } return f.newTransportFromToken(tok), nil } // 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 } // NewTransport returns a Transport. func (f *Flow) NewTransport() *Transport { return f.newTransportFromToken(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, t) } 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 Cache Cacher 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"].(int) 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"].(int) } expires = 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 }