PageRenderTime 55ms CodeModel.GetById 14ms app.highlight 36ms RepoModel.GetById 1ms app.codeStats 0ms

/oauth/oauth.go

https://code.google.com/p/goauth2/
Go | 398 lines | 258 code | 37 blank | 103 comment | 77 complexity | 242822d19c909c0864ea5aa79fe0aad1 MD5 | raw file
  1// Copyright 2011 The goauth2 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// The oauth package provides support for making
  6// OAuth2-authenticated HTTP requests.
  7//
  8// Example usage:
  9//
 10//	// Specify your configuration. (typically as a global variable)
 11//	var config = &oauth.Config{
 12//		ClientId:     YOUR_CLIENT_ID,
 13//		ClientSecret: YOUR_CLIENT_SECRET,
 14//		Scope:        "https://www.googleapis.com/auth/buzz",
 15//		AuthURL:      "https://accounts.google.com/o/oauth2/auth",
 16//		TokenURL:     "https://accounts.google.com/o/oauth2/token",
 17//		RedirectURL:  "http://you.example.org/handler",
 18//	}
 19//
 20//	// A landing page redirects to the OAuth provider to get the auth code.
 21//	func landing(w http.ResponseWriter, r *http.Request) {
 22//		http.Redirect(w, r, config.AuthCodeURL("foo"), http.StatusFound)
 23//	}
 24//
 25//	// The user will be redirected back to this handler, that takes the
 26//	// "code" query parameter and Exchanges it for an access token.
 27//	func handler(w http.ResponseWriter, r *http.Request) {
 28//		t := &oauth.Transport{Config: config}
 29//		t.Exchange(r.FormValue("code"))
 30//		// The Transport now has a valid Token. Create an *http.Client
 31//		// with which we can make authenticated API requests.
 32//		c := t.Client()
 33//		c.Post(...)
 34//		// ...
 35//		// btw, r.FormValue("state") == "foo"
 36//	}
 37//
 38package oauth
 39
 40import (
 41	"encoding/json"
 42	"io/ioutil"
 43	"mime"
 44	"net/http"
 45	"net/url"
 46	"os"
 47	"strings"
 48	"time"
 49)
 50
 51type OAuthError struct {
 52	prefix string
 53	msg    string
 54}
 55
 56func (oe OAuthError) Error() string {
 57	return "OAuthError: " + oe.prefix + ": " + oe.msg
 58}
 59
 60// Cache specifies the methods that implement a Token cache.
 61type Cache interface {
 62	Token() (*Token, error)
 63	PutToken(*Token) error
 64}
 65
 66// CacheFile implements Cache. Its value is the name of the file in which
 67// the Token is stored in JSON format.
 68type CacheFile string
 69
 70func (f CacheFile) Token() (*Token, error) {
 71	file, err := os.Open(string(f))
 72	if err != nil {
 73		return nil, OAuthError{"CacheFile.Token", err.Error()}
 74	}
 75	defer file.Close()
 76	tok := &Token{}
 77	if err := json.NewDecoder(file).Decode(tok); err != nil {
 78		return nil, OAuthError{"CacheFile.Token", err.Error()}
 79	}
 80	return tok, nil
 81}
 82
 83func (f CacheFile) PutToken(tok *Token) error {
 84	file, err := os.OpenFile(string(f), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
 85	if err != nil {
 86		return OAuthError{"CacheFile.PutToken", err.Error()}
 87	}
 88	if err := json.NewEncoder(file).Encode(tok); err != nil {
 89		file.Close()
 90		return OAuthError{"CacheFile.PutToken", err.Error()}
 91	}
 92	if err := file.Close(); err != nil {
 93		return OAuthError{"CacheFile.PutToken", err.Error()}
 94	}
 95	return nil
 96}
 97
 98// Config is the configuration of an OAuth consumer.
 99type Config struct {
100	// ClientId is the OAuth client identifier used when communicating with
101	// the configured OAuth provider.
102	ClientId string
103
104	// ClientSecret is the OAuth client secret used when communicating with
105	// the configured OAuth provider.
106	ClientSecret string
107
108	// Scope identifies the level of access being requested. Multiple scope
109	// values should be provided as a space-delimited string.
110	Scope string
111
112	// AuthURL is the URL the user will be directed to in order to grant
113	// access.
114	AuthURL string
115
116	// TokenURL is the URL used to retrieve OAuth tokens.
117	TokenURL string
118
119	// RedirectURL is the URL to which the user will be returned after
120	// granting (or denying) access.
121	RedirectURL string
122
123	// TokenCache allows tokens to be cached for subsequent requests.
124	TokenCache Cache
125
126	AccessType string // Optional, "online" (default) or "offline", no refresh token if "online"
127
128	// ApprovalPrompt indicates whether the user should be
129	// re-prompted for consent. If set to "auto" (default) the
130	// user will be prompted only if they haven't previously
131	// granted consent and the code can only be exchanged for an
132	// access token.
133	// If set to "force" the user will always be prompted, and the
134	// code can be exchanged for a refresh token.
135	ApprovalPrompt string
136}
137
138// Token contains an end-user's tokens.
139// This is the data you must store to persist authentication.
140type Token struct {
141	AccessToken  string
142	RefreshToken string
143	Expiry       time.Time         // If zero the token has no (known) expiry time.
144	Extra        map[string]string // May be nil.
145}
146
147func (t *Token) Expired() bool {
148	if t.Expiry.IsZero() {
149		return false
150	}
151	return t.Expiry.Before(time.Now())
152}
153
154// Transport implements http.RoundTripper. When configured with a valid
155// Config and Token it can be used to make authenticated HTTP requests.
156//
157//	t := &oauth.Transport{config}
158//      t.Exchange(code)
159//      // t now contains a valid Token
160//	r, _, err := t.Client().Get("http://example.org/url/requiring/auth")
161//
162// It will automatically refresh the Token if it can,
163// updating the supplied Token in place.
164type Transport struct {
165	*Config
166	*Token
167
168	// Transport is the HTTP transport to use when making requests.
169	// It will default to http.DefaultTransport if nil.
170	// (It should never be an oauth.Transport.)
171	Transport http.RoundTripper
172}
173
174// Client returns an *http.Client that makes OAuth-authenticated requests.
175func (t *Transport) Client() *http.Client {
176	return &http.Client{Transport: t}
177}
178
179func (t *Transport) transport() http.RoundTripper {
180	if t.Transport != nil {
181		return t.Transport
182	}
183	return http.DefaultTransport
184}
185
186// AuthCodeURL returns a URL that the end-user should be redirected to,
187// so that they may obtain an authorization code.
188func (c *Config) AuthCodeURL(state string) string {
189	url_, err := url.Parse(c.AuthURL)
190	if err != nil {
191		panic("AuthURL malformed: " + err.Error())
192	}
193	q := url.Values{
194		"response_type":   {"code"},
195		"client_id":       {c.ClientId},
196		"redirect_uri":    {c.RedirectURL},
197		"scope":           {c.Scope},
198		"state":           {state},
199		"access_type":     {c.AccessType},
200		"approval_prompt": {c.ApprovalPrompt},
201	}.Encode()
202	if url_.RawQuery == "" {
203		url_.RawQuery = q
204	} else {
205		url_.RawQuery += "&" + q
206	}
207	return url_.String()
208}
209
210// Exchange takes a code and gets access Token from the remote server.
211func (t *Transport) Exchange(code string) (*Token, error) {
212	if t.Config == nil {
213		return nil, OAuthError{"Exchange", "no Config supplied"}
214	}
215
216	// If the transport or the cache already has a token, it is
217	// passed to `updateToken` to preserve existing refresh token.
218	tok := t.Token
219	if tok == nil && t.TokenCache != nil {
220		tok, _ = t.TokenCache.Token()
221	}
222	if tok == nil {
223		tok = new(Token)
224	}
225	err := t.updateToken(tok, url.Values{
226		"grant_type":   {"authorization_code"},
227		"redirect_uri": {t.RedirectURL},
228		"scope":        {t.Scope},
229		"code":         {code},
230	})
231	if err != nil {
232		return nil, err
233	}
234	t.Token = tok
235	if t.TokenCache != nil {
236		return tok, t.TokenCache.PutToken(tok)
237	}
238	return tok, nil
239}
240
241// RoundTrip executes a single HTTP transaction using the Transport's
242// Token as authorization headers.
243//
244// This method will attempt to renew the Token if it has expired and may return
245// an error related to that Token renewal before attempting the client request.
246// If the Token cannot be renewed a non-nil os.Error value will be returned.
247// If the Token is invalid callers should expect HTTP-level errors,
248// as indicated by the Response's StatusCode.
249func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
250	if t.Token == nil {
251		if t.Config == nil {
252			return nil, OAuthError{"RoundTrip", "no Config supplied"}
253		}
254		if t.TokenCache == nil {
255			return nil, OAuthError{"RoundTrip", "no Token supplied"}
256		}
257		var err error
258		t.Token, err = t.TokenCache.Token()
259		if err != nil {
260			return nil, err
261		}
262	}
263
264	// Refresh the Token if it has expired.
265	if t.Expired() {
266		if err := t.Refresh(); err != nil {
267			return nil, err
268		}
269	}
270
271	// To set the Authorization header, we must make a copy of the Request
272	// so that we don't modify the Request we were given.
273	// This is required by the specification of http.RoundTripper.
274	req = cloneRequest(req)
275	req.Header.Set("Authorization", "Bearer "+t.AccessToken)
276
277	// Make the HTTP request.
278	return t.transport().RoundTrip(req)
279}
280
281// cloneRequest returns a clone of the provided *http.Request.
282// The clone is a shallow copy of the struct and its Header map.
283func cloneRequest(r *http.Request) *http.Request {
284	// shallow copy of the struct
285	r2 := new(http.Request)
286	*r2 = *r
287	// deep copy of the Header
288	r2.Header = make(http.Header)
289	for k, s := range r.Header {
290		r2.Header[k] = s
291	}
292	return r2
293}
294
295// Refresh renews the Transport's AccessToken using its RefreshToken.
296func (t *Transport) Refresh() error {
297	if t.Token == nil {
298		return OAuthError{"Refresh", "no existing Token"}
299	}
300	if t.RefreshToken == "" {
301		return OAuthError{"Refresh", "Token expired; no Refresh Token"}
302	}
303	if t.Config == nil {
304		return OAuthError{"Refresh", "no Config supplied"}
305	}
306
307	err := t.updateToken(t.Token, url.Values{
308		"grant_type":    {"refresh_token"},
309		"refresh_token": {t.RefreshToken},
310	})
311	if err != nil {
312		return err
313	}
314	if t.TokenCache != nil {
315		return t.TokenCache.PutToken(t.Token)
316	}
317	return nil
318}
319
320// AuthenticateClient gets an access Token using the client_credentials grant
321// type.
322func (t *Transport) AuthenticateClient() error {
323	if t.Config == nil {
324		return OAuthError{"Exchange", "no Config supplied"}
325	}
326	if t.Token == nil {
327		t.Token = &Token{}
328	}
329	return t.updateToken(t.Token, url.Values{"grant_type": {"client_credentials"}})
330}
331
332func (t *Transport) updateToken(tok *Token, v url.Values) error {
333	v.Set("client_id", t.ClientId)
334	v.Set("client_secret", t.ClientSecret)
335	client := &http.Client{Transport: t.transport()}
336	req, err := http.NewRequest("POST", t.TokenURL, strings.NewReader(v.Encode()))
337	if err != nil {
338		return err
339	}
340	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
341	req.SetBasicAuth(t.ClientId, t.ClientSecret)
342	r, err := client.Do(req)
343	if err != nil {
344		return err
345	}
346	defer r.Body.Close()
347	if r.StatusCode != 200 {
348		return OAuthError{"updateToken", r.Status}
349	}
350	var b struct {
351		Access    string        `json:"access_token"`
352		Refresh   string        `json:"refresh_token"`
353		ExpiresIn time.Duration `json:"expires_in"`
354		Id        string        `json:"id_token"`
355	}
356
357	content, _, _ := mime.ParseMediaType(r.Header.Get("Content-Type"))
358	switch content {
359	case "application/x-www-form-urlencoded", "text/plain":
360		body, err := ioutil.ReadAll(r.Body)
361		if err != nil {
362			return err
363		}
364		vals, err := url.ParseQuery(string(body))
365		if err != nil {
366			return err
367		}
368
369		b.Access = vals.Get("access_token")
370		b.Refresh = vals.Get("refresh_token")
371		b.ExpiresIn, _ = time.ParseDuration(vals.Get("expires_in") + "s")
372		b.Id = vals.Get("id_token")
373	default:
374		if err = json.NewDecoder(r.Body).Decode(&b); err != nil {
375			return err
376		}
377		// The JSON parser treats the unitless ExpiresIn like 'ns' instead of 's' as above,
378		// so compensate here.
379		b.ExpiresIn *= time.Second
380	}
381	tok.AccessToken = b.Access
382	// Don't overwrite `RefreshToken` with an empty value
383	if len(b.Refresh) > 0 {
384		tok.RefreshToken = b.Refresh
385	}
386	if b.ExpiresIn == 0 {
387		tok.Expiry = time.Time{}
388	} else {
389		tok.Expiry = time.Now().Add(b.ExpiresIn)
390	}
391	if b.Id != "" {
392		if tok.Extra == nil {
393			tok.Extra = make(map[string]string)
394		}
395		tok.Extra["id_token"] = b.Id
396	}
397	return nil
398}