/oauth/oauth.go
https://code.google.com/p/goauth2/ · Go · 398 lines · 258 code · 37 blank · 103 comment · 79 complexity · 242822d19c909c0864ea5aa79fe0aad1 MD5 · raw file
- // Copyright 2011 The goauth2 Authors. All rights reserved.
- // Use of this source code is governed by a BSD-style
- // license that can be found in the LICENSE file.
- // The oauth package provides support for making
- // OAuth2-authenticated HTTP requests.
- //
- // Example usage:
- //
- // // Specify your configuration. (typically as a global variable)
- // var config = &oauth.Config{
- // ClientId: YOUR_CLIENT_ID,
- // ClientSecret: YOUR_CLIENT_SECRET,
- // Scope: "https://www.googleapis.com/auth/buzz",
- // AuthURL: "https://accounts.google.com/o/oauth2/auth",
- // TokenURL: "https://accounts.google.com/o/oauth2/token",
- // RedirectURL: "http://you.example.org/handler",
- // }
- //
- // // 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 := &oauth.Transport{Config: config}
- // t.Exchange(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(...)
- // // ...
- // // btw, r.FormValue("state") == "foo"
- // }
- //
- package oauth
- import (
- "encoding/json"
- "io/ioutil"
- "mime"
- "net/http"
- "net/url"
- "os"
- "strings"
- "time"
- )
- type OAuthError struct {
- prefix string
- msg string
- }
- func (oe OAuthError) Error() string {
- return "OAuthError: " + oe.prefix + ": " + oe.msg
- }
- // Cache specifies the methods that implement a Token cache.
- type Cache interface {
- Token() (*Token, error)
- PutToken(*Token) error
- }
- // CacheFile implements Cache. Its value is the name of the file in which
- // the Token is stored in JSON format.
- type CacheFile string
- func (f CacheFile) Token() (*Token, error) {
- file, err := os.Open(string(f))
- if err != nil {
- return nil, OAuthError{"CacheFile.Token", err.Error()}
- }
- defer file.Close()
- tok := &Token{}
- if err := json.NewDecoder(file).Decode(tok); err != nil {
- return nil, OAuthError{"CacheFile.Token", err.Error()}
- }
- return tok, nil
- }
- func (f CacheFile) PutToken(tok *Token) error {
- file, err := os.OpenFile(string(f), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
- if err != nil {
- return OAuthError{"CacheFile.PutToken", err.Error()}
- }
- if err := json.NewEncoder(file).Encode(tok); err != nil {
- file.Close()
- return OAuthError{"CacheFile.PutToken", err.Error()}
- }
- if err := file.Close(); err != nil {
- return OAuthError{"CacheFile.PutToken", err.Error()}
- }
- return nil
- }
- // Config is the configuration of an OAuth consumer.
- type Config 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
- // Scope identifies the level of access being requested. Multiple scope
- // values should be provided as a space-delimited string.
- Scope string
- // 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
- // RedirectURL is the URL to which the user will be returned after
- // granting (or denying) access.
- RedirectURL string
- // TokenCache allows tokens to be cached for subsequent requests.
- TokenCache Cache
- AccessType string // Optional, "online" (default) or "offline", no refresh token if "online"
- // 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
- }
- // Token contains an end-user's tokens.
- // This is the data you must store to persist authentication.
- type Token struct {
- AccessToken string
- RefreshToken string
- Expiry time.Time // If zero the token has no (known) expiry time.
- Extra map[string]string // May be nil.
- }
- func (t *Token) Expired() bool {
- if t.Expiry.IsZero() {
- return false
- }
- return t.Expiry.Before(time.Now())
- }
- // Transport implements http.RoundTripper. When configured with a valid
- // Config and Token it can be used to make authenticated HTTP requests.
- //
- // t := &oauth.Transport{config}
- // t.Exchange(code)
- // // t now contains a valid Token
- // r, _, err := t.Client().Get("http://example.org/url/requiring/auth")
- //
- // It will automatically refresh the Token if it can,
- // updating the supplied Token in place.
- type Transport struct {
- *Config
- *Token
- // Transport is the HTTP transport to use when making requests.
- // It will default to http.DefaultTransport if nil.
- // (It should never be an oauth.Transport.)
- Transport http.RoundTripper
- }
- // Client returns an *http.Client that makes OAuth-authenticated requests.
- func (t *Transport) Client() *http.Client {
- return &http.Client{Transport: t}
- }
- func (t *Transport) transport() http.RoundTripper {
- if t.Transport != nil {
- return t.Transport
- }
- return http.DefaultTransport
- }
- // AuthCodeURL returns a URL that the end-user should be redirected to,
- // so that they may obtain an authorization code.
- func (c *Config) AuthCodeURL(state string) string {
- url_, err := url.Parse(c.AuthURL)
- if err != nil {
- panic("AuthURL malformed: " + err.Error())
- }
- q := url.Values{
- "response_type": {"code"},
- "client_id": {c.ClientId},
- "redirect_uri": {c.RedirectURL},
- "scope": {c.Scope},
- "state": {state},
- "access_type": {c.AccessType},
- "approval_prompt": {c.ApprovalPrompt},
- }.Encode()
- if url_.RawQuery == "" {
- url_.RawQuery = q
- } else {
- url_.RawQuery += "&" + q
- }
- return url_.String()
- }
- // Exchange takes a code and gets access Token from the remote server.
- func (t *Transport) Exchange(code string) (*Token, error) {
- if t.Config == nil {
- return nil, OAuthError{"Exchange", "no Config supplied"}
- }
- // If the transport or the cache already has a token, it is
- // passed to `updateToken` to preserve existing refresh token.
- tok := t.Token
- if tok == nil && t.TokenCache != nil {
- tok, _ = t.TokenCache.Token()
- }
- if tok == nil {
- tok = new(Token)
- }
- err := t.updateToken(tok, url.Values{
- "grant_type": {"authorization_code"},
- "redirect_uri": {t.RedirectURL},
- "scope": {t.Scope},
- "code": {code},
- })
- if err != nil {
- return nil, err
- }
- t.Token = tok
- if t.TokenCache != nil {
- return tok, t.TokenCache.PutToken(tok)
- }
- return tok, nil
- }
- // RoundTrip executes a single HTTP transaction using the Transport's
- // Token as authorization headers.
- //
- // This method will attempt to renew the Token if it has expired and may return
- // an error related to that Token renewal before attempting the client request.
- // If the Token cannot be renewed a non-nil os.Error value will be returned.
- // If the Token is invalid callers should expect HTTP-level errors,
- // as indicated by the Response's StatusCode.
- func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
- if t.Token == nil {
- if t.Config == nil {
- return nil, OAuthError{"RoundTrip", "no Config supplied"}
- }
- if t.TokenCache == nil {
- return nil, OAuthError{"RoundTrip", "no Token supplied"}
- }
- var err error
- t.Token, err = t.TokenCache.Token()
- if err != nil {
- return nil, err
- }
- }
- // Refresh the Token if it has expired.
- if t.Expired() {
- if err := t.Refresh(); err != nil {
- return nil, err
- }
- }
- // To set the Authorization header, we must make a copy of the Request
- // so that we don't modify the Request we were given.
- // This is required by the specification of http.RoundTripper.
- req = cloneRequest(req)
- req.Header.Set("Authorization", "Bearer "+t.AccessToken)
- // Make the HTTP request.
- return t.transport().RoundTrip(req)
- }
- // cloneRequest returns a clone of the provided *http.Request.
- // The clone is a shallow copy of the struct and its Header map.
- func cloneRequest(r *http.Request) *http.Request {
- // shallow copy of the struct
- r2 := new(http.Request)
- *r2 = *r
- // deep copy of the Header
- r2.Header = make(http.Header)
- for k, s := range r.Header {
- r2.Header[k] = s
- }
- return r2
- }
- // Refresh renews the Transport's AccessToken using its RefreshToken.
- func (t *Transport) Refresh() error {
- if t.Token == nil {
- return OAuthError{"Refresh", "no existing Token"}
- }
- if t.RefreshToken == "" {
- return OAuthError{"Refresh", "Token expired; no Refresh Token"}
- }
- if t.Config == nil {
- return OAuthError{"Refresh", "no Config supplied"}
- }
- err := t.updateToken(t.Token, url.Values{
- "grant_type": {"refresh_token"},
- "refresh_token": {t.RefreshToken},
- })
- if err != nil {
- return err
- }
- if t.TokenCache != nil {
- return t.TokenCache.PutToken(t.Token)
- }
- return nil
- }
- // AuthenticateClient gets an access Token using the client_credentials grant
- // type.
- func (t *Transport) AuthenticateClient() error {
- if t.Config == nil {
- return OAuthError{"Exchange", "no Config supplied"}
- }
- if t.Token == nil {
- t.Token = &Token{}
- }
- return t.updateToken(t.Token, url.Values{"grant_type": {"client_credentials"}})
- }
- func (t *Transport) updateToken(tok *Token, v url.Values) error {
- v.Set("client_id", t.ClientId)
- v.Set("client_secret", t.ClientSecret)
- client := &http.Client{Transport: t.transport()}
- req, err := http.NewRequest("POST", t.TokenURL, strings.NewReader(v.Encode()))
- if err != nil {
- return err
- }
- req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
- req.SetBasicAuth(t.ClientId, t.ClientSecret)
- r, err := client.Do(req)
- if err != nil {
- return err
- }
- defer r.Body.Close()
- if r.StatusCode != 200 {
- return OAuthError{"updateToken", r.Status}
- }
- var b struct {
- Access string `json:"access_token"`
- Refresh string `json:"refresh_token"`
- ExpiresIn time.Duration `json:"expires_in"`
- Id string `json:"id_token"`
- }
- 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
- }
- b.Access = vals.Get("access_token")
- b.Refresh = vals.Get("refresh_token")
- b.ExpiresIn, _ = time.ParseDuration(vals.Get("expires_in") + "s")
- b.Id = vals.Get("id_token")
- default:
- if err = json.NewDecoder(r.Body).Decode(&b); err != nil {
- return err
- }
- // The JSON parser treats the unitless ExpiresIn like 'ns' instead of 's' as above,
- // so compensate here.
- b.ExpiresIn *= time.Second
- }
- tok.AccessToken = b.Access
- // Don't overwrite `RefreshToken` with an empty value
- if len(b.Refresh) > 0 {
- tok.RefreshToken = b.Refresh
- }
- if b.ExpiresIn == 0 {
- tok.Expiry = time.Time{}
- } else {
- tok.Expiry = time.Now().Add(b.ExpiresIn)
- }
- if b.Id != "" {
- if tok.Extra == nil {
- tok.Extra = make(map[string]string)
- }
- tok.Extra["id_token"] = b.Id
- }
- return nil
- }