2014-05-06 01:54:23 +04:00
|
|
|
// Package oauth2 provides support for making
|
|
|
|
// OAuth2 authorized and authenticated HTTP requests.
|
|
|
|
// It can additionally grant authorization with Bearer JWT.
|
|
|
|
//
|
|
|
|
// Example usage:
|
|
|
|
//
|
|
|
|
// // Specify your configuration. (typically as a global variable)
|
|
|
|
// var config = oauth2.NewConfig(&oauth2.Options{
|
|
|
|
// ClientID: YOUR_CLIENT_ID,
|
|
|
|
// ClientSecret: YOUR_CLIENT_SECRET,
|
|
|
|
// RedirectURL: "http://you.example.org/handler",
|
|
|
|
// Scopes: []string{ "scope1", "scope2" },
|
|
|
|
// }, OAUTH2_PROVIDER_AUTH_URL, OAUTH2_PROVIDER_TOKEN_URL)
|
|
|
|
//
|
|
|
|
// // A landing page redirects to the OAuth provider to get the auth code.
|
|
|
|
// func landing(w http.ResponseWriter, r *http.Request) {
|
|
|
|
// http.Redirect(w, r, config.AuthCodeURL("foo"), http.StatusFound)
|
|
|
|
// }
|
|
|
|
//
|
|
|
|
// // The user will be redirected back to this handler, that takes the
|
|
|
|
// // "code" query parameter and Exchanges it for an access token.
|
|
|
|
// func handler(w http.ResponseWriter, r *http.Request) {
|
|
|
|
// t, err := config.NewTransportWithCode(r.FormValue("code"))
|
|
|
|
// // The Transport now has a valid Token. Create an *http.Client
|
|
|
|
// // with which we can make authenticated API requests.
|
|
|
|
// c := t.Client()
|
|
|
|
// c.Post(...)
|
|
|
|
// }
|
|
|
|
//
|
|
|
|
package oauth2
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
|
|
|
"errors"
|
|
|
|
"io/ioutil"
|
|
|
|
"mime"
|
|
|
|
"net/http"
|
|
|
|
"net/url"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
)
|
|
|
|
|
|
|
|
// The default transport implementation to be used while
|
|
|
|
// making the authorized requests.
|
|
|
|
var DefaultTransport = http.DefaultTransport
|
|
|
|
|
|
|
|
type tokenRespBody struct {
|
|
|
|
AccessToken string `json:"access_token"`
|
|
|
|
TokenType string `json:"token_type"`
|
|
|
|
RefreshToken string `json:"refresh_token"`
|
|
|
|
ExpiresIn time.Duration `json:"expires_in"`
|
|
|
|
IdToken string `json:"id_token"`
|
|
|
|
}
|
|
|
|
|
2014-05-10 11:41:39 +04:00
|
|
|
// TokenFetcher refreshes or fetches a new access token from the
|
2014-05-06 01:54:23 +04:00
|
|
|
// provider. It should return an error if it's not capable of
|
|
|
|
// retrieving a token.
|
2014-05-10 11:41:39 +04:00
|
|
|
type TokenFetcher interface {
|
|
|
|
// FetchToken retrieves a new access token for the provider.
|
2014-05-06 01:54:23 +04:00
|
|
|
// If the implementation doesn't know how to retrieve a new token,
|
|
|
|
// it returns an error.
|
2014-05-10 11:41:39 +04:00
|
|
|
FetchToken(existing *Token) (*Token, error)
|
2014-05-06 01: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"`
|
|
|
|
|
|
|
|
// Optional, identifies the level of access being requested.
|
|
|
|
Scopes []string `json:"scopes"`
|
|
|
|
|
|
|
|
// Optional, "online" (default) or "offline", no refresh token if "online"
|
|
|
|
AccessType string `json:"omit"`
|
|
|
|
|
|
|
|
// 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.
|
|
|
|
ApprovalPrompt string `json:"omit"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// Config represents an OAuth 2.0 provider and client options to
|
|
|
|
// provide authorized transports.
|
|
|
|
type Config interface {
|
|
|
|
// NewTransport creates a transport which is configured to be
|
|
|
|
// authorized with the config provided.
|
|
|
|
NewTransport() (Transport, error)
|
|
|
|
|
|
|
|
// NewTransportWithCode creates a transport after going through
|
|
|
|
// the OAuth 2.0 exchange flow to retrieve a valid token from
|
|
|
|
// the exchange server.
|
|
|
|
NewTransportWithCode(exchangeCode string) (Transport, error)
|
|
|
|
|
|
|
|
// AuthCodeURL generates a URL to the consent page.
|
|
|
|
AuthCodeURL(state string) (string, error)
|
|
|
|
|
|
|
|
// Exchange ecxhanges the code with the provider to retrieve
|
|
|
|
// a new access token.
|
|
|
|
Exchange(exchangeCode string) (*Token, error)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Config represents an OAuth 2.0 provider and client options to
|
|
|
|
// provide authorized transports with a Bearer JWT token.
|
|
|
|
type JWTConfig interface {
|
|
|
|
// NewTransport creates a transport which is configured to
|
|
|
|
// be authorized with OAuth 2.0 JWT Bearer flow.
|
|
|
|
NewTransport() (Transport, error)
|
|
|
|
|
|
|
|
// NewTransportWithUser creates a transport which is configured
|
|
|
|
// to be authorized with OAuth 2.0 JWT Bearer flow and
|
|
|
|
// impersonates the provided user.
|
|
|
|
NewTransportWithUser(user string) (Transport, error)
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewConfig creates a generic OAuth 2.0 configuration that talks
|
|
|
|
// to an OAuth 2.0 provider specified with authURL and tokenURL.
|
|
|
|
func NewConfig(opts *Options, authURL, tokenURL string) (Config, error) {
|
|
|
|
conf := &config{
|
|
|
|
opts: opts,
|
|
|
|
authURL: authURL,
|
|
|
|
tokenURL: tokenURL,
|
|
|
|
}
|
|
|
|
if err := conf.validate(); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return conf, nil
|
|
|
|
}
|
|
|
|
|
2014-05-10 11:41:39 +04:00
|
|
|
// config represents the configuration of an OAuth 2.0 consumer client.
|
2014-05-06 01:54:23 +04:00
|
|
|
type config struct {
|
|
|
|
opts *Options
|
|
|
|
// AuthURL is the URL the user will be directed to
|
|
|
|
// in order to grant access.
|
|
|
|
authURL string
|
|
|
|
// TokenURL is the URL used to retrieve OAuth tokens.
|
|
|
|
tokenURL string
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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, err error) {
|
|
|
|
u, err := url.Parse(c.authURL)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
q := url.Values{
|
|
|
|
"response_type": {"code"},
|
|
|
|
"client_id": {c.opts.ClientID},
|
|
|
|
"redirect_uri": {c.opts.RedirectURL},
|
|
|
|
"scope": {strings.Join(c.opts.Scopes, " ")},
|
|
|
|
"state": {state},
|
|
|
|
"access_type": {c.opts.AccessType},
|
|
|
|
"approval_prompt": {c.opts.ApprovalPrompt},
|
|
|
|
}.Encode()
|
|
|
|
if u.RawQuery == "" {
|
|
|
|
u.RawQuery = q
|
|
|
|
} else {
|
|
|
|
u.RawQuery += "&" + q
|
|
|
|
}
|
|
|
|
return u.String(), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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, error) {
|
2014-05-10 11:41:39 +04:00
|
|
|
return NewAuthorizedTransport(c, nil), nil
|
2014-05-06 01:54:23 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
// NewTransportWithCode exchanges the OAuth 2.0 exchange code with
|
|
|
|
// 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(exchangeCode string) (Transport, error) {
|
|
|
|
token, err := c.Exchange(exchangeCode)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2014-05-10 11:41:39 +04:00
|
|
|
return NewAuthorizedTransport(c, token), nil
|
2014-05-06 01:54:23 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
// Exchange exchanges the exchange code with the OAuth 2.0 provider
|
|
|
|
// to retrieve a new access token.
|
|
|
|
func (c *config) Exchange(exchangeCode string) (*Token, error) {
|
|
|
|
token := &Token{}
|
|
|
|
err := c.updateToken(token, url.Values{
|
|
|
|
"grant_type": {"authorization_code"},
|
|
|
|
"redirect_uri": {c.opts.RedirectURL},
|
|
|
|
"scope": {strings.Join(c.opts.Scopes, " ")},
|
|
|
|
"code": {exchangeCode},
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return token, nil
|
|
|
|
}
|
|
|
|
|
2014-05-10 11:41:39 +04:00
|
|
|
// FetchToken retrieves a new access token and updates the existing token
|
2014-05-06 01:54:23 +04:00
|
|
|
// with the newly fetched credentials. If existing token doesn't
|
|
|
|
// contain a refresh token, it returns an error.
|
2014-05-10 11:41:39 +04:00
|
|
|
func (c *config) FetchToken(existing *Token) (*Token, error) {
|
2014-05-06 01:54:23 +04:00
|
|
|
if existing == nil || existing.RefreshToken == "" {
|
|
|
|
return nil, errors.New("cannot fetch access token without refresh token.")
|
|
|
|
}
|
|
|
|
err := c.updateToken(existing, url.Values{
|
|
|
|
"grant_type": {"refresh_token"},
|
|
|
|
"refresh_token": {existing.RefreshToken},
|
|
|
|
})
|
|
|
|
return existing, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Checks if all required configuration fields have non-zero values.
|
|
|
|
func (c *config) validate() error {
|
|
|
|
if c.opts.ClientID == "" {
|
|
|
|
return errors.New("A client ID should be provided.")
|
|
|
|
}
|
|
|
|
if c.opts.ClientSecret == "" {
|
|
|
|
return errors.New("A client secret should be provided.")
|
|
|
|
}
|
|
|
|
// TODO(jbd): Are redirect URIs allowed to be a
|
|
|
|
// non-value string in the spec?
|
|
|
|
if c.opts.RedirectURL == "" {
|
|
|
|
return errors.New("A redirect URL should be provided.")
|
|
|
|
}
|
|
|
|
// TODO(jbd): Validate the URLs. Maybe convert them to URL
|
|
|
|
// objects on construction.
|
|
|
|
if c.authURL == "" {
|
|
|
|
return errors.New("An auth URL should be provided.")
|
|
|
|
}
|
|
|
|
if c.tokenURL == "" {
|
|
|
|
return errors.New("A token URL should be provided.")
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *config) updateToken(tok *Token, v url.Values) error {
|
|
|
|
v.Set("client_id", c.opts.ClientID)
|
|
|
|
v.Set("client_secret", c.opts.ClientSecret)
|
|
|
|
r, err := (&http.Client{Transport: DefaultTransport}).PostForm(c.tokenURL, v)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer r.Body.Close()
|
|
|
|
if r.StatusCode != 200 {
|
|
|
|
// TODO(jbd): Add status code or error message
|
|
|
|
return errors.New("Error during updating token.")
|
|
|
|
}
|
|
|
|
|
|
|
|
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 err
|
|
|
|
}
|
|
|
|
vals, err := url.ParseQuery(string(body))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
resp.AccessToken = vals.Get("access_token")
|
|
|
|
resp.TokenType = vals.Get("token_type")
|
|
|
|
resp.RefreshToken = vals.Get("refresh_token")
|
|
|
|
resp.ExpiresIn, _ = time.ParseDuration(vals.Get("expires_in") + "s")
|
|
|
|
resp.IdToken = vals.Get("id_token")
|
|
|
|
default:
|
|
|
|
if err = json.NewDecoder(r.Body).Decode(&resp); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
// The JSON parser treats the unitless ExpiresIn like 'ns' instead of 's' as above,
|
|
|
|
// so compensate here.
|
|
|
|
resp.ExpiresIn *= time.Second
|
|
|
|
}
|
|
|
|
tok.AccessToken = resp.AccessToken
|
|
|
|
tok.TokenType = resp.TokenType
|
|
|
|
// Don't overwrite `RefreshToken` with an empty value
|
2014-05-10 11:41:39 +04:00
|
|
|
if resp.RefreshToken != "" {
|
2014-05-06 01:54:23 +04:00
|
|
|
tok.RefreshToken = resp.RefreshToken
|
|
|
|
}
|
|
|
|
if resp.ExpiresIn == 0 {
|
|
|
|
tok.Expiry = time.Time{}
|
|
|
|
} else {
|
|
|
|
tok.Expiry = time.Now().Add(resp.ExpiresIn)
|
|
|
|
}
|
|
|
|
if resp.IdToken != "" {
|
|
|
|
if tok.Extra == nil {
|
|
|
|
tok.Extra = make(map[string]string)
|
|
|
|
}
|
|
|
|
tok.Extra["id_token"] = resp.IdToken
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|