├── google ├── testdata │ └── gcloud │ │ ├── properties │ │ └── credentials ├── appengine_hook.go ├── sdk_test.go ├── jwt.go ├── appengine.go ├── google_test.go ├── example_test.go ├── google.go ├── default.go └── sdk.go ├── AUTHORS ├── CONTRIBUTORS ├── .travis.yml ├── vk └── vk.go ├── github └── github.go ├── facebook └── facebook.go ├── linkedin └── linkedin.go ├── client_appengine.go ├── odnoklassniki └── odnoklassniki.go ├── .github └── workflows │ └── semgrep.yml ├── internal ├── token_test.go ├── oauth2_test.go ├── transport.go ├── oauth2.go └── token.go ├── paypal └── paypal.go ├── jwt ├── example_test.go ├── jwt_test.go └── jwt.go ├── CONTRIBUTING.md ├── example_test.go ├── token_test.go ├── LICENSE ├── README.md ├── transport_test.go ├── clientcredentials ├── clientcredentials_test.go └── clientcredentials.go ├── transport.go ├── jws └── jws.go ├── token.go ├── oauth2.go └── oauth2_test.go /google/testdata/gcloud/properties: -------------------------------------------------------------------------------- 1 | [core] 2 | account = bar@example.com -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # This source code refers to The Go Authors for copyright purposes. 2 | # The master list of authors is in the main Go distribution, 3 | # visible at http://tip.golang.org/AUTHORS. 4 | -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | # This source code was written by the Go contributors. 2 | # The master list of contributors is in the main Go distribution, 3 | # visible at http://tip.golang.org/CONTRIBUTORS. 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.3 5 | - 1.4 6 | 7 | install: 8 | - export GOPATH="$HOME/gopath" 9 | - mkdir -p "$GOPATH/src/golang.org/x" 10 | - mv "$TRAVIS_BUILD_DIR" "$GOPATH/src/golang.org/x/oauth2" 11 | - go get -v -t -d golang.org/x/oauth2/... 12 | 13 | script: 14 | - go test -v golang.org/x/oauth2/... 15 | -------------------------------------------------------------------------------- /google/appengine_hook.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The oauth2 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 | // +build appengine 6 | 7 | package google 8 | 9 | import "google.golang.org/appengine" 10 | 11 | func init() { 12 | appengineTokenFunc = appengine.AccessToken 13 | } 14 | -------------------------------------------------------------------------------- /vk/vk.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The oauth2 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 | // Package vk provides constants for using OAuth2 to access VK.com. 6 | package vk // import "github.com/cloudflare/oauth2/vk" 7 | 8 | import ( 9 | "github.com/cloudflare/oauth2" 10 | ) 11 | 12 | // Endpoint is VK's OAuth 2.0 endpoint. 13 | var Endpoint = oauth2.Endpoint{ 14 | AuthURL: "https://oauth.vk.com/authorize", 15 | TokenURL: "https://oauth.vk.com/access_token", 16 | } 17 | -------------------------------------------------------------------------------- /github/github.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The oauth2 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 | // Package github provides constants for using OAuth2 to access Github. 6 | package github // import "github.com/cloudflare/oauth2/github" 7 | 8 | import ( 9 | "github.com/cloudflare/oauth2" 10 | ) 11 | 12 | // Endpoint is Github's OAuth 2.0 endpoint. 13 | var Endpoint = oauth2.Endpoint{ 14 | AuthURL: "https://github.com/login/oauth/authorize", 15 | TokenURL: "https://github.com/login/oauth/access_token", 16 | } 17 | -------------------------------------------------------------------------------- /facebook/facebook.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The oauth2 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 | // Package facebook provides constants for using OAuth2 to access Facebook. 6 | package facebook // import "github.com/cloudflare/oauth2/facebook" 7 | 8 | import ( 9 | "github.com/cloudflare/oauth2" 10 | ) 11 | 12 | // Endpoint is Facebook's OAuth 2.0 endpoint. 13 | var Endpoint = oauth2.Endpoint{ 14 | AuthURL: "https://www.facebook.com/dialog/oauth", 15 | TokenURL: "https://graph.facebook.com/oauth/access_token", 16 | } 17 | -------------------------------------------------------------------------------- /linkedin/linkedin.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The oauth2 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 | // Package linkedin provides constants for using OAuth2 to access LinkedIn. 6 | package linkedin // import "github.com/cloudflare/oauth2/linkedin" 7 | 8 | import ( 9 | "github.com/cloudflare/oauth2" 10 | ) 11 | 12 | // Endpoint is LinkedIn's OAuth 2.0 endpoint. 13 | var Endpoint = oauth2.Endpoint{ 14 | AuthURL: "https://www.linkedin.com/uas/oauth2/authorization", 15 | TokenURL: "https://www.linkedin.com/uas/oauth2/accessToken", 16 | } 17 | -------------------------------------------------------------------------------- /client_appengine.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The oauth2 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 | // +build appengine 6 | 7 | // App Engine hooks. 8 | 9 | package oauth2 10 | 11 | import ( 12 | "net/http" 13 | 14 | "golang.org/x/net/context" 15 | "github.com/cloudflare/oauth2/internal" 16 | "google.golang.org/appengine/urlfetch" 17 | ) 18 | 19 | func init() { 20 | internal.RegisterContextClientFunc(contextClientAppEngine) 21 | } 22 | 23 | func contextClientAppEngine(ctx context.Context) (*http.Client, error) { 24 | return urlfetch.Client(ctx), nil 25 | } 26 | -------------------------------------------------------------------------------- /odnoklassniki/odnoklassniki.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The oauth2 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 | // Package odnoklassniki provides constants for using OAuth2 to access Odnoklassniki. 6 | package odnoklassniki // import "github.com/cloudflare/oauth2/odnoklassniki" 7 | 8 | import ( 9 | "github.com/cloudflare/oauth2" 10 | ) 11 | 12 | // Endpoint is Odnoklassniki's OAuth 2.0 endpoint. 13 | var Endpoint = oauth2.Endpoint{ 14 | AuthURL: "https://www.odnoklassniki.ru/oauth/authorize", 15 | TokenURL: "https://api.odnoklassniki.ru/oauth/token.do", 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/semgrep.yml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: {} 3 | workflow_dispatch: {} 4 | push: 5 | branches: 6 | - main 7 | - master 8 | schedule: 9 | - cron: '0 0 * * *' 10 | name: Semgrep config 11 | jobs: 12 | semgrep: 13 | name: semgrep/ci 14 | runs-on: ubuntu-latest 15 | env: 16 | SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} 17 | SEMGREP_URL: https://cloudflare.semgrep.dev 18 | SEMGREP_APP_URL: https://cloudflare.semgrep.dev 19 | SEMGREP_VERSION_CHECK_URL: https://cloudflare.semgrep.dev/api/check-version 20 | container: 21 | image: semgrep/semgrep 22 | steps: 23 | - uses: actions/checkout@v4 24 | - run: semgrep ci 25 | -------------------------------------------------------------------------------- /internal/token_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The oauth2 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 | // Package internal contains support packages for oauth2 package. 6 | package internal 7 | 8 | import ( 9 | "fmt" 10 | "testing" 11 | ) 12 | 13 | func Test_providerAuthHeaderWorks(t *testing.T) { 14 | for _, p := range brokenAuthHeaderProviders { 15 | if providerAuthHeaderWorks(p) { 16 | t.Errorf("URL: %s not found in list", p) 17 | } 18 | p := fmt.Sprintf("%ssomesuffix", p) 19 | if providerAuthHeaderWorks(p) { 20 | t.Errorf("URL: %s not found in list", p) 21 | } 22 | } 23 | p := "https://api.not-in-the-list-example.com/" 24 | if !providerAuthHeaderWorks(p) { 25 | t.Errorf("URL: %s found in list", p) 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /paypal/paypal.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The oauth2 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 | // Package paypal provides constants for using OAuth2 to access PayPal. 6 | package paypal // import "github.com/cloudflare/oauth2/paypal" 7 | 8 | import ( 9 | "github.com/cloudflare/oauth2" 10 | ) 11 | 12 | // Endpoint is PayPal's OAuth 2.0 endpoint in live (production) environment. 13 | var Endpoint = oauth2.Endpoint{ 14 | AuthURL: "https://www.paypal.com/webapps/auth/protocol/openidconnect/v1/authorize", 15 | TokenURL: "https://api.paypal.com/v1/identity/openidconnect/tokenservice", 16 | } 17 | 18 | // SandboxEndpoint is PayPal's OAuth 2.0 endpoint in sandbox (testing) environment. 19 | var SandboxEndpoint = oauth2.Endpoint{ 20 | AuthURL: "https://www.sandbox.paypal.com/webapps/auth/protocol/openidconnect/v1/authorize", 21 | TokenURL: "https://api.sandbox.paypal.com/v1/identity/openidconnect/tokenservice", 22 | } 23 | -------------------------------------------------------------------------------- /jwt/example_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The oauth2 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 | package jwt_test 6 | 7 | import ( 8 | "github.com/cloudflare/oauth2" 9 | "github.com/cloudflare/oauth2/jwt" 10 | ) 11 | 12 | func ExampleJWTConfig() { 13 | conf := &jwt.Config{ 14 | Email: "xxx@developer.com", 15 | // The contents of your RSA private key or your PEM file 16 | // that contains a private key. 17 | // If you have a p12 file instead, you 18 | // can use `openssl` to export the private key into a pem file. 19 | // 20 | // $ openssl pkcs12 -in key.p12 -out key.pem -nodes 21 | // 22 | // It only supports PEM containers with no passphrase. 23 | PrivateKey: []byte("-----BEGIN RSA PRIVATE KEY-----..."), 24 | Subject: "user@example.com", 25 | TokenURL: "https://provider.com/o/oauth2/token", 26 | } 27 | // Initiate an http.Client, the following GET request will be 28 | // authorized and authenticated on the behalf of user@example.com. 29 | client := conf.Client(oauth2.NoContext) 30 | client.Get("...") 31 | } 32 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Go 2 | 3 | Go is an open source project. 4 | 5 | It is the work of hundreds of contributors. We appreciate your help! 6 | 7 | 8 | ## Filing issues 9 | 10 | When [filing an issue](https://github.com/golang/oauth2/issues), make sure to answer these five questions: 11 | 12 | 1. What version of Go are you using (`go version`)? 13 | 2. What operating system and processor architecture are you using? 14 | 3. What did you do? 15 | 4. What did you expect to see? 16 | 5. What did you see instead? 17 | 18 | General questions should go to the [golang-nuts mailing list](https://groups.google.com/group/golang-nuts) instead of the issue tracker. 19 | The gophers there will answer or ask you to file an issue if you've tripped over a bug. 20 | 21 | ## Contributing code 22 | 23 | Please read the [Contribution Guidelines](https://golang.org/doc/contribute.html) 24 | before sending patches. 25 | 26 | **We do not accept GitHub pull requests** 27 | (we use [Gerrit](https://code.google.com/p/gerrit/) instead for code review). 28 | 29 | Unless otherwise noted, the Go source files are distributed under 30 | the BSD-style license found in the LICENSE file. 31 | 32 | -------------------------------------------------------------------------------- /google/sdk_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The oauth2 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 | package google 6 | 7 | import "testing" 8 | 9 | func TestSDKConfig(t *testing.T) { 10 | sdkConfigPath = func() (string, error) { 11 | return "testdata/gcloud", nil 12 | } 13 | 14 | tests := []struct { 15 | account string 16 | accessToken string 17 | err bool 18 | }{ 19 | {"", "bar_access_token", false}, 20 | {"foo@example.com", "foo_access_token", false}, 21 | {"bar@example.com", "bar_access_token", false}, 22 | {"baz@serviceaccount.example.com", "", true}, 23 | } 24 | for _, tt := range tests { 25 | c, err := NewSDKConfig(tt.account) 26 | if got, want := err != nil, tt.err; got != want { 27 | if !tt.err { 28 | t.Errorf("expected no error, got error: %v", tt.err, err) 29 | } else { 30 | t.Errorf("expected error, got none") 31 | } 32 | continue 33 | } 34 | if err != nil { 35 | continue 36 | } 37 | tok := c.initialToken 38 | if tok == nil { 39 | t.Errorf("expected token %q, got: nil", tt.accessToken) 40 | continue 41 | } 42 | if tok.AccessToken != tt.accessToken { 43 | t.Errorf("expected token %q, got: %q", tt.accessToken, tok.AccessToken) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The oauth2 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 | package oauth2_test 6 | 7 | import ( 8 | "fmt" 9 | "log" 10 | 11 | "github.com/cloudflare/oauth2" 12 | ) 13 | 14 | func ExampleConfig() { 15 | conf := &oauth2.Config{ 16 | ClientID: "YOUR_CLIENT_ID", 17 | ClientSecret: "YOUR_CLIENT_SECRET", 18 | Scopes: []string{"SCOPE1", "SCOPE2"}, 19 | Endpoint: oauth2.Endpoint{ 20 | AuthURL: "https://provider.com/o/oauth2/auth", 21 | TokenURL: "https://provider.com/o/oauth2/token", 22 | }, 23 | } 24 | 25 | // Redirect user to consent page to ask for permission 26 | // for the scopes specified above. 27 | url := conf.AuthCodeURL("state", oauth2.AccessTypeOffline) 28 | fmt.Printf("Visit the URL for the auth dialog: %v", url) 29 | 30 | // Use the authorization code that is pushed to the redirect URL. 31 | // NewTransportWithCode will do the handshake to retrieve 32 | // an access token and initiate a Transport that is 33 | // authorized and authenticated by the retrieved token. 34 | var code string 35 | if _, err := fmt.Scan(&code); err != nil { 36 | log.Fatal(err) 37 | } 38 | tok, err := conf.Exchange(oauth2.NoContext, code) 39 | if err != nil { 40 | log.Fatal(err) 41 | } 42 | 43 | client := conf.Client(oauth2.NoContext, tok) 44 | client.Get("...") 45 | } 46 | -------------------------------------------------------------------------------- /token_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The oauth2 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 | package oauth2 6 | 7 | import ( 8 | "testing" 9 | "time" 10 | ) 11 | 12 | func TestTokenExtra(t *testing.T) { 13 | type testCase struct { 14 | key string 15 | val interface{} 16 | want interface{} 17 | } 18 | const key = "extra-key" 19 | cases := []testCase{ 20 | {key: key, val: "abc", want: "abc"}, 21 | {key: key, val: 123, want: 123}, 22 | {key: key, val: "", want: ""}, 23 | {key: "other-key", val: "def", want: nil}, 24 | } 25 | for _, tc := range cases { 26 | extra := make(map[string]interface{}) 27 | extra[tc.key] = tc.val 28 | tok := &Token{raw: extra} 29 | if got, want := tok.Extra(key), tc.want; got != want { 30 | t.Errorf("Extra(%q) = %q; want %q", key, got, want) 31 | } 32 | } 33 | } 34 | 35 | func TestTokenExpiry(t *testing.T) { 36 | now := time.Now() 37 | cases := []struct { 38 | name string 39 | tok *Token 40 | want bool 41 | }{ 42 | {name: "12 seconds", tok: &Token{Expiry: now.Add(12 * time.Second)}, want: false}, 43 | {name: "10 seconds", tok: &Token{Expiry: now.Add(expiryDelta)}, want: true}, 44 | } 45 | for _, tc := range cases { 46 | if got, want := tc.tok.expired(), tc.want; got != want { 47 | t.Errorf("expired (%q) = %v; want %v", tc.name, got, want) 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /internal/oauth2_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The oauth2 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 | // Package internal contains support packages for oauth2 package. 6 | package internal 7 | 8 | import ( 9 | "reflect" 10 | "strings" 11 | "testing" 12 | ) 13 | 14 | func TestParseINI(t *testing.T) { 15 | tests := []struct { 16 | ini string 17 | want map[string]map[string]string 18 | }{ 19 | { 20 | `root = toor 21 | [foo] 22 | bar = hop 23 | ini = nin 24 | `, 25 | map[string]map[string]string{ 26 | "": map[string]string{"root": "toor"}, 27 | "foo": map[string]string{"bar": "hop", "ini": "nin"}, 28 | }, 29 | }, 30 | { 31 | `[empty] 32 | [section] 33 | empty= 34 | `, 35 | map[string]map[string]string{ 36 | "": map[string]string{}, 37 | "empty": map[string]string{}, 38 | "section": map[string]string{"empty": ""}, 39 | }, 40 | }, 41 | { 42 | `ignore 43 | [invalid 44 | =stuff 45 | ;comment=true 46 | `, 47 | map[string]map[string]string{ 48 | "": map[string]string{}, 49 | }, 50 | }, 51 | } 52 | for _, tt := range tests { 53 | result, err := ParseINI(strings.NewReader(tt.ini)) 54 | if err != nil { 55 | t.Errorf("ParseINI(%q) error %v, want: no error", tt.ini, err) 56 | continue 57 | } 58 | if !reflect.DeepEqual(result, tt.want) { 59 | t.Errorf("ParseINI(%q) = %#v, want: %#v", tt.ini, result, tt.want) 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 The oauth2 Authors. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google Inc. nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /internal/transport.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The oauth2 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 | // Package internal contains support packages for oauth2 package. 6 | package internal 7 | 8 | import ( 9 | "net/http" 10 | 11 | "golang.org/x/net/context" 12 | ) 13 | 14 | // HTTPClient is the context key to use with golang.org/x/net/context's 15 | // WithValue function to associate an *http.Client value with a context. 16 | var HTTPClient ContextKey 17 | 18 | // ContextKey is just an empty struct. It exists so HTTPClient can be 19 | // an immutable public variable with a unique type. It's immutable 20 | // because nobody else can create a ContextKey, being unexported. 21 | type ContextKey struct{} 22 | 23 | // ContextClientFunc is a func which tries to return an *http.Client 24 | // given a Context value. If it returns an error, the search stops 25 | // with that error. If it returns (nil, nil), the search continues 26 | // down the list of registered funcs. 27 | type ContextClientFunc func(context.Context) (*http.Client, error) 28 | 29 | var contextClientFuncs []ContextClientFunc 30 | 31 | func RegisterContextClientFunc(fn ContextClientFunc) { 32 | contextClientFuncs = append(contextClientFuncs, fn) 33 | } 34 | 35 | func ContextClient(ctx context.Context) (*http.Client, error) { 36 | for _, fn := range contextClientFuncs { 37 | c, err := fn(ctx) 38 | if err != nil { 39 | return nil, err 40 | } 41 | if c != nil { 42 | return c, nil 43 | } 44 | } 45 | if hc, ok := ctx.Value(HTTPClient).(*http.Client); ok { 46 | return hc, nil 47 | } 48 | return http.DefaultClient, nil 49 | } 50 | 51 | func ContextTransport(ctx context.Context) http.RoundTripper { 52 | hc, err := ContextClient(ctx) 53 | // This is a rare error case (somebody using nil on App Engine). 54 | if err != nil { 55 | return ErrorTransport{err} 56 | } 57 | return hc.Transport 58 | } 59 | 60 | // ErrorTransport returns the specified error on RoundTrip. 61 | // This RoundTripper should be used in rare error cases where 62 | // error handling can be postponed to response handling time. 63 | type ErrorTransport struct{ Err error } 64 | 65 | func (t ErrorTransport) RoundTrip(*http.Request) (*http.Response, error) { 66 | return nil, t.Err 67 | } 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OAuth2 for Go 2 | 3 | [![Build Status](https://travis-ci.org/golang/oauth2.svg?branch=master)](https://travis-ci.org/golang/oauth2) 4 | 5 | oauth2 package contains a client implementation for OAuth 2.0 spec. 6 | 7 | ## Installation 8 | 9 | ~~~~ 10 | go get golang.org/x/oauth2 11 | ~~~~ 12 | 13 | See godoc for further documentation and examples. 14 | 15 | * [godoc.org/golang.org/x/oauth2](http://godoc.org/golang.org/x/oauth2) 16 | * [godoc.org/golang.org/x/oauth2/google](http://godoc.org/golang.org/x/oauth2/google) 17 | 18 | 19 | ## App Engine 20 | 21 | In change 96e89be (March 2015) we removed the `oauth2.Context2` type in favor 22 | of the [`context.Context`](https://golang.org/x/net/context#Context) type from 23 | the `golang.org/x/net/context` package 24 | 25 | This means its no longer possible to use the "Classic App Engine" 26 | `appengine.Context` type with the `oauth2` package. (You're using 27 | Classic App Engine if you import the package `"appengine"`.) 28 | 29 | To work around this, you may use the new `"google.golang.org/appengine"` 30 | package. This package has almost the same API as the `"appengine"` package, 31 | but it can be fetched with `go get` and used on "Managed VMs" and well as 32 | Classic App Engine. 33 | 34 | See the [new `appengine` package's readme](https://github.com/golang/appengine#updating-a-go-app-engine-app) 35 | for information on updating your app. 36 | 37 | If you don't want to update your entire app to use the new App Engine packages, 38 | you may use both sets of packages in parallel, using only the new packages 39 | with the `oauth2` package. 40 | 41 | import ( 42 | "golang.org/x/net/context" 43 | "golang.org/x/oauth2" 44 | "golang.org/x/oauth2/google" 45 | newappengine "google.golang.org/appengine" 46 | newurlfetch "google.golang.org/appengine/urlfetch" 47 | 48 | "appengine" 49 | ) 50 | 51 | func handler(w http.ResponseWriter, r *http.Request) { 52 | var c appengine.Context = appengine.NewContext(r) 53 | c.Infof("Logging a message with the old package") 54 | 55 | var ctx context.Context = newappengine.NewContext(r) 56 | client := &http.Client{ 57 | Transport: &oauth2.Transport{ 58 | Source: google.AppEngineTokenSource(ctx, "scope"), 59 | Base: &newurlfetch.Transport{Context: ctx}, 60 | }, 61 | } 62 | client.Get("...") 63 | } 64 | 65 | -------------------------------------------------------------------------------- /google/jwt.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The oauth2 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 | package google 6 | 7 | import ( 8 | "crypto/rsa" 9 | "fmt" 10 | "time" 11 | 12 | "github.com/cloudflare/oauth2" 13 | "github.com/cloudflare/oauth2/internal" 14 | "github.com/cloudflare/oauth2/jws" 15 | ) 16 | 17 | // JWTAccessTokenSourceFromJSON uses a Google Developers service account JSON 18 | // key file to read the credentials that authorize and authenticate the 19 | // requests, and returns a TokenSource that does not use any OAuth2 flow but 20 | // instead creates a JWT and sends that as the access token. 21 | // The audience is typically a URL that specifies the scope of the credentials. 22 | // 23 | // Note that this is not a standard OAuth flow, but rather an 24 | // optimization supported by a few Google services. 25 | // Unless you know otherwise, you should use JWTConfigFromJSON instead. 26 | func JWTAccessTokenSourceFromJSON(jsonKey []byte, audience string) (oauth2.TokenSource, error) { 27 | cfg, err := JWTConfigFromJSON(jsonKey) 28 | if err != nil { 29 | return nil, fmt.Errorf("google: could not parse JSON key: %v", err) 30 | } 31 | pk, err := internal.ParseKey(cfg.PrivateKey) 32 | if err != nil { 33 | return nil, fmt.Errorf("google: could not parse key: %v", err) 34 | } 35 | ts := &jwtAccessTokenSource{ 36 | email: cfg.Email, 37 | audience: audience, 38 | pk: pk, 39 | } 40 | tok, err := ts.Token() 41 | if err != nil { 42 | return nil, err 43 | } 44 | return oauth2.ReuseTokenSource(tok, ts), nil 45 | } 46 | 47 | type jwtAccessTokenSource struct { 48 | email, audience string 49 | pk *rsa.PrivateKey 50 | } 51 | 52 | func (ts *jwtAccessTokenSource) Token() (*oauth2.Token, error) { 53 | iat := time.Now() 54 | exp := iat.Add(time.Hour) 55 | cs := &jws.ClaimSet{ 56 | Iss: ts.email, 57 | Sub: ts.email, 58 | Aud: ts.audience, 59 | Iat: iat.Unix(), 60 | Exp: exp.Unix(), 61 | } 62 | hdr := &jws.Header{ 63 | Algorithm: "RS256", 64 | Typ: "JWT", 65 | } 66 | msg, err := jws.Encode(hdr, cs, ts.pk) 67 | if err != nil { 68 | return nil, fmt.Errorf("google: could not encode JWT: %v", err) 69 | } 70 | return &oauth2.Token{AccessToken: msg, TokenType: "Bearer"}, nil 71 | } 72 | -------------------------------------------------------------------------------- /transport_test.go: -------------------------------------------------------------------------------- 1 | package oauth2 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | type tokenSource struct{ token *Token } 11 | 12 | func (t *tokenSource) Token() (*Token, error) { 13 | return t.token, nil 14 | } 15 | 16 | func TestTransportTokenSource(t *testing.T) { 17 | ts := &tokenSource{ 18 | token: &Token{ 19 | AccessToken: "abc", 20 | }, 21 | } 22 | tr := &Transport{ 23 | Source: ts, 24 | } 25 | server := newMockServer(func(w http.ResponseWriter, r *http.Request) { 26 | if r.Header.Get("Authorization") != "Bearer abc" { 27 | t.Errorf("Transport doesn't set the Authorization header from the fetched token") 28 | } 29 | }) 30 | defer server.Close() 31 | client := http.Client{Transport: tr} 32 | client.Get(server.URL) 33 | } 34 | 35 | // Test for case-sensitive token types, per https://github.com/golang/oauth2/issues/113 36 | func TestTransportTokenSourceTypes(t *testing.T) { 37 | const val = "abc" 38 | tests := []struct { 39 | key string 40 | val string 41 | want string 42 | }{ 43 | {key: "bearer", val: val, want: "Bearer abc"}, 44 | {key: "mac", val: val, want: "MAC abc"}, 45 | {key: "basic", val: val, want: "Basic abc"}, 46 | } 47 | for _, tc := range tests { 48 | ts := &tokenSource{ 49 | token: &Token{ 50 | AccessToken: tc.val, 51 | TokenType: tc.key, 52 | }, 53 | } 54 | tr := &Transport{ 55 | Source: ts, 56 | } 57 | server := newMockServer(func(w http.ResponseWriter, r *http.Request) { 58 | if got, want := r.Header.Get("Authorization"), tc.want; got != want { 59 | t.Errorf("Authorization header (%q) = %q; want %q", val, got, want) 60 | } 61 | }) 62 | defer server.Close() 63 | client := http.Client{Transport: tr} 64 | client.Get(server.URL) 65 | } 66 | } 67 | 68 | func TestTokenValidNoAccessToken(t *testing.T) { 69 | token := &Token{} 70 | if token.Valid() { 71 | t.Errorf("Token should not be valid with no access token") 72 | } 73 | } 74 | 75 | func TestExpiredWithExpiry(t *testing.T) { 76 | token := &Token{ 77 | Expiry: time.Now().Add(-5 * time.Hour), 78 | } 79 | if token.Valid() { 80 | t.Errorf("Token should not be valid if it expired in the past") 81 | } 82 | } 83 | 84 | func newMockServer(handler func(w http.ResponseWriter, r *http.Request)) *httptest.Server { 85 | return httptest.NewServer(http.HandlerFunc(handler)) 86 | } 87 | -------------------------------------------------------------------------------- /internal/oauth2.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The oauth2 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 | // Package internal contains support packages for oauth2 package. 6 | package internal 7 | 8 | import ( 9 | "bufio" 10 | "crypto/rsa" 11 | "crypto/x509" 12 | "encoding/pem" 13 | "errors" 14 | "fmt" 15 | "io" 16 | "strings" 17 | ) 18 | 19 | // ParseKey converts the binary contents of a private key file 20 | // to an *rsa.PrivateKey. It detects whether the private key is in a 21 | // PEM container or not. If so, it extracts the the private key 22 | // from PEM container before conversion. It only supports PEM 23 | // containers with no passphrase. 24 | func ParseKey(key []byte) (*rsa.PrivateKey, error) { 25 | block, _ := pem.Decode(key) 26 | if block != nil { 27 | key = block.Bytes 28 | } 29 | parsedKey, err := x509.ParsePKCS8PrivateKey(key) 30 | if err != nil { 31 | parsedKey, err = x509.ParsePKCS1PrivateKey(key) 32 | if err != nil { 33 | return nil, fmt.Errorf("private key should be a PEM or plain PKSC1 or PKCS8; parse error: %v", err) 34 | } 35 | } 36 | parsed, ok := parsedKey.(*rsa.PrivateKey) 37 | if !ok { 38 | return nil, errors.New("private key is invalid") 39 | } 40 | return parsed, nil 41 | } 42 | 43 | func ParseINI(ini io.Reader) (map[string]map[string]string, error) { 44 | result := map[string]map[string]string{ 45 | "": map[string]string{}, // root section 46 | } 47 | scanner := bufio.NewScanner(ini) 48 | currentSection := "" 49 | for scanner.Scan() { 50 | line := strings.TrimSpace(scanner.Text()) 51 | if strings.HasPrefix(line, ";") { 52 | // comment. 53 | continue 54 | } 55 | if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") { 56 | currentSection = strings.TrimSpace(line[1 : len(line)-1]) 57 | result[currentSection] = map[string]string{} 58 | continue 59 | } 60 | parts := strings.SplitN(line, "=", 2) 61 | if len(parts) == 2 && parts[0] != "" { 62 | result[currentSection][strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1]) 63 | } 64 | } 65 | if err := scanner.Err(); err != nil { 66 | return nil, fmt.Errorf("error scanning ini: %v", err) 67 | } 68 | return result, nil 69 | } 70 | 71 | func CondVal(v string) []string { 72 | if v == "" { 73 | return nil 74 | } 75 | return []string{v} 76 | } 77 | -------------------------------------------------------------------------------- /google/appengine.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The oauth2 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 | package google 6 | 7 | import ( 8 | "sort" 9 | "strings" 10 | "sync" 11 | "time" 12 | 13 | "golang.org/x/net/context" 14 | "github.com/cloudflare/oauth2" 15 | ) 16 | 17 | // Set at init time by appengine_hook.go. If nil, we're not on App Engine. 18 | var appengineTokenFunc func(c context.Context, scopes ...string) (token string, expiry time.Time, err error) 19 | 20 | // AppEngineTokenSource returns a token source that fetches tokens 21 | // issued to the current App Engine application's service account. 22 | // If you are implementing a 3-legged OAuth 2.0 flow on App Engine 23 | // that involves user accounts, see oauth2.Config instead. 24 | // 25 | // The provided context must have come from appengine.NewContext. 26 | func AppEngineTokenSource(ctx context.Context, scope ...string) oauth2.TokenSource { 27 | if appengineTokenFunc == nil { 28 | panic("google: AppEngineTokenSource can only be used on App Engine.") 29 | } 30 | scopes := append([]string{}, scope...) 31 | sort.Strings(scopes) 32 | return &appEngineTokenSource{ 33 | ctx: ctx, 34 | scopes: scopes, 35 | key: strings.Join(scopes, " "), 36 | } 37 | } 38 | 39 | // aeTokens helps the fetched tokens to be reused until their expiration. 40 | var ( 41 | aeTokensMu sync.Mutex 42 | aeTokens = make(map[string]*tokenLock) // key is space-separated scopes 43 | ) 44 | 45 | type tokenLock struct { 46 | mu sync.Mutex // guards t; held while fetching or updating t 47 | t *oauth2.Token 48 | } 49 | 50 | type appEngineTokenSource struct { 51 | ctx context.Context 52 | scopes []string 53 | key string // to aeTokens map; space-separated scopes 54 | } 55 | 56 | func (ts *appEngineTokenSource) Token() (*oauth2.Token, error) { 57 | if appengineTokenFunc == nil { 58 | panic("google: AppEngineTokenSource can only be used on App Engine.") 59 | } 60 | 61 | aeTokensMu.Lock() 62 | tok, ok := aeTokens[ts.key] 63 | if !ok { 64 | tok = &tokenLock{} 65 | aeTokens[ts.key] = tok 66 | } 67 | aeTokensMu.Unlock() 68 | 69 | tok.mu.Lock() 70 | defer tok.mu.Unlock() 71 | if tok.t.Valid() { 72 | return tok.t, nil 73 | } 74 | access, exp, err := appengineTokenFunc(ts.ctx, ts.scopes...) 75 | if err != nil { 76 | return nil, err 77 | } 78 | tok.t = &oauth2.Token{ 79 | AccessToken: access, 80 | Expiry: exp, 81 | } 82 | return tok.t, nil 83 | } 84 | -------------------------------------------------------------------------------- /google/google_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The oauth2 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 | package google 6 | 7 | import ( 8 | "strings" 9 | "testing" 10 | ) 11 | 12 | var webJSONKey = []byte(` 13 | { 14 | "web": { 15 | "auth_uri": "https://google.com/o/oauth2/auth", 16 | "client_secret": "3Oknc4jS_wA2r9i", 17 | "token_uri": "https://google.com/o/oauth2/token", 18 | "client_email": "222-nprqovg5k43uum874cs9osjt2koe97g8@developer.gserviceaccount.com", 19 | "redirect_uris": ["https://www.example.com/oauth2callback"], 20 | "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/222-nprqovg5k43uum874cs9osjt2koe97g8@developer.gserviceaccount.com", 21 | "client_id": "222-nprqovg5k43uum874cs9osjt2koe97g8.apps.googleusercontent.com", 22 | "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", 23 | "javascript_origins": ["https://www.example.com"] 24 | } 25 | }`) 26 | 27 | var installedJSONKey = []byte(`{ 28 | "installed": { 29 | "client_id": "222-installed.apps.googleusercontent.com", 30 | "redirect_uris": ["https://www.example.com/oauth2callback"] 31 | } 32 | }`) 33 | 34 | func TestConfigFromJSON(t *testing.T) { 35 | conf, err := ConfigFromJSON(webJSONKey, "scope1", "scope2") 36 | if err != nil { 37 | t.Error(err) 38 | } 39 | if got, want := conf.ClientID, "222-nprqovg5k43uum874cs9osjt2koe97g8.apps.googleusercontent.com"; got != want { 40 | t.Errorf("ClientID = %q; want %q", got, want) 41 | } 42 | if got, want := conf.ClientSecret, "3Oknc4jS_wA2r9i"; got != want { 43 | t.Errorf("ClientSecret = %q; want %q", got, want) 44 | } 45 | if got, want := conf.RedirectURL, "https://www.example.com/oauth2callback"; got != want { 46 | t.Errorf("RedictURL = %q; want %q", got, want) 47 | } 48 | if got, want := strings.Join(conf.Scopes, ","), "scope1,scope2"; got != want { 49 | t.Errorf("Scopes = %q; want %q", got, want) 50 | } 51 | if got, want := conf.Endpoint.AuthURL, "https://google.com/o/oauth2/auth"; got != want { 52 | t.Errorf("AuthURL = %q; want %q", got, want) 53 | } 54 | if got, want := conf.Endpoint.TokenURL, "https://google.com/o/oauth2/token"; got != want { 55 | t.Errorf("TokenURL = %q; want %q", got, want) 56 | } 57 | } 58 | 59 | func TestConfigFromJSON_Installed(t *testing.T) { 60 | conf, err := ConfigFromJSON(installedJSONKey) 61 | if err != nil { 62 | t.Error(err) 63 | } 64 | if got, want := conf.ClientID, "222-installed.apps.googleusercontent.com"; got != want { 65 | t.Errorf("ClientID = %q; want %q", got, want) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /clientcredentials/clientcredentials_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The oauth2 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 | package clientcredentials 6 | 7 | import ( 8 | "io/ioutil" 9 | "net/http" 10 | "net/http/httptest" 11 | "testing" 12 | 13 | "github.com/cloudflare/oauth2" 14 | ) 15 | 16 | func newConf(url string) *Config { 17 | return &Config{ 18 | ClientID: "CLIENT_ID", 19 | ClientSecret: "CLIENT_SECRET", 20 | Scopes: []string{"scope1", "scope2"}, 21 | TokenURL: url + "/token", 22 | } 23 | } 24 | 25 | type mockTransport struct { 26 | rt func(req *http.Request) (resp *http.Response, err error) 27 | } 28 | 29 | func (t *mockTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) { 30 | return t.rt(req) 31 | } 32 | 33 | func TestTokenRequest(t *testing.T) { 34 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 35 | if r.URL.String() != "/token" { 36 | t.Errorf("authenticate client request URL = %q; want %q", r.URL, "/token") 37 | } 38 | headerAuth := r.Header.Get("Authorization") 39 | if headerAuth != "Basic Q0xJRU5UX0lEOkNMSUVOVF9TRUNSRVQ=" { 40 | t.Errorf("Unexpected authorization header, %v is found.", headerAuth) 41 | } 42 | if got, want := r.Header.Get("Content-Type"), "application/x-www-form-urlencoded"; got != want { 43 | t.Errorf("Content-Type header = %q; want %q", got, want) 44 | } 45 | body, err := ioutil.ReadAll(r.Body) 46 | if err != nil { 47 | r.Body.Close() 48 | } 49 | if err != nil { 50 | t.Errorf("failed reading request body: %s.", err) 51 | } 52 | if string(body) != "client_id=CLIENT_ID&grant_type=client_credentials&scope=scope1+scope2" { 53 | t.Errorf("payload = %q; want %q", string(body), "client_id=CLIENT_ID&grant_type=client_credentials&scope=scope1+scope2") 54 | } 55 | w.Header().Set("Content-Type", "application/x-www-form-urlencoded") 56 | w.Write([]byte("access_token=90d64460d14870c08c81352a05dedd3465940a7c&token_type=bearer")) 57 | })) 58 | defer ts.Close() 59 | conf := newConf(ts.URL) 60 | tok, err := conf.Token(oauth2.NoContext) 61 | if err != nil { 62 | t.Error(err) 63 | } 64 | if !tok.Valid() { 65 | t.Fatalf("token invalid. got: %#v", tok) 66 | } 67 | if tok.AccessToken != "90d64460d14870c08c81352a05dedd3465940a7c" { 68 | t.Errorf("Access token = %q; want %q", tok.AccessToken, "90d64460d14870c08c81352a05dedd3465940a7c") 69 | } 70 | if tok.TokenType != "bearer" { 71 | t.Errorf("token type = %q; want %q", tok.TokenType, "bearer") 72 | } 73 | } 74 | 75 | func TestTokenRefreshRequest(t *testing.T) { 76 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 77 | if r.URL.String() == "/somethingelse" { 78 | return 79 | } 80 | if r.URL.String() != "/token" { 81 | t.Errorf("Unexpected token refresh request URL, %v is found.", r.URL) 82 | } 83 | headerContentType := r.Header.Get("Content-Type") 84 | if headerContentType != "application/x-www-form-urlencoded" { 85 | t.Errorf("Unexpected Content-Type header, %v is found.", headerContentType) 86 | } 87 | body, _ := ioutil.ReadAll(r.Body) 88 | if string(body) != "client_id=CLIENT_ID&grant_type=client_credentials&scope=scope1+scope2" { 89 | t.Errorf("Unexpected refresh token payload, %v is found.", string(body)) 90 | } 91 | })) 92 | defer ts.Close() 93 | conf := newConf(ts.URL) 94 | c := conf.Client(oauth2.NoContext) 95 | c.Get(ts.URL + "/somethingelse") 96 | } 97 | -------------------------------------------------------------------------------- /transport.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The oauth2 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 | package oauth2 6 | 7 | import ( 8 | "errors" 9 | "io" 10 | "net/http" 11 | "sync" 12 | ) 13 | 14 | // Transport is an http.RoundTripper that makes OAuth 2.0 HTTP requests, 15 | // wrapping a base RoundTripper and adding an Authorization header 16 | // with a token from the supplied Sources. 17 | // 18 | // Transport is a low-level mechanism. Most code will use the 19 | // higher-level Config.Client method instead. 20 | type Transport struct { 21 | // Source supplies the token to add to outgoing requests' 22 | // Authorization headers. 23 | Source TokenSource 24 | 25 | // Base is the base RoundTripper used to make HTTP requests. 26 | // If nil, http.DefaultTransport is used. 27 | Base http.RoundTripper 28 | 29 | mu sync.Mutex // guards modReq 30 | modReq map[*http.Request]*http.Request // original -> modified 31 | } 32 | 33 | // RoundTrip authorizes and authenticates the request with an 34 | // access token. If no token exists or token is expired, 35 | // tries to refresh/fetch a new token. 36 | func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) { 37 | if t.Source == nil { 38 | return nil, errors.New("oauth2: Transport's Source is nil") 39 | } 40 | token, err := t.Source.Token() 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | req2 := cloneRequest(req) // per RoundTripper contract 46 | token.SetAuthHeader(req2) 47 | t.setModReq(req, req2) 48 | res, err := t.base().RoundTrip(req2) 49 | if err != nil { 50 | t.setModReq(req, nil) 51 | return nil, err 52 | } 53 | res.Body = &onEOFReader{ 54 | rc: res.Body, 55 | fn: func() { t.setModReq(req, nil) }, 56 | } 57 | return res, nil 58 | } 59 | 60 | // CancelRequest cancels an in-flight request by closing its connection. 61 | func (t *Transport) CancelRequest(req *http.Request) { 62 | type canceler interface { 63 | CancelRequest(*http.Request) 64 | } 65 | if cr, ok := t.base().(canceler); ok { 66 | t.mu.Lock() 67 | modReq := t.modReq[req] 68 | delete(t.modReq, req) 69 | t.mu.Unlock() 70 | cr.CancelRequest(modReq) 71 | } 72 | } 73 | 74 | func (t *Transport) base() http.RoundTripper { 75 | if t.Base != nil { 76 | return t.Base 77 | } 78 | return http.DefaultTransport 79 | } 80 | 81 | func (t *Transport) setModReq(orig, mod *http.Request) { 82 | t.mu.Lock() 83 | defer t.mu.Unlock() 84 | if t.modReq == nil { 85 | t.modReq = make(map[*http.Request]*http.Request) 86 | } 87 | if mod == nil { 88 | delete(t.modReq, orig) 89 | } else { 90 | t.modReq[orig] = mod 91 | } 92 | } 93 | 94 | // cloneRequest returns a clone of the provided *http.Request. 95 | // The clone is a shallow copy of the struct and its Header map. 96 | func cloneRequest(r *http.Request) *http.Request { 97 | // shallow copy of the struct 98 | r2 := new(http.Request) 99 | *r2 = *r 100 | // deep copy of the Header 101 | r2.Header = make(http.Header, len(r.Header)) 102 | for k, s := range r.Header { 103 | r2.Header[k] = append([]string(nil), s...) 104 | } 105 | return r2 106 | } 107 | 108 | type onEOFReader struct { 109 | rc io.ReadCloser 110 | fn func() 111 | } 112 | 113 | func (r *onEOFReader) Read(p []byte) (n int, err error) { 114 | n, err = r.rc.Read(p) 115 | if err == io.EOF { 116 | r.runFunc() 117 | } 118 | return 119 | } 120 | 121 | func (r *onEOFReader) Close() error { 122 | err := r.rc.Close() 123 | r.runFunc() 124 | return err 125 | } 126 | 127 | func (r *onEOFReader) runFunc() { 128 | if fn := r.fn; fn != nil { 129 | fn() 130 | r.fn = nil 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /clientcredentials/clientcredentials.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The oauth2 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 | // Package clientcredentials implements the OAuth2.0 "client credentials" token flow, 6 | // also known as the "two-legged OAuth 2.0". 7 | // 8 | // This should be used when the client is acting on its own behalf or when the client 9 | // is the resource owner. It may also be used when requesting access to protected 10 | // resources based on an authorization previously arranged with the authorization 11 | // server. 12 | // 13 | // See http://tools.ietf.org/html/draft-ietf-oauth-v2-31#section-4.4 14 | package clientcredentials // import "github.com/cloudflare/oauth2/clientcredentials" 15 | 16 | import ( 17 | "net/http" 18 | "net/url" 19 | "strings" 20 | 21 | "golang.org/x/net/context" 22 | "github.com/cloudflare/oauth2" 23 | "github.com/cloudflare/oauth2/internal" 24 | ) 25 | 26 | // tokenFromInternal maps an *internal.Token struct into 27 | // an *oauth2.Token struct. 28 | func tokenFromInternal(t *internal.Token) *oauth2.Token { 29 | if t == nil { 30 | return nil 31 | } 32 | tk := &oauth2.Token{ 33 | AccessToken: t.AccessToken, 34 | TokenType: t.TokenType, 35 | RefreshToken: t.RefreshToken, 36 | Expiry: t.Expiry, 37 | } 38 | return tk.WithExtra(t.Raw) 39 | } 40 | 41 | // retrieveToken takes a *Config and uses that to retrieve an *internal.Token. 42 | // This token is then mapped from *internal.Token into an *oauth2.Token which is 43 | // returned along with an error. 44 | func retrieveToken(ctx context.Context, c *Config, v url.Values) (*oauth2.Token, error) { 45 | tk, err := internal.RetrieveToken(ctx, c.ClientID, c.ClientSecret, c.TokenURL, v) 46 | if err != nil { 47 | return nil, err 48 | } 49 | return tokenFromInternal(tk), nil 50 | } 51 | 52 | // Client Credentials Config describes a 2-legged OAuth2 flow, with both the 53 | // client application information and the server's endpoint URLs. 54 | type Config struct { 55 | // ClientID is the application's ID. 56 | ClientID string 57 | 58 | // ClientSecret is the application's secret. 59 | ClientSecret string 60 | 61 | // TokenURL is the resource server's token endpoint 62 | // URL. This is a constant specific to each server. 63 | TokenURL string 64 | 65 | // Scope specifies optional requested permissions. 66 | Scopes []string 67 | } 68 | 69 | // Token uses client credentials to retreive a token. 70 | // The HTTP client to use is derived from the context. 71 | // If nil, http.DefaultClient is used. 72 | func (c *Config) Token(ctx context.Context) (*oauth2.Token, error) { 73 | return retrieveToken(ctx, c, url.Values{ 74 | "grant_type": {"client_credentials"}, 75 | "scope": internal.CondVal(strings.Join(c.Scopes, " ")), 76 | }) 77 | } 78 | 79 | // Client returns an HTTP client using the provided token. 80 | // The token will auto-refresh as necessary. The underlying 81 | // HTTP transport will be obtained using the provided context. 82 | // The returned client and its Transport should not be modified. 83 | func (c *Config) Client(ctx context.Context) *http.Client { 84 | return oauth2.NewClient(ctx, c.TokenSource(ctx)) 85 | } 86 | 87 | // TokenSource returns a TokenSource that returns t until t expires, 88 | // automatically refreshing it as necessary using the provided context and the 89 | // client ID and client secret. 90 | // 91 | // Most users will use Config.Client instead. 92 | func (c *Config) TokenSource(ctx context.Context) oauth2.TokenSource { 93 | source := &tokenSource{ 94 | ctx: ctx, 95 | conf: c, 96 | } 97 | return oauth2.ReuseTokenSource(nil, source) 98 | } 99 | 100 | type tokenSource struct { 101 | ctx context.Context 102 | conf *Config 103 | } 104 | 105 | // Token refreshes the token by using a new client credentials request. 106 | // tokens received this way do not include a refresh token 107 | func (c *tokenSource) Token() (*oauth2.Token, error) { 108 | return retrieveToken(c.ctx, c.conf, url.Values{ 109 | "grant_type": {"client_credentials"}, 110 | "scope": internal.CondVal(strings.Join(c.conf.Scopes, " ")), 111 | }) 112 | } 113 | -------------------------------------------------------------------------------- /jwt/jwt_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The oauth2 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 | package jwt 6 | 7 | import ( 8 | "net/http" 9 | "net/http/httptest" 10 | "testing" 11 | 12 | "github.com/cloudflare/oauth2" 13 | ) 14 | 15 | var dummyPrivateKey = []byte(`-----BEGIN RSA PRIVATE KEY----- 16 | MIIEpAIBAAKCAQEAx4fm7dngEmOULNmAs1IGZ9Apfzh+BkaQ1dzkmbUgpcoghucE 17 | DZRnAGd2aPyB6skGMXUytWQvNYav0WTR00wFtX1ohWTfv68HGXJ8QXCpyoSKSSFY 18 | fuP9X36wBSkSX9J5DVgiuzD5VBdzUISSmapjKm+DcbRALjz6OUIPEWi1Tjl6p5RK 19 | 1w41qdbmt7E5/kGhKLDuT7+M83g4VWhgIvaAXtnhklDAggilPPa8ZJ1IFe31lNlr 20 | k4DRk38nc6sEutdf3RL7QoH7FBusI7uXV03DC6dwN1kP4GE7bjJhcRb/7jYt7CQ9 21 | /E9Exz3c0yAp0yrTg0Fwh+qxfH9dKwN52S7SBwIDAQABAoIBAQCaCs26K07WY5Jt 22 | 3a2Cw3y2gPrIgTCqX6hJs7O5ByEhXZ8nBwsWANBUe4vrGaajQHdLj5OKfsIDrOvn 23 | 2NI1MqflqeAbu/kR32q3tq8/Rl+PPiwUsW3E6Pcf1orGMSNCXxeducF2iySySzh3 24 | nSIhCG5uwJDWI7a4+9KiieFgK1pt/Iv30q1SQS8IEntTfXYwANQrfKUVMmVF9aIK 25 | 6/WZE2yd5+q3wVVIJ6jsmTzoDCX6QQkkJICIYwCkglmVy5AeTckOVwcXL0jqw5Kf 26 | 5/soZJQwLEyBoQq7Kbpa26QHq+CJONetPP8Ssy8MJJXBT+u/bSseMb3Zsr5cr43e 27 | DJOhwsThAoGBAPY6rPKl2NT/K7XfRCGm1sbWjUQyDShscwuWJ5+kD0yudnT/ZEJ1 28 | M3+KS/iOOAoHDdEDi9crRvMl0UfNa8MAcDKHflzxg2jg/QI+fTBjPP5GOX0lkZ9g 29 | z6VePoVoQw2gpPFVNPPTxKfk27tEzbaffvOLGBEih0Kb7HTINkW8rIlzAoGBAM9y 30 | 1yr+jvfS1cGFtNU+Gotoihw2eMKtIqR03Yn3n0PK1nVCDKqwdUqCypz4+ml6cxRK 31 | J8+Pfdh7D+ZJd4LEG6Y4QRDLuv5OA700tUoSHxMSNn3q9As4+T3MUyYxWKvTeu3U 32 | f2NWP9ePU0lV8ttk7YlpVRaPQmc1qwooBA/z/8AdAoGAW9x0HWqmRICWTBnpjyxx 33 | QGlW9rQ9mHEtUotIaRSJ6K/F3cxSGUEkX1a3FRnp6kPLcckC6NlqdNgNBd6rb2rA 34 | cPl/uSkZP42Als+9YMoFPU/xrrDPbUhu72EDrj3Bllnyb168jKLa4VBOccUvggxr 35 | Dm08I1hgYgdN5huzs7y6GeUCgYEAj+AZJSOJ6o1aXS6rfV3mMRve9bQ9yt8jcKXw 36 | 5HhOCEmMtaSKfnOF1Ziih34Sxsb7O2428DiX0mV/YHtBnPsAJidL0SdLWIapBzeg 37 | KHArByIRkwE6IvJvwpGMdaex1PIGhx5i/3VZL9qiq/ElT05PhIb+UXgoWMabCp84 38 | OgxDK20CgYAeaFo8BdQ7FmVX2+EEejF+8xSge6WVLtkaon8bqcn6P0O8lLypoOhd 39 | mJAYH8WU+UAy9pecUnDZj14LAGNVmYcse8HFX71MoshnvCTFEPVo4rZxIAGwMpeJ 40 | 5jgQ3slYLpqrGlcbLgUXBUgzEO684Wk/UV9DFPlHALVqCfXQ9dpJPg== 41 | -----END RSA PRIVATE KEY-----`) 42 | 43 | func TestJWTFetch_JSONResponse(t *testing.T) { 44 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 45 | w.Header().Set("Content-Type", "application/json") 46 | w.Write([]byte(`{ 47 | "access_token": "90d64460d14870c08c81352a05dedd3465940a7c", 48 | "scope": "user", 49 | "token_type": "bearer", 50 | "expires_in": 3600 51 | }`)) 52 | })) 53 | defer ts.Close() 54 | 55 | conf := &Config{ 56 | Email: "aaa@xxx.com", 57 | PrivateKey: dummyPrivateKey, 58 | TokenURL: ts.URL, 59 | } 60 | tok, err := conf.TokenSource(oauth2.NoContext).Token() 61 | if err != nil { 62 | t.Fatal(err) 63 | } 64 | if !tok.Valid() { 65 | t.Errorf("Token invalid") 66 | } 67 | if tok.AccessToken != "90d64460d14870c08c81352a05dedd3465940a7c" { 68 | t.Errorf("Unexpected access token, %#v", tok.AccessToken) 69 | } 70 | if tok.TokenType != "bearer" { 71 | t.Errorf("Unexpected token type, %#v", tok.TokenType) 72 | } 73 | if tok.Expiry.IsZero() { 74 | t.Errorf("Unexpected token expiry, %#v", tok.Expiry) 75 | } 76 | scope := tok.Extra("scope") 77 | if scope != "user" { 78 | t.Errorf("Unexpected value for scope: %v", scope) 79 | } 80 | } 81 | 82 | func TestJWTFetch_BadResponse(t *testing.T) { 83 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 84 | w.Header().Set("Content-Type", "application/json") 85 | w.Write([]byte(`{"scope": "user", "token_type": "bearer"}`)) 86 | })) 87 | defer ts.Close() 88 | 89 | conf := &Config{ 90 | Email: "aaa@xxx.com", 91 | PrivateKey: dummyPrivateKey, 92 | TokenURL: ts.URL, 93 | } 94 | tok, err := conf.TokenSource(oauth2.NoContext).Token() 95 | if err != nil { 96 | t.Fatal(err) 97 | } 98 | if tok == nil { 99 | t.Fatalf("token is nil") 100 | } 101 | if tok.Valid() { 102 | t.Errorf("token is valid. want invalid.") 103 | } 104 | if tok.AccessToken != "" { 105 | t.Errorf("Unexpected non-empty access token %q.", tok.AccessToken) 106 | } 107 | if want := "bearer"; tok.TokenType != want { 108 | t.Errorf("TokenType = %q; want %q", tok.TokenType, want) 109 | } 110 | scope := tok.Extra("scope") 111 | if want := "user"; scope != want { 112 | t.Errorf("token scope = %q; want %q", scope, want) 113 | } 114 | } 115 | 116 | func TestJWTFetch_BadResponseType(t *testing.T) { 117 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 118 | w.Header().Set("Content-Type", "application/json") 119 | w.Write([]byte(`{"access_token":123, "scope": "user", "token_type": "bearer"}`)) 120 | })) 121 | defer ts.Close() 122 | conf := &Config{ 123 | Email: "aaa@xxx.com", 124 | PrivateKey: dummyPrivateKey, 125 | TokenURL: ts.URL, 126 | } 127 | tok, err := conf.TokenSource(oauth2.NoContext).Token() 128 | if err == nil { 129 | t.Error("got a token; expected error") 130 | if tok.AccessToken != "" { 131 | t.Errorf("Unexpected access token, %#v.", tok.AccessToken) 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /jwt/jwt.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The oauth2 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 | // Package jwt implements the OAuth 2.0 JSON Web Token flow, commonly 6 | // known as "two-legged OAuth 2.0". 7 | // 8 | // See: https://tools.ietf.org/html/draft-ietf-oauth-jwt-bearer-12 9 | package jwt 10 | 11 | import ( 12 | "encoding/json" 13 | "fmt" 14 | "io" 15 | "io/ioutil" 16 | "net/http" 17 | "net/url" 18 | "strings" 19 | "time" 20 | 21 | "golang.org/x/net/context" 22 | "github.com/cloudflare/oauth2" 23 | "github.com/cloudflare/oauth2/internal" 24 | "github.com/cloudflare/oauth2/jws" 25 | ) 26 | 27 | var ( 28 | defaultGrantType = "urn:ietf:params:oauth:grant-type:jwt-bearer" 29 | defaultHeader = &jws.Header{Algorithm: "RS256", Typ: "JWT"} 30 | ) 31 | 32 | // Config is the configuration for using JWT to fetch tokens, 33 | // commonly known as "two-legged OAuth 2.0". 34 | type Config struct { 35 | // Email is the OAuth client identifier used when communicating with 36 | // the configured OAuth provider. 37 | Email string 38 | 39 | // PrivateKey contains the contents of an RSA private key or the 40 | // contents of a PEM file that contains a private key. The provided 41 | // private key is used to sign JWT payloads. 42 | // PEM containers with a passphrase are not supported. 43 | // Use the following command to convert a PKCS 12 file into a PEM. 44 | // 45 | // $ openssl pkcs12 -in key.p12 -out key.pem -nodes 46 | // 47 | PrivateKey []byte 48 | 49 | // Subject is the optional user to impersonate. 50 | Subject string 51 | 52 | // Scopes optionally specifies a list of requested permission scopes. 53 | Scopes []string 54 | 55 | // TokenURL is the endpoint required to complete the 2-legged JWT flow. 56 | TokenURL string 57 | } 58 | 59 | // TokenSource returns a JWT TokenSource using the configuration 60 | // in c and the HTTP client from the provided context. 61 | func (c *Config) TokenSource(ctx context.Context) oauth2.TokenSource { 62 | return oauth2.ReuseTokenSource(nil, jwtSource{ctx, c}) 63 | } 64 | 65 | // Client returns an HTTP client wrapping the context's 66 | // HTTP transport and adding Authorization headers with tokens 67 | // obtained from c. 68 | // 69 | // The returned client and its Transport should not be modified. 70 | func (c *Config) Client(ctx context.Context) *http.Client { 71 | return oauth2.NewClient(ctx, c.TokenSource(ctx)) 72 | } 73 | 74 | // jwtSource is a source that always does a signed JWT request for a token. 75 | // It should typically be wrapped with a reuseTokenSource. 76 | type jwtSource struct { 77 | ctx context.Context 78 | conf *Config 79 | } 80 | 81 | func (js jwtSource) Token() (*oauth2.Token, error) { 82 | pk, err := internal.ParseKey(js.conf.PrivateKey) 83 | if err != nil { 84 | return nil, err 85 | } 86 | hc := oauth2.NewClient(js.ctx, nil) 87 | claimSet := &jws.ClaimSet{ 88 | Iss: js.conf.Email, 89 | Scope: strings.Join(js.conf.Scopes, " "), 90 | Aud: js.conf.TokenURL, 91 | } 92 | if subject := js.conf.Subject; subject != "" { 93 | claimSet.Sub = subject 94 | // prn is the old name of sub. Keep setting it 95 | // to be compatible with legacy OAuth 2.0 providers. 96 | claimSet.Prn = subject 97 | } 98 | payload, err := jws.Encode(defaultHeader, claimSet, pk) 99 | if err != nil { 100 | return nil, err 101 | } 102 | v := url.Values{} 103 | v.Set("grant_type", defaultGrantType) 104 | v.Set("assertion", payload) 105 | resp, err := hc.PostForm(js.conf.TokenURL, v) 106 | if err != nil { 107 | return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err) 108 | } 109 | defer resp.Body.Close() 110 | body, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1<<20)) 111 | if err != nil { 112 | return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err) 113 | } 114 | if c := resp.StatusCode; c < 200 || c > 299 { 115 | return nil, fmt.Errorf("oauth2: cannot fetch token: %v\nResponse: %s", resp.Status, body) 116 | } 117 | // tokenRes is the JSON response body. 118 | var tokenRes struct { 119 | AccessToken string `json:"access_token"` 120 | TokenType string `json:"token_type"` 121 | IDToken string `json:"id_token"` 122 | ExpiresIn int64 `json:"expires_in"` // relative seconds from now 123 | } 124 | if err := json.Unmarshal(body, &tokenRes); err != nil { 125 | return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err) 126 | } 127 | token := &oauth2.Token{ 128 | AccessToken: tokenRes.AccessToken, 129 | TokenType: tokenRes.TokenType, 130 | } 131 | raw := make(map[string]interface{}) 132 | json.Unmarshal(body, &raw) // no error checks for optional fields 133 | token = token.WithExtra(raw) 134 | 135 | if secs := tokenRes.ExpiresIn; secs > 0 { 136 | token.Expiry = time.Now().Add(time.Duration(secs) * time.Second) 137 | } 138 | if v := tokenRes.IDToken; v != "" { 139 | // decode returned id token to get expiry 140 | claimSet, err := jws.Decode(v) 141 | if err != nil { 142 | return nil, fmt.Errorf("oauth2: error decoding JWT token: %v", err) 143 | } 144 | token.Expiry = time.Unix(claimSet.Exp, 0) 145 | } 146 | return token, nil 147 | } 148 | -------------------------------------------------------------------------------- /google/example_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The oauth2 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 | // +build appenginevm !appengine 6 | 7 | package google_test 8 | 9 | import ( 10 | "fmt" 11 | "io/ioutil" 12 | "log" 13 | "net/http" 14 | 15 | "github.com/cloudflare/oauth2" 16 | "github.com/cloudflare/oauth2/google" 17 | "github.com/cloudflare/oauth2/jwt" 18 | "google.golang.org/appengine" 19 | "google.golang.org/appengine/urlfetch" 20 | ) 21 | 22 | func ExampleDefaultClient() { 23 | client, err := google.DefaultClient(oauth2.NoContext, 24 | "https://www.googleapis.com/auth/devstorage.full_control") 25 | if err != nil { 26 | log.Fatal(err) 27 | } 28 | client.Get("...") 29 | } 30 | 31 | func Example_webServer() { 32 | // Your credentials should be obtained from the Google 33 | // Developer Console (https://console.developers.google.com). 34 | conf := &oauth2.Config{ 35 | ClientID: "YOUR_CLIENT_ID", 36 | ClientSecret: "YOUR_CLIENT_SECRET", 37 | RedirectURL: "YOUR_REDIRECT_URL", 38 | Scopes: []string{ 39 | "https://www.googleapis.com/auth/bigquery", 40 | "https://www.googleapis.com/auth/blogger", 41 | }, 42 | Endpoint: google.Endpoint, 43 | } 44 | // Redirect user to Google's consent page to ask for permission 45 | // for the scopes specified above. 46 | url := conf.AuthCodeURL("state") 47 | fmt.Printf("Visit the URL for the auth dialog: %v", url) 48 | 49 | // Handle the exchange code to initiate a transport. 50 | tok, err := conf.Exchange(oauth2.NoContext, "authorization-code") 51 | if err != nil { 52 | log.Fatal(err) 53 | } 54 | client := conf.Client(oauth2.NoContext, tok) 55 | client.Get("...") 56 | } 57 | 58 | func ExampleJWTConfigFromJSON() { 59 | // Your credentials should be obtained from the Google 60 | // Developer Console (https://console.developers.google.com). 61 | // Navigate to your project, then see the "Credentials" page 62 | // under "APIs & Auth". 63 | // To create a service account client, click "Create new Client ID", 64 | // select "Service Account", and click "Create Client ID". A JSON 65 | // key file will then be downloaded to your computer. 66 | data, err := ioutil.ReadFile("/path/to/your-project-key.json") 67 | if err != nil { 68 | log.Fatal(err) 69 | } 70 | conf, err := google.JWTConfigFromJSON(data, "https://www.googleapis.com/auth/bigquery") 71 | if err != nil { 72 | log.Fatal(err) 73 | } 74 | // Initiate an http.Client. The following GET request will be 75 | // authorized and authenticated on the behalf of 76 | // your service account. 77 | client := conf.Client(oauth2.NoContext) 78 | client.Get("...") 79 | } 80 | 81 | func ExampleSDKConfig() { 82 | // The credentials will be obtained from the first account that 83 | // has been authorized with `gcloud auth login`. 84 | conf, err := google.NewSDKConfig("") 85 | if err != nil { 86 | log.Fatal(err) 87 | } 88 | // Initiate an http.Client. The following GET request will be 89 | // authorized and authenticated on the behalf of the SDK user. 90 | client := conf.Client(oauth2.NoContext) 91 | client.Get("...") 92 | } 93 | 94 | func Example_serviceAccount() { 95 | // Your credentials should be obtained from the Google 96 | // Developer Console (https://console.developers.google.com). 97 | conf := &jwt.Config{ 98 | Email: "xxx@developer.gserviceaccount.com", 99 | // The contents of your RSA private key or your PEM file 100 | // that contains a private key. 101 | // If you have a p12 file instead, you 102 | // can use `openssl` to export the private key into a pem file. 103 | // 104 | // $ openssl pkcs12 -in key.p12 -passin pass:notasecret -out key.pem -nodes 105 | // 106 | // The field only supports PEM containers with no passphrase. 107 | // The openssl command will convert p12 keys to passphrase-less PEM containers. 108 | PrivateKey: []byte("-----BEGIN RSA PRIVATE KEY-----..."), 109 | Scopes: []string{ 110 | "https://www.googleapis.com/auth/bigquery", 111 | "https://www.googleapis.com/auth/blogger", 112 | }, 113 | TokenURL: google.JWTTokenURL, 114 | // If you would like to impersonate a user, you can 115 | // create a transport with a subject. The following GET 116 | // request will be made on the behalf of user@example.com. 117 | // Optional. 118 | Subject: "user@example.com", 119 | } 120 | // Initiate an http.Client, the following GET request will be 121 | // authorized and authenticated on the behalf of user@example.com. 122 | client := conf.Client(oauth2.NoContext) 123 | client.Get("...") 124 | } 125 | 126 | func ExampleAppEngineTokenSource() { 127 | var req *http.Request // from the ServeHTTP handler 128 | ctx := appengine.NewContext(req) 129 | client := &http.Client{ 130 | Transport: &oauth2.Transport{ 131 | Source: google.AppEngineTokenSource(ctx, "https://www.googleapis.com/auth/bigquery"), 132 | Base: &urlfetch.Transport{ 133 | Context: ctx, 134 | }, 135 | }, 136 | } 137 | client.Get("...") 138 | } 139 | 140 | func ExampleComputeTokenSource() { 141 | client := &http.Client{ 142 | Transport: &oauth2.Transport{ 143 | // Fetch from Google Compute Engine's metadata server to retrieve 144 | // an access token for the provided account. 145 | // If no account is specified, "default" is used. 146 | Source: google.ComputeTokenSource(""), 147 | }, 148 | } 149 | client.Get("...") 150 | } 151 | -------------------------------------------------------------------------------- /jws/jws.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The oauth2 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 | // Package jws provides encoding and decoding utilities for 6 | // signed JWS messages. 7 | package jws // import "github.com/cloudflare/oauth2/jws" 8 | 9 | import ( 10 | "bytes" 11 | "crypto" 12 | "crypto/rand" 13 | "crypto/rsa" 14 | "crypto/sha256" 15 | "encoding/base64" 16 | "encoding/json" 17 | "errors" 18 | "fmt" 19 | "strings" 20 | "time" 21 | ) 22 | 23 | // ClaimSet contains information about the JWT signature including the 24 | // permissions being requested (scopes), the target of the token, the issuer, 25 | // the time the token was issued, and the lifetime of the token. 26 | type ClaimSet struct { 27 | Iss string `json:"iss"` // email address of the client_id of the application making the access token request 28 | Scope string `json:"scope,omitempty"` // space-delimited list of the permissions the application requests 29 | Aud string `json:"aud"` // descriptor of the intended target of the assertion (Optional). 30 | Exp int64 `json:"exp"` // the expiration time of the assertion (seconds since Unix epoch) 31 | Iat int64 `json:"iat"` // the time the assertion was issued (seconds since Unix epoch) 32 | Typ string `json:"typ,omitempty"` // token type (Optional). 33 | 34 | // Email for which the application is requesting delegated access (Optional). 35 | Sub string `json:"sub,omitempty"` 36 | 37 | // The old name of Sub. Client keeps setting Prn to be 38 | // complaint with legacy OAuth 2.0 providers. (Optional) 39 | Prn string `json:"prn,omitempty"` 40 | 41 | // See http://tools.ietf.org/html/draft-jones-json-web-token-10#section-4.3 42 | // This array is marshalled using custom code (see (c *ClaimSet) encode()). 43 | PrivateClaims map[string]interface{} `json:"-"` 44 | 45 | exp time.Time 46 | iat time.Time 47 | } 48 | 49 | func (c *ClaimSet) encode() (string, error) { 50 | if c.exp.IsZero() || c.iat.IsZero() { 51 | // Reverting time back for machines whose time is not perfectly in sync. 52 | // If client machine's time is in the future according 53 | // to Google servers, an access token will not be issued. 54 | now := time.Now().Add(-10 * time.Second) 55 | c.iat = now 56 | c.exp = now.Add(time.Hour) 57 | } 58 | 59 | c.Exp = c.exp.Unix() 60 | c.Iat = c.iat.Unix() 61 | 62 | b, err := json.Marshal(c) 63 | if err != nil { 64 | return "", err 65 | } 66 | 67 | if len(c.PrivateClaims) == 0 { 68 | return base64Encode(b), nil 69 | } 70 | 71 | // Marshal private claim set and then append it to b. 72 | prv, err := json.Marshal(c.PrivateClaims) 73 | if err != nil { 74 | return "", fmt.Errorf("jws: invalid map of private claims %v", c.PrivateClaims) 75 | } 76 | 77 | // Concatenate public and private claim JSON objects. 78 | if !bytes.HasSuffix(b, []byte{'}'}) { 79 | return "", fmt.Errorf("jws: invalid JSON %s", b) 80 | } 81 | if !bytes.HasPrefix(prv, []byte{'{'}) { 82 | return "", fmt.Errorf("jws: invalid JSON %s", prv) 83 | } 84 | b[len(b)-1] = ',' // Replace closing curly brace with a comma. 85 | b = append(b, prv[1:]...) // Append private claims. 86 | return base64Encode(b), nil 87 | } 88 | 89 | // Header represents the header for the signed JWS payloads. 90 | type Header struct { 91 | // The algorithm used for signature. 92 | Algorithm string `json:"alg"` 93 | 94 | // Represents the token type. 95 | Typ string `json:"typ"` 96 | } 97 | 98 | func (h *Header) encode() (string, error) { 99 | b, err := json.Marshal(h) 100 | if err != nil { 101 | return "", err 102 | } 103 | return base64Encode(b), nil 104 | } 105 | 106 | // Decode decodes a claim set from a JWS payload. 107 | func Decode(payload string) (*ClaimSet, error) { 108 | // decode returned id token to get expiry 109 | s := strings.Split(payload, ".") 110 | if len(s) < 2 { 111 | // TODO(jbd): Provide more context about the error. 112 | return nil, errors.New("jws: invalid token received") 113 | } 114 | decoded, err := base64Decode(s[1]) 115 | if err != nil { 116 | return nil, err 117 | } 118 | c := &ClaimSet{} 119 | err = json.NewDecoder(bytes.NewBuffer(decoded)).Decode(c) 120 | return c, err 121 | } 122 | 123 | // Encode encodes a signed JWS with provided header and claim set. 124 | func Encode(header *Header, c *ClaimSet, signature *rsa.PrivateKey) (string, error) { 125 | head, err := header.encode() 126 | if err != nil { 127 | return "", err 128 | } 129 | cs, err := c.encode() 130 | if err != nil { 131 | return "", err 132 | } 133 | ss := fmt.Sprintf("%s.%s", head, cs) 134 | h := sha256.New() 135 | h.Write([]byte(ss)) 136 | b, err := rsa.SignPKCS1v15(rand.Reader, signature, crypto.SHA256, h.Sum(nil)) 137 | if err != nil { 138 | return "", err 139 | } 140 | sig := base64Encode(b) 141 | return fmt.Sprintf("%s.%s", ss, sig), nil 142 | } 143 | 144 | // base64Encode returns and Base64url encoded version of the input string with any 145 | // trailing "=" stripped. 146 | func base64Encode(b []byte) string { 147 | return strings.TrimRight(base64.URLEncoding.EncodeToString(b), "=") 148 | } 149 | 150 | // base64Decode decodes the Base64url encoded string 151 | func base64Decode(s string) ([]byte, error) { 152 | // add back missing padding 153 | switch len(s) % 4 { 154 | case 2: 155 | s += "==" 156 | case 3: 157 | s += "=" 158 | } 159 | return base64.URLEncoding.DecodeString(s) 160 | } 161 | -------------------------------------------------------------------------------- /google/google.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The oauth2 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 | // Package google provides support for making OAuth2 authorized and 6 | // authenticated HTTP requests to Google APIs. 7 | // It supports the Web server flow, client-side credentials, service accounts, 8 | // Google Compute Engine service accounts, and Google App Engine service 9 | // accounts. 10 | // 11 | // For more information, please read 12 | // https://developers.google.com/accounts/docs/OAuth2 13 | // and 14 | // https://developers.google.com/accounts/docs/application-default-credentials. 15 | package google // import "github.com/cloudflare/oauth2/google" 16 | 17 | import ( 18 | "encoding/json" 19 | "errors" 20 | "fmt" 21 | "strings" 22 | "time" 23 | 24 | "github.com/cloudflare/oauth2" 25 | "github.com/cloudflare/oauth2/jwt" 26 | "google.golang.org/cloud/compute/metadata" 27 | ) 28 | 29 | // Endpoint is Google's OAuth 2.0 endpoint. 30 | var Endpoint = oauth2.Endpoint{ 31 | AuthURL: "https://accounts.google.com/o/oauth2/auth", 32 | TokenURL: "https://accounts.google.com/o/oauth2/token", 33 | } 34 | 35 | // JWTTokenURL is Google's OAuth 2.0 token URL to use with the JWT flow. 36 | const JWTTokenURL = "https://accounts.google.com/o/oauth2/token" 37 | 38 | // ConfigFromJSON uses a Google Developers Console client_credentials.json 39 | // file to construct a config. 40 | // client_credentials.json can be downloadable from https://console.developers.google.com, 41 | // under "APIs & Auth" > "Credentials". Download the Web application credentials in the 42 | // JSON format and provide the contents of the file as jsonKey. 43 | func ConfigFromJSON(jsonKey []byte, scope ...string) (*oauth2.Config, error) { 44 | type cred struct { 45 | ClientID string `json:"client_id"` 46 | ClientSecret string `json:"client_secret"` 47 | RedirectURIs []string `json:"redirect_uris"` 48 | AuthURI string `json:"auth_uri"` 49 | TokenURI string `json:"token_uri"` 50 | } 51 | var j struct { 52 | Web *cred `json:"web"` 53 | Installed *cred `json:"installed"` 54 | } 55 | if err := json.Unmarshal(jsonKey, &j); err != nil { 56 | return nil, err 57 | } 58 | var c *cred 59 | switch { 60 | case j.Web != nil: 61 | c = j.Web 62 | case j.Installed != nil: 63 | c = j.Installed 64 | default: 65 | return nil, fmt.Errorf("oauth2/google: no credentials found") 66 | } 67 | if len(c.RedirectURIs) < 1 { 68 | return nil, errors.New("oauth2/google: missing redirect URL in the client_credentials.json") 69 | } 70 | return &oauth2.Config{ 71 | ClientID: c.ClientID, 72 | ClientSecret: c.ClientSecret, 73 | RedirectURL: c.RedirectURIs[0], 74 | Scopes: scope, 75 | Endpoint: oauth2.Endpoint{ 76 | AuthURL: c.AuthURI, 77 | TokenURL: c.TokenURI, 78 | }, 79 | }, nil 80 | } 81 | 82 | // JWTConfigFromJSON uses a Google Developers service account JSON key file to read 83 | // the credentials that authorize and authenticate the requests. 84 | // Create a service account on "Credentials" page under "APIs & Auth" for your 85 | // project at https://console.developers.google.com to download a JSON key file. 86 | func JWTConfigFromJSON(jsonKey []byte, scope ...string) (*jwt.Config, error) { 87 | var key struct { 88 | Email string `json:"client_email"` 89 | PrivateKey string `json:"private_key"` 90 | } 91 | if err := json.Unmarshal(jsonKey, &key); err != nil { 92 | return nil, err 93 | } 94 | return &jwt.Config{ 95 | Email: key.Email, 96 | PrivateKey: []byte(key.PrivateKey), 97 | Scopes: scope, 98 | TokenURL: JWTTokenURL, 99 | }, nil 100 | } 101 | 102 | // ComputeTokenSource returns a token source that fetches access tokens 103 | // from Google Compute Engine (GCE)'s metadata server. It's only valid to use 104 | // this token source if your program is running on a GCE instance. 105 | // If no account is specified, "default" is used. 106 | // Further information about retrieving access tokens from the GCE metadata 107 | // server can be found at https://cloud.google.com/compute/docs/authentication. 108 | func ComputeTokenSource(account string) oauth2.TokenSource { 109 | return oauth2.ReuseTokenSource(nil, computeSource{account: account}) 110 | } 111 | 112 | type computeSource struct { 113 | account string 114 | } 115 | 116 | func (cs computeSource) Token() (*oauth2.Token, error) { 117 | if !metadata.OnGCE() { 118 | return nil, errors.New("oauth2/google: can't get a token from the metadata service; not running on GCE") 119 | } 120 | acct := cs.account 121 | if acct == "" { 122 | acct = "default" 123 | } 124 | tokenJSON, err := metadata.Get("instance/service-accounts/" + acct + "/token") 125 | if err != nil { 126 | return nil, err 127 | } 128 | var res struct { 129 | AccessToken string `json:"access_token"` 130 | ExpiresInSec int `json:"expires_in"` 131 | TokenType string `json:"token_type"` 132 | } 133 | err = json.NewDecoder(strings.NewReader(tokenJSON)).Decode(&res) 134 | if err != nil { 135 | return nil, fmt.Errorf("oauth2/google: invalid token JSON from metadata: %v", err) 136 | } 137 | if res.ExpiresInSec == 0 || res.AccessToken == "" { 138 | return nil, fmt.Errorf("oauth2/google: incomplete token received from metadata") 139 | } 140 | return &oauth2.Token{ 141 | AccessToken: res.AccessToken, 142 | TokenType: res.TokenType, 143 | Expiry: time.Now().Add(time.Duration(res.ExpiresInSec) * time.Second), 144 | }, nil 145 | } 146 | -------------------------------------------------------------------------------- /google/default.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The oauth2 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 | package google 6 | 7 | import ( 8 | "encoding/json" 9 | "errors" 10 | "fmt" 11 | "io/ioutil" 12 | "net/http" 13 | "os" 14 | "path/filepath" 15 | "runtime" 16 | 17 | "golang.org/x/net/context" 18 | "github.com/cloudflare/oauth2" 19 | "github.com/cloudflare/oauth2/jwt" 20 | "google.golang.org/cloud/compute/metadata" 21 | ) 22 | 23 | // DefaultClient returns an HTTP Client that uses the 24 | // DefaultTokenSource to obtain authentication credentials. 25 | // 26 | // This client should be used when developing services 27 | // that run on Google App Engine or Google Compute Engine 28 | // and use "Application Default Credentials." 29 | // 30 | // For more details, see: 31 | // https://developers.google.com/accounts/docs/application-default-credentials 32 | // 33 | func DefaultClient(ctx context.Context, scope ...string) (*http.Client, error) { 34 | ts, err := DefaultTokenSource(ctx, scope...) 35 | if err != nil { 36 | return nil, err 37 | } 38 | return oauth2.NewClient(ctx, ts), nil 39 | } 40 | 41 | // DefaultTokenSource is a token source that uses 42 | // "Application Default Credentials". 43 | // 44 | // It looks for credentials in the following places, 45 | // preferring the first location found: 46 | // 47 | // 1. A JSON file whose path is specified by the 48 | // GOOGLE_APPLICATION_CREDENTIALS environment variable. 49 | // 2. A JSON file in a location known to the gcloud command-line tool. 50 | // On Windows, this is %APPDATA%/gcloud/application_default_credentials.json. 51 | // On other systems, $HOME/.config/gcloud/application_default_credentials.json. 52 | // 3. On Google App Engine it uses the appengine.AccessToken function. 53 | // 4. On Google Compute Engine, it fetches credentials from the metadata server. 54 | // (In this final case any provided scopes are ignored.) 55 | // 56 | // For more details, see: 57 | // https://developers.google.com/accounts/docs/application-default-credentials 58 | // 59 | func DefaultTokenSource(ctx context.Context, scope ...string) (oauth2.TokenSource, error) { 60 | // First, try the environment variable. 61 | const envVar = "GOOGLE_APPLICATION_CREDENTIALS" 62 | if filename := os.Getenv(envVar); filename != "" { 63 | ts, err := tokenSourceFromFile(ctx, filename, scope) 64 | if err != nil { 65 | return nil, fmt.Errorf("google: error getting credentials using %v environment variable: %v", envVar, err) 66 | } 67 | return ts, nil 68 | } 69 | 70 | // Second, try a well-known file. 71 | filename := wellKnownFile() 72 | _, err := os.Stat(filename) 73 | if err == nil { 74 | ts, err2 := tokenSourceFromFile(ctx, filename, scope) 75 | if err2 == nil { 76 | return ts, nil 77 | } 78 | err = err2 79 | } else if os.IsNotExist(err) { 80 | err = nil // ignore this error 81 | } 82 | if err != nil { 83 | return nil, fmt.Errorf("google: error getting credentials using well-known file (%v): %v", filename, err) 84 | } 85 | 86 | // Third, if we're on Google App Engine use those credentials. 87 | if appengineTokenFunc != nil { 88 | return AppEngineTokenSource(ctx, scope...), nil 89 | } 90 | 91 | // Fourth, if we're on Google Compute Engine use the metadata server. 92 | if metadata.OnGCE() { 93 | return ComputeTokenSource(""), nil 94 | } 95 | 96 | // None are found; return helpful error. 97 | const url = "https://developers.google.com/accounts/docs/application-default-credentials" 98 | return nil, fmt.Errorf("google: could not find default credentials. See %v for more information.", url) 99 | } 100 | 101 | func wellKnownFile() string { 102 | const f = "application_default_credentials.json" 103 | if runtime.GOOS == "windows" { 104 | return filepath.Join(os.Getenv("APPDATA"), "gcloud", f) 105 | } 106 | return filepath.Join(guessUnixHomeDir(), ".config", "gcloud", f) 107 | } 108 | 109 | func tokenSourceFromFile(ctx context.Context, filename string, scopes []string) (oauth2.TokenSource, error) { 110 | b, err := ioutil.ReadFile(filename) 111 | if err != nil { 112 | return nil, err 113 | } 114 | var d struct { 115 | // Common fields 116 | Type string 117 | ClientID string `json:"client_id"` 118 | 119 | // User Credential fields 120 | ClientSecret string `json:"client_secret"` 121 | RefreshToken string `json:"refresh_token"` 122 | 123 | // Service Account fields 124 | ClientEmail string `json:"client_email"` 125 | PrivateKeyID string `json:"private_key_id"` 126 | PrivateKey string `json:"private_key"` 127 | } 128 | if err := json.Unmarshal(b, &d); err != nil { 129 | return nil, err 130 | } 131 | switch d.Type { 132 | case "authorized_user": 133 | cfg := &oauth2.Config{ 134 | ClientID: d.ClientID, 135 | ClientSecret: d.ClientSecret, 136 | Scopes: append([]string{}, scopes...), // copy 137 | Endpoint: Endpoint, 138 | } 139 | tok := &oauth2.Token{RefreshToken: d.RefreshToken} 140 | return cfg.TokenSource(ctx, tok), nil 141 | case "service_account": 142 | cfg := &jwt.Config{ 143 | Email: d.ClientEmail, 144 | PrivateKey: []byte(d.PrivateKey), 145 | Scopes: append([]string{}, scopes...), // copy 146 | TokenURL: JWTTokenURL, 147 | } 148 | return cfg.TokenSource(ctx), nil 149 | case "": 150 | return nil, errors.New("missing 'type' field in credentials") 151 | default: 152 | return nil, fmt.Errorf("unknown credential type: %q", d.Type) 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /token.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The oauth2 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 | package oauth2 6 | 7 | import ( 8 | "net/http" 9 | "net/url" 10 | "strings" 11 | "time" 12 | 13 | "github.com/cloudflare/oauth2/internal" 14 | "golang.org/x/net/context" 15 | ) 16 | 17 | // expiryDelta determines how earlier a token should be considered 18 | // expired than its actual expiration time. It is used to avoid late 19 | // expirations due to client-server time mismatches. 20 | const expiryDelta = 10 * time.Second 21 | 22 | // These are the fields which we normally expect to be in a token, 23 | // so we don't include them with requests for the Extra fields. 24 | var standardTokenFields = []string{"access_token", "token_type", "refresh_token", "expires_in", "expires"} 25 | 26 | // Token represents the crendentials used to authorize 27 | // the requests to access protected resources on the OAuth 2.0 28 | // provider's backend. 29 | // 30 | // Most users of this package should not access fields of Token 31 | // directly. They're exported mostly for use by related packages 32 | // implementing derivative OAuth2 flows. 33 | type Token struct { 34 | // AccessToken is the token that authorizes and authenticates 35 | // the requests. 36 | AccessToken string `json:"access_token"` 37 | 38 | // TokenType is the type of token. 39 | // The Type method returns either this or "Bearer", the default. 40 | TokenType string `json:"token_type,omitempty"` 41 | 42 | // RefreshToken is a token that's used by the application 43 | // (as opposed to the user) to refresh the access token 44 | // if it expires. 45 | RefreshToken string `json:"refresh_token,omitempty"` 46 | 47 | // Expiry is the optional expiration time of the access token. 48 | // 49 | // If zero, TokenSource implementations will reuse the same 50 | // token forever and RefreshToken or equivalent 51 | // mechanisms for that TokenSource will not be used. 52 | Expiry time.Time `json:"expiry,omitempty"` 53 | 54 | // raw optionally contains extra metadata from the server 55 | // when updating a token. 56 | raw interface{} 57 | } 58 | 59 | // Type returns t.TokenType if non-empty, else "Bearer". 60 | func (t *Token) Type() string { 61 | if strings.EqualFold(t.TokenType, "bearer") { 62 | return "Bearer" 63 | } 64 | if strings.EqualFold(t.TokenType, "mac") { 65 | return "MAC" 66 | } 67 | if strings.EqualFold(t.TokenType, "basic") { 68 | return "Basic" 69 | } 70 | if t.TokenType != "" { 71 | return t.TokenType 72 | } 73 | return "Bearer" 74 | } 75 | 76 | // SetAuthHeader sets the Authorization header to r using the access 77 | // token in t. 78 | // 79 | // This method is unnecessary when using Transport or an HTTP Client 80 | // returned by this package. 81 | func (t *Token) SetAuthHeader(r *http.Request) { 82 | r.Header.Set("Authorization", t.Type()+" "+t.AccessToken) 83 | } 84 | 85 | // WithExtra returns a new Token that's a clone of t, but using the 86 | // provided raw extra map. This is only intended for use by packages 87 | // implementing derivative OAuth2 flows. 88 | func (t *Token) WithExtra(extra interface{}) *Token { 89 | t2 := new(Token) 90 | *t2 = *t 91 | t2.raw = extra 92 | return t2 93 | } 94 | 95 | // Extra returns an extra field. 96 | // Extra fields are key-value pairs returned by the server as a 97 | // part of the token retrieval response. 98 | func (t *Token) Extra(key string) interface{} { 99 | if vals, ok := t.raw.(url.Values); ok { 100 | // TODO(jbd): Cast numeric values to int64 or float64. 101 | return vals.Get(key) 102 | } 103 | if raw, ok := t.raw.(map[string]interface{}); ok { 104 | return raw[key] 105 | } 106 | return nil 107 | } 108 | 109 | // Return all extra fields which were returned with the access token. 110 | // This will not include the fields which are already extracted into other fields in 111 | // the token. 112 | func (t *Token) ExtraAsMap() map[string]interface{} { 113 | // The extra fields in a token ('Raw') can be either a map or URL values depending on the 114 | // encoding of the token. 115 | 116 | extra := make(map[string]interface{}) 117 | 118 | asValues, ok := t.raw.(url.Values) 119 | if ok { 120 | for key, values := range asValues { 121 | extra[key] = values[0] 122 | } 123 | } else { 124 | asMap, ok := t.raw.(map[string]interface{}) 125 | if ok { 126 | extra = asMap 127 | } 128 | } 129 | 130 | for key := range extra { 131 | for _, field := range standardTokenFields { 132 | if field == key { 133 | delete(extra, key) 134 | break 135 | } 136 | } 137 | } 138 | 139 | return extra 140 | } 141 | 142 | // expired reports whether the token is expired. 143 | // t must be non-nil. 144 | func (t *Token) expired() bool { 145 | if t.Expiry.IsZero() { 146 | return false 147 | } 148 | return t.Expiry.Add(-expiryDelta).Before(time.Now()) 149 | } 150 | 151 | // Valid reports whether t is non-nil, has an AccessToken, and is not expired. 152 | func (t *Token) Valid() bool { 153 | return t != nil && t.AccessToken != "" && !t.expired() 154 | } 155 | 156 | // tokenFromInternal maps an *internal.Token struct into 157 | // a *Token struct. 158 | func tokenFromInternal(t *internal.Token) *Token { 159 | if t == nil { 160 | return nil 161 | } 162 | return &Token{ 163 | AccessToken: t.AccessToken, 164 | TokenType: t.TokenType, 165 | RefreshToken: t.RefreshToken, 166 | Expiry: t.Expiry, 167 | raw: t.Raw, 168 | } 169 | } 170 | 171 | // retrieveToken takes a *Config and uses that to retrieve an *internal.Token. 172 | // This token is then mapped from *internal.Token into an *oauth2.Token which is returned along 173 | // with an error.. 174 | func retrieveToken(ctx context.Context, c *Config, v url.Values) (*Token, error) { 175 | tk, err := internal.RetrieveToken(ctx, c.ClientID, c.ClientSecret, c.Endpoint.TokenURL, v) 176 | if err != nil { 177 | return nil, err 178 | } 179 | return tokenFromInternal(tk), nil 180 | } 181 | -------------------------------------------------------------------------------- /google/sdk.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The oauth2 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 | package google 6 | 7 | import ( 8 | "encoding/json" 9 | "errors" 10 | "fmt" 11 | "net/http" 12 | "os" 13 | "os/user" 14 | "path/filepath" 15 | "runtime" 16 | "strings" 17 | "time" 18 | 19 | "golang.org/x/net/context" 20 | "github.com/cloudflare/oauth2" 21 | "github.com/cloudflare/oauth2/internal" 22 | ) 23 | 24 | type sdkCredentials struct { 25 | Data []struct { 26 | Credential struct { 27 | ClientID string `json:"client_id"` 28 | ClientSecret string `json:"client_secret"` 29 | AccessToken string `json:"access_token"` 30 | RefreshToken string `json:"refresh_token"` 31 | TokenExpiry *time.Time `json:"token_expiry"` 32 | } `json:"credential"` 33 | Key struct { 34 | Account string `json:"account"` 35 | Scope string `json:"scope"` 36 | } `json:"key"` 37 | } 38 | } 39 | 40 | // An SDKConfig provides access to tokens from an account already 41 | // authorized via the Google Cloud SDK. 42 | type SDKConfig struct { 43 | conf oauth2.Config 44 | initialToken *oauth2.Token 45 | } 46 | 47 | // NewSDKConfig creates an SDKConfig for the given Google Cloud SDK 48 | // account. If account is empty, the account currently active in 49 | // Google Cloud SDK properties is used. 50 | // Google Cloud SDK credentials must be created by running `gcloud auth` 51 | // before using this function. 52 | // The Google Cloud SDK is available at https://cloud.google.com/sdk/. 53 | func NewSDKConfig(account string) (*SDKConfig, error) { 54 | configPath, err := sdkConfigPath() 55 | if err != nil { 56 | return nil, fmt.Errorf("oauth2/google: error getting SDK config path: %v", err) 57 | } 58 | credentialsPath := filepath.Join(configPath, "credentials") 59 | f, err := os.Open(credentialsPath) 60 | if err != nil { 61 | return nil, fmt.Errorf("oauth2/google: failed to load SDK credentials: %v", err) 62 | } 63 | defer f.Close() 64 | 65 | var c sdkCredentials 66 | if err := json.NewDecoder(f).Decode(&c); err != nil { 67 | return nil, fmt.Errorf("oauth2/google: failed to decode SDK credentials from %q: %v", credentialsPath, err) 68 | } 69 | if len(c.Data) == 0 { 70 | return nil, fmt.Errorf("oauth2/google: no credentials found in %q, run `gcloud auth login` to create one", credentialsPath) 71 | } 72 | if account == "" { 73 | propertiesPath := filepath.Join(configPath, "properties") 74 | f, err := os.Open(propertiesPath) 75 | if err != nil { 76 | return nil, fmt.Errorf("oauth2/google: failed to load SDK properties: %v", err) 77 | } 78 | defer f.Close() 79 | ini, err := internal.ParseINI(f) 80 | if err != nil { 81 | return nil, fmt.Errorf("oauth2/google: failed to parse SDK properties %q: %v", propertiesPath, err) 82 | } 83 | core, ok := ini["core"] 84 | if !ok { 85 | return nil, fmt.Errorf("oauth2/google: failed to find [core] section in %v", ini) 86 | } 87 | active, ok := core["account"] 88 | if !ok { 89 | return nil, fmt.Errorf("oauth2/google: failed to find %q attribute in %v", "account", core) 90 | } 91 | account = active 92 | } 93 | 94 | for _, d := range c.Data { 95 | if account == "" || d.Key.Account == account { 96 | if d.Credential.AccessToken == "" && d.Credential.RefreshToken == "" { 97 | return nil, fmt.Errorf("oauth2/google: no token available for account %q", account) 98 | } 99 | var expiry time.Time 100 | if d.Credential.TokenExpiry != nil { 101 | expiry = *d.Credential.TokenExpiry 102 | } 103 | return &SDKConfig{ 104 | conf: oauth2.Config{ 105 | ClientID: d.Credential.ClientID, 106 | ClientSecret: d.Credential.ClientSecret, 107 | Scopes: strings.Split(d.Key.Scope, " "), 108 | Endpoint: Endpoint, 109 | RedirectURL: "oob", 110 | }, 111 | initialToken: &oauth2.Token{ 112 | AccessToken: d.Credential.AccessToken, 113 | RefreshToken: d.Credential.RefreshToken, 114 | Expiry: expiry, 115 | }, 116 | }, nil 117 | } 118 | } 119 | return nil, fmt.Errorf("oauth2/google: no such credentials for account %q", account) 120 | } 121 | 122 | // Client returns an HTTP client using Google Cloud SDK credentials to 123 | // authorize requests. The token will auto-refresh as necessary. The 124 | // underlying http.RoundTripper will be obtained using the provided 125 | // context. The returned client and its Transport should not be 126 | // modified. 127 | func (c *SDKConfig) Client(ctx context.Context) *http.Client { 128 | return &http.Client{ 129 | Transport: &oauth2.Transport{ 130 | Source: c.TokenSource(ctx), 131 | }, 132 | } 133 | } 134 | 135 | // TokenSource returns an oauth2.TokenSource that retrieve tokens from 136 | // Google Cloud SDK credentials using the provided context. 137 | // It will returns the current access token stored in the credentials, 138 | // and refresh it when it expires, but it won't update the credentials 139 | // with the new access token. 140 | func (c *SDKConfig) TokenSource(ctx context.Context) oauth2.TokenSource { 141 | return c.conf.TokenSource(ctx, c.initialToken) 142 | } 143 | 144 | // Scopes are the OAuth 2.0 scopes the current account is authorized for. 145 | func (c *SDKConfig) Scopes() []string { 146 | return c.conf.Scopes 147 | } 148 | 149 | // sdkConfigPath tries to guess where the gcloud config is located. 150 | // It can be overridden during tests. 151 | var sdkConfigPath = func() (string, error) { 152 | if runtime.GOOS == "windows" { 153 | return filepath.Join(os.Getenv("APPDATA"), "gcloud"), nil 154 | } 155 | homeDir := guessUnixHomeDir() 156 | if homeDir == "" { 157 | return "", errors.New("unable to get current user home directory: os/user lookup failed; $HOME is empty") 158 | } 159 | return filepath.Join(homeDir, ".config", "gcloud"), nil 160 | } 161 | 162 | func guessUnixHomeDir() string { 163 | usr, err := user.Current() 164 | if err == nil { 165 | return usr.HomeDir 166 | } 167 | return os.Getenv("HOME") 168 | } 169 | -------------------------------------------------------------------------------- /google/testdata/gcloud/credentials: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "credential": { 5 | "_class": "OAuth2Credentials", 6 | "_module": "oauth2client.client", 7 | "access_token": "foo_access_token", 8 | "client_id": "foo_client_id", 9 | "client_secret": "foo_client_secret", 10 | "id_token": { 11 | "at_hash": "foo_at_hash", 12 | "aud": "foo_aud", 13 | "azp": "foo_azp", 14 | "cid": "foo_cid", 15 | "email": "foo@example.com", 16 | "email_verified": true, 17 | "exp": 1420573614, 18 | "iat": 1420569714, 19 | "id": "1337", 20 | "iss": "accounts.google.com", 21 | "sub": "1337", 22 | "token_hash": "foo_token_hash", 23 | "verified_email": true 24 | }, 25 | "invalid": false, 26 | "refresh_token": "foo_refresh_token", 27 | "revoke_uri": "https://accounts.google.com/o/oauth2/revoke", 28 | "token_expiry": "2015-01-09T00:51:51Z", 29 | "token_response": { 30 | "access_token": "foo_access_token", 31 | "expires_in": 3600, 32 | "id_token": "foo_id_token", 33 | "token_type": "Bearer" 34 | }, 35 | "token_uri": "https://accounts.google.com/o/oauth2/token", 36 | "user_agent": "Cloud SDK Command Line Tool" 37 | }, 38 | "key": { 39 | "account": "foo@example.com", 40 | "clientId": "foo_client_id", 41 | "scope": "https://www.googleapis.com/auth/appengine.admin https://www.googleapis.com/auth/bigquery https://www.googleapis.com/auth/compute https://www.googleapis.com/auth/devstorage.full_control https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/ndev.cloudman https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/sqlservice.admin https://www.googleapis.com/auth/prediction https://www.googleapis.com/auth/projecthosting", 42 | "type": "google-cloud-sdk" 43 | } 44 | }, 45 | { 46 | "credential": { 47 | "_class": "OAuth2Credentials", 48 | "_module": "oauth2client.client", 49 | "access_token": "bar_access_token", 50 | "client_id": "bar_client_id", 51 | "client_secret": "bar_client_secret", 52 | "id_token": { 53 | "at_hash": "bar_at_hash", 54 | "aud": "bar_aud", 55 | "azp": "bar_azp", 56 | "cid": "bar_cid", 57 | "email": "bar@example.com", 58 | "email_verified": true, 59 | "exp": 1420573614, 60 | "iat": 1420569714, 61 | "id": "1337", 62 | "iss": "accounts.google.com", 63 | "sub": "1337", 64 | "token_hash": "bar_token_hash", 65 | "verified_email": true 66 | }, 67 | "invalid": false, 68 | "refresh_token": "bar_refresh_token", 69 | "revoke_uri": "https://accounts.google.com/o/oauth2/revoke", 70 | "token_expiry": "2015-01-09T00:51:51Z", 71 | "token_response": { 72 | "access_token": "bar_access_token", 73 | "expires_in": 3600, 74 | "id_token": "bar_id_token", 75 | "token_type": "Bearer" 76 | }, 77 | "token_uri": "https://accounts.google.com/o/oauth2/token", 78 | "user_agent": "Cloud SDK Command Line Tool" 79 | }, 80 | "key": { 81 | "account": "bar@example.com", 82 | "clientId": "bar_client_id", 83 | "scope": "https://www.googleapis.com/auth/appengine.admin https://www.googleapis.com/auth/bigquery https://www.googleapis.com/auth/compute https://www.googleapis.com/auth/devstorage.full_control https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/ndev.cloudman https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/sqlservice.admin https://www.googleapis.com/auth/prediction https://www.googleapis.com/auth/projecthosting", 84 | "type": "google-cloud-sdk" 85 | } 86 | }, 87 | { 88 | "credential": { 89 | "_class": "ServiceAccountCredentials", 90 | "_kwargs": {}, 91 | "_module": "oauth2client.client", 92 | "_private_key_id": "00000000000000000000000000000000", 93 | "_private_key_pkcs8_text": "-----BEGIN RSA PRIVATE KEY-----\nMIICWwIBAAKBgQCt3fpiynPSaUhWSIKMGV331zudwJ6GkGmvQtwsoK2S2LbvnSwU\nNxgj4fp08kIDR5p26wF4+t/HrKydMwzftXBfZ9UmLVJgRdSswmS5SmChCrfDS5OE\nvFFcN5+6w1w8/Nu657PF/dse8T0bV95YrqyoR0Osy8WHrUOMSIIbC3hRuwIDAQAB\nAoGAJrGE/KFjn0sQ7yrZ6sXmdLawrM3mObo/2uI9T60+k7SpGbBX0/Pi6nFrJMWZ\nTVONG7P3Mu5aCPzzuVRYJB0j8aldSfzABTY3HKoWCczqw1OztJiEseXGiYz4QOyr\nYU3qDyEpdhS6q6wcoLKGH+hqRmz6pcSEsc8XzOOu7s4xW8kCQQDkc75HjhbarCnd\nJJGMe3U76+6UGmdK67ltZj6k6xoB5WbTNChY9TAyI2JC+ppYV89zv3ssj4L+02u3\nHIHFGxsHAkEAwtU1qYb1tScpchPobnYUFiVKJ7KA8EZaHVaJJODW/cghTCV7BxcJ\nbgVvlmk4lFKn3lPKAgWw7PdQsBTVBUcCrQJATPwoIirizrv3u5soJUQxZIkENAqV\nxmybZx9uetrzP7JTrVbFRf0SScMcyN90hdLJiQL8+i4+gaszgFht7sNMnwJAAbfj\nq0UXcauQwALQ7/h2oONfTg5S+MuGC/AxcXPSMZbMRGGoPh3D5YaCv27aIuS/ukQ+\n6dmm/9AGlCb64fsIWQJAPaokbjIifo+LwC5gyK73Mc4t8nAOSZDenzd/2f6TCq76\nS1dcnKiPxaED7W/y6LJiuBT2rbZiQ2L93NJpFZD/UA==\n-----END RSA PRIVATE KEY-----\n", 94 | "_revoke_uri": "https://accounts.google.com/o/oauth2/revoke", 95 | "_scopes": "https://www.googleapis.com/auth/appengine.admin https://www.googleapis.com/auth/bigquery https://www.googleapis.com/auth/compute https://www.googleapis.com/auth/devstorage.full_control https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/ndev.cloudman https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/sqlservice.admin https://www.googleapis.com/auth/prediction https://www.googleapis.com/auth/projecthosting", 96 | "_service_account_email": "baz@serviceaccount.example.com", 97 | "_service_account_id": "baz.serviceaccount.example.com", 98 | "_token_uri": "https://accounts.google.com/o/oauth2/token", 99 | "_user_agent": "Cloud SDK Command Line Tool", 100 | "access_token": null, 101 | "assertion_type": null, 102 | "client_id": null, 103 | "client_secret": null, 104 | "id_token": null, 105 | "invalid": false, 106 | "refresh_token": null, 107 | "revoke_uri": "https://accounts.google.com/o/oauth2/revoke", 108 | "service_account_name": "baz@serviceaccount.example.com", 109 | "token_expiry": null, 110 | "token_response": null, 111 | "user_agent": "Cloud SDK Command Line Tool" 112 | }, 113 | "key": { 114 | "account": "baz@serviceaccount.example.com", 115 | "clientId": "baz_client_id", 116 | "scope": "https://www.googleapis.com/auth/appengine.admin https://www.googleapis.com/auth/bigquery https://www.googleapis.com/auth/compute https://www.googleapis.com/auth/devstorage.full_control https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/ndev.cloudman https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/sqlservice.admin https://www.googleapis.com/auth/prediction https://www.googleapis.com/auth/projecthosting", 117 | "type": "google-cloud-sdk" 118 | } 119 | } 120 | ], 121 | "file_version": 1 122 | } 123 | -------------------------------------------------------------------------------- /internal/token.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The oauth2 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 | // Package internal contains support packages for oauth2 package. 6 | package internal 7 | 8 | import ( 9 | "encoding/json" 10 | "fmt" 11 | "io" 12 | "io/ioutil" 13 | "mime" 14 | "net/http" 15 | "net/url" 16 | "strconv" 17 | "strings" 18 | "time" 19 | 20 | "golang.org/x/net/context" 21 | ) 22 | 23 | // Token represents the crendentials used to authorize 24 | // the requests to access protected resources on the OAuth 2.0 25 | // provider's backend. 26 | // 27 | // This type is a mirror of oauth2.Token and exists to break 28 | // an otherwise-circular dependency. Other internal packages 29 | // should convert this Token into an oauth2.Token before use. 30 | type Token struct { 31 | // AccessToken is the token that authorizes and authenticates 32 | // the requests. 33 | AccessToken string 34 | 35 | // TokenType is the type of token. 36 | // The Type method returns either this or "Bearer", the default. 37 | TokenType string 38 | 39 | // RefreshToken is a token that's used by the application 40 | // (as opposed to the user) to refresh the access token 41 | // if it expires. 42 | RefreshToken string 43 | 44 | // Expiry is the optional expiration time of the access token. 45 | // 46 | // If zero, TokenSource implementations will reuse the same 47 | // token forever and RefreshToken or equivalent 48 | // mechanisms for that TokenSource will not be used. 49 | Expiry time.Time 50 | 51 | // Raw optionally contains extra metadata from the server 52 | // when updating a token. 53 | Raw interface{} 54 | } 55 | 56 | // tokenJSON is the struct representing the HTTP response from OAuth2 57 | // providers returning a token in JSON form. 58 | type tokenJSON struct { 59 | AccessToken string `json:"access_token"` 60 | TokenType string `json:"token_type"` 61 | RefreshToken string `json:"refresh_token"` 62 | ExpiresIn expirationTime `json:"expires_in"` // at least PayPal returns string, while most return number 63 | Expires expirationTime `json:"expires"` // broken Facebook spelling of expires_in 64 | } 65 | 66 | func (e *tokenJSON) expiry() (t time.Time) { 67 | if v := e.ExpiresIn; v != 0 { 68 | return time.Now().Add(time.Duration(v) * time.Second) 69 | } 70 | if v := e.Expires; v != 0 { 71 | return time.Now().Add(time.Duration(v) * time.Second) 72 | } 73 | return 74 | } 75 | 76 | type expirationTime int32 77 | 78 | func (e *expirationTime) UnmarshalJSON(b []byte) error { 79 | var n json.Number 80 | err := json.Unmarshal(b, &n) 81 | if err != nil { 82 | return err 83 | } 84 | i, err := n.Int64() 85 | if err != nil { 86 | return err 87 | } 88 | *e = expirationTime(i) 89 | return nil 90 | } 91 | 92 | var brokenAuthHeaderProviders = []string{ 93 | "https://accounts.google.com/", 94 | "https://www.googleapis.com/", 95 | "https://github.com/", 96 | "https://api.instagram.com/", 97 | "https://www.douban.com/", 98 | "https://api.dropbox.com/", 99 | "https://api.soundcloud.com/", 100 | "https://www.linkedin.com/", 101 | "https://api.twitch.tv/", 102 | "https://oauth.vk.com/", 103 | "https://api.odnoklassniki.ru/", 104 | "https://connect.stripe.com/", 105 | "https://api.pushbullet.com/", 106 | "https://oauth.sandbox.trainingpeaks.com/", 107 | "https://oauth.trainingpeaks.com/", 108 | "https://www.strava.com/oauth/", 109 | "https://app.box.com/", 110 | "https://test-sandbox.auth.corp.google.com", 111 | "https://user.gini.net/", 112 | "https://api.netatmo.net/", 113 | "https://login.mailchimp.com/", 114 | "https://api.createsend.com/", 115 | "https://disqus.com/", 116 | "https://slack.com/", 117 | } 118 | 119 | // providerAuthHeaderWorks reports whether the OAuth2 server identified by the tokenURL 120 | // implements the OAuth2 spec correctly 121 | // See https://code.google.com/p/goauth2/issues/detail?id=31 for background. 122 | // In summary: 123 | // - Reddit only accepts client secret in the Authorization header 124 | // - Dropbox accepts either it in URL param or Auth header, but not both. 125 | // - Google only accepts URL param (not spec compliant?), not Auth header 126 | // - Stripe only accepts client secret in Auth header with Bearer method, not Basic 127 | func providerAuthHeaderWorks(tokenURL string) bool { 128 | for _, s := range brokenAuthHeaderProviders { 129 | if strings.HasPrefix(tokenURL, s) { 130 | // Some sites fail to implement the OAuth2 spec fully. 131 | return false 132 | } 133 | } 134 | 135 | // Assume the provider implements the spec properly 136 | // otherwise. We can add more exceptions as they're 137 | // discovered. We will _not_ be adding configurable hooks 138 | // to this package to let users select server bugs. 139 | return true 140 | } 141 | 142 | func RetrieveToken(ctx context.Context, ClientID, ClientSecret, TokenURL string, v url.Values) (*Token, error) { 143 | hc, err := ContextClient(ctx) 144 | if err != nil { 145 | return nil, err 146 | } 147 | v.Set("client_id", ClientID) 148 | bustedAuth := !providerAuthHeaderWorks(TokenURL) 149 | if bustedAuth && ClientSecret != "" { 150 | v.Set("client_secret", ClientSecret) 151 | } 152 | req, err := http.NewRequest("POST", TokenURL, strings.NewReader(v.Encode())) 153 | if err != nil { 154 | return nil, err 155 | } 156 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 157 | if !bustedAuth { 158 | req.SetBasicAuth(ClientID, ClientSecret) 159 | } 160 | r, err := hc.Do(req) 161 | if err != nil { 162 | return nil, err 163 | } 164 | defer r.Body.Close() 165 | body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1<<20)) 166 | if err != nil { 167 | return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err) 168 | } 169 | if code := r.StatusCode; code < 200 || code > 299 { 170 | return nil, fmt.Errorf("oauth2: cannot fetch token: %v\nResponse: %s", r.Status, body) 171 | } 172 | 173 | var token *Token 174 | content, _, _ := mime.ParseMediaType(r.Header.Get("Content-Type")) 175 | switch content { 176 | case "application/x-www-form-urlencoded", "text/plain": 177 | vals, err := url.ParseQuery(string(body)) 178 | if err != nil { 179 | return nil, err 180 | } 181 | token = &Token{ 182 | AccessToken: vals.Get("access_token"), 183 | TokenType: vals.Get("token_type"), 184 | RefreshToken: vals.Get("refresh_token"), 185 | Raw: vals, 186 | } 187 | e := vals.Get("expires_in") 188 | if e == "" { 189 | // TODO(jbd): Facebook's OAuth2 implementation is broken and 190 | // returns expires_in field in expires. Remove the fallback to expires, 191 | // when Facebook fixes their implementation. 192 | e = vals.Get("expires") 193 | } 194 | expires, _ := strconv.Atoi(e) 195 | if expires != 0 { 196 | token.Expiry = time.Now().Add(time.Duration(expires) * time.Second) 197 | } 198 | default: 199 | var tj tokenJSON 200 | if err = json.Unmarshal(body, &tj); err != nil { 201 | return nil, err 202 | } 203 | token = &Token{ 204 | AccessToken: tj.AccessToken, 205 | TokenType: tj.TokenType, 206 | RefreshToken: tj.RefreshToken, 207 | Expiry: tj.expiry(), 208 | Raw: make(map[string]interface{}), 209 | } 210 | json.Unmarshal(body, &token.Raw) // no error checks for optional fields 211 | } 212 | // Don't overwrite `RefreshToken` with an empty value 213 | // if this was a token refreshing request. 214 | if token.RefreshToken == "" { 215 | token.RefreshToken = v.Get("refresh_token") 216 | } 217 | return token, nil 218 | } 219 | -------------------------------------------------------------------------------- /oauth2.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The oauth2 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 | // Package oauth2 provides support for making 6 | // OAuth2 authorized and authenticated HTTP requests. 7 | // It can additionally grant authorization with Bearer JWT. 8 | package oauth2 // import "github.com/cloudflare/oauth2" 9 | 10 | import ( 11 | "bytes" 12 | "errors" 13 | "net/http" 14 | "net/url" 15 | "strings" 16 | "sync" 17 | 18 | "golang.org/x/net/context" 19 | "github.com/cloudflare/oauth2/internal" 20 | ) 21 | 22 | // NoContext is the default context you should supply if not using 23 | // your own context.Context (see https://golang.org/x/net/context). 24 | var NoContext = context.TODO() 25 | 26 | // Config describes a typical 3-legged OAuth2 flow, with both the 27 | // client application information and the server's endpoint URLs. 28 | type Config struct { 29 | // ClientID is the application's ID. 30 | ClientID string 31 | 32 | // ClientSecret is the application's secret. 33 | ClientSecret string 34 | 35 | // Endpoint contains the resource server's token endpoint 36 | // URLs. These are constants specific to each server and are 37 | // often available via site-specific packages, such as 38 | // google.Endpoint or github.Endpoint. 39 | Endpoint Endpoint 40 | 41 | // RedirectURL is the URL to redirect users going through 42 | // the OAuth flow, after the resource owner's URLs. 43 | RedirectURL string 44 | 45 | // Scope specifies optional requested permissions. 46 | Scopes []string 47 | } 48 | 49 | // A TokenSource is anything that can return a token. 50 | type TokenSource interface { 51 | // Token returns a token or an error. 52 | // Token must be safe for concurrent use by multiple goroutines. 53 | // The returned Token must not be modified. 54 | Token() (*Token, error) 55 | } 56 | 57 | // Endpoint contains the OAuth 2.0 provider's authorization and token 58 | // endpoint URLs. 59 | type Endpoint struct { 60 | AuthURL string 61 | TokenURL string 62 | } 63 | 64 | var ( 65 | // AccessTypeOnline and AccessTypeOffline are options passed 66 | // to the Options.AuthCodeURL method. They modify the 67 | // "access_type" field that gets sent in the URL returned by 68 | // AuthCodeURL. 69 | // 70 | // Online is the default if neither is specified. If your 71 | // application needs to refresh access tokens when the user 72 | // is not present at the browser, then use offline. This will 73 | // result in your application obtaining a refresh token the 74 | // first time your application exchanges an authorization 75 | // code for a user. 76 | AccessTypeOnline AuthCodeOption = SetAuthURLParam("access_type", "online") 77 | AccessTypeOffline AuthCodeOption = SetAuthURLParam("access_type", "offline") 78 | 79 | // ApprovalForce forces the users to view the consent dialog 80 | // and confirm the permissions request at the URL returned 81 | // from AuthCodeURL, even if they've already done so. 82 | ApprovalForce AuthCodeOption = SetAuthURLParam("approval_prompt", "force") 83 | ) 84 | 85 | // An AuthCodeOption is passed to Config.AuthCodeURL. 86 | type AuthCodeOption interface { 87 | setValue(url.Values) 88 | } 89 | 90 | type setParam struct{ k, v string } 91 | 92 | func (p setParam) setValue(m url.Values) { m.Set(p.k, p.v) } 93 | 94 | // SetAuthURLParam builds an AuthCodeOption which passes key/value parameters 95 | // to a provider's authorization endpoint. 96 | func SetAuthURLParam(key, value string) AuthCodeOption { 97 | return setParam{key, value} 98 | } 99 | 100 | // AuthCodeURL returns a URL to OAuth 2.0 provider's consent page 101 | // that asks for permissions for the required scopes explicitly. 102 | // 103 | // State is a token to protect the user from CSRF attacks. You must 104 | // always provide a non-zero string and validate that it matches the 105 | // the state query parameter on your redirect callback. 106 | // See http://tools.ietf.org/html/rfc6749#section-10.12 for more info. 107 | // 108 | // Opts may include AccessTypeOnline or AccessTypeOffline, as well 109 | // as ApprovalForce. 110 | func (c *Config) AuthCodeURL(state string, opts ...AuthCodeOption) string { 111 | var buf bytes.Buffer 112 | buf.WriteString(c.Endpoint.AuthURL) 113 | v := url.Values{ 114 | "response_type": {"code"}, 115 | "client_id": {c.ClientID}, 116 | "redirect_uri": internal.CondVal(c.RedirectURL), 117 | "scope": internal.CondVal(strings.Join(c.Scopes, " ")), 118 | "state": internal.CondVal(state), 119 | } 120 | for _, opt := range opts { 121 | opt.setValue(v) 122 | } 123 | if strings.Contains(c.Endpoint.AuthURL, "?") { 124 | buf.WriteByte('&') 125 | } else { 126 | buf.WriteByte('?') 127 | } 128 | buf.WriteString(v.Encode()) 129 | return buf.String() 130 | } 131 | 132 | // PasswordCredentialsToken converts a resource owner username and password 133 | // pair into a token. 134 | // 135 | // Per the RFC, this grant type should only be used "when there is a high 136 | // degree of trust between the resource owner and the client (e.g., the client 137 | // is part of the device operating system or a highly privileged application), 138 | // and when other authorization grant types are not available." 139 | // See https://tools.ietf.org/html/rfc6749#section-4.3 for more info. 140 | // 141 | // The HTTP client to use is derived from the context. 142 | // If nil, http.DefaultClient is used. 143 | func (c *Config) PasswordCredentialsToken(ctx context.Context, username, password string) (*Token, error) { 144 | return retrieveToken(ctx, c, url.Values{ 145 | "grant_type": {"password"}, 146 | "username": {username}, 147 | "password": {password}, 148 | "scope": internal.CondVal(strings.Join(c.Scopes, " ")), 149 | }) 150 | } 151 | 152 | // Exchange converts an authorization code into a token. 153 | // 154 | // It is used after a resource provider redirects the user back 155 | // to the Redirect URI (the URL obtained from AuthCodeURL). 156 | // 157 | // The HTTP client to use is derived from the context. 158 | // If a client is not provided via the context, http.DefaultClient is used. 159 | // 160 | // The code will be in the *http.Request.FormValue("code"). Before 161 | // calling Exchange, be sure to validate FormValue("state"). 162 | func (c *Config) Exchange(ctx context.Context, code string) (*Token, error) { 163 | return retrieveToken(ctx, c, url.Values{ 164 | "grant_type": {"authorization_code"}, 165 | "code": {code}, 166 | "redirect_uri": internal.CondVal(c.RedirectURL), 167 | "scope": internal.CondVal(strings.Join(c.Scopes, " ")), 168 | }) 169 | } 170 | 171 | // Client returns an HTTP client using the provided token. 172 | // The token will auto-refresh as necessary. The underlying 173 | // HTTP transport will be obtained using the provided context. 174 | // The returned client and its Transport should not be modified. 175 | func (c *Config) Client(ctx context.Context, t *Token) *http.Client { 176 | return NewClient(ctx, c.TokenSource(ctx, t)) 177 | } 178 | 179 | // TokenSource returns a TokenSource that returns t until t expires, 180 | // automatically refreshing it as necessary using the provided context. 181 | // 182 | // Most users will use Config.Client instead. 183 | func (c *Config) TokenSource(ctx context.Context, t *Token) TokenSource { 184 | tkr := &tokenRefresher{ 185 | ctx: ctx, 186 | conf: c, 187 | } 188 | if t != nil { 189 | tkr.refreshToken = t.RefreshToken 190 | } 191 | return &reuseTokenSource{ 192 | t: t, 193 | new: tkr, 194 | } 195 | } 196 | 197 | // tokenRefresher is a TokenSource that makes "grant_type"=="refresh_token" 198 | // HTTP requests to renew a token using a RefreshToken. 199 | type tokenRefresher struct { 200 | ctx context.Context // used to get HTTP requests 201 | conf *Config 202 | refreshToken string 203 | } 204 | 205 | // WARNING: Token is not safe for concurrent access, as it 206 | // updates the tokenRefresher's refreshToken field. 207 | // Within this package, it is used by reuseTokenSource which 208 | // synchronizes calls to this method with its own mutex. 209 | func (tf *tokenRefresher) Token() (*Token, error) { 210 | if tf.refreshToken == "" { 211 | return nil, errors.New("oauth2: token expired and refresh token is not set") 212 | } 213 | 214 | tk, err := retrieveToken(tf.ctx, tf.conf, url.Values{ 215 | "grant_type": {"refresh_token"}, 216 | "refresh_token": {tf.refreshToken}, 217 | }) 218 | 219 | if err != nil { 220 | return nil, err 221 | } 222 | if tf.refreshToken != tk.RefreshToken { 223 | tf.refreshToken = tk.RefreshToken 224 | } 225 | return tk, err 226 | } 227 | 228 | // reuseTokenSource is a TokenSource that holds a single token in memory 229 | // and validates its expiry before each call to retrieve it with 230 | // Token. If it's expired, it will be auto-refreshed using the 231 | // new TokenSource. 232 | type reuseTokenSource struct { 233 | new TokenSource // called when t is expired. 234 | 235 | mu sync.Mutex // guards t 236 | t *Token 237 | } 238 | 239 | // Token returns the current token if it's still valid, else will 240 | // refresh the current token (using r.Context for HTTP client 241 | // information) and return the new one. 242 | func (s *reuseTokenSource) Token() (*Token, error) { 243 | s.mu.Lock() 244 | defer s.mu.Unlock() 245 | if s.t.Valid() { 246 | return s.t, nil 247 | } 248 | t, err := s.new.Token() 249 | if err != nil { 250 | return nil, err 251 | } 252 | s.t = t 253 | return t, nil 254 | } 255 | 256 | // StaticTokenSource returns a TokenSource that always returns the same token. 257 | // Because the provided token t is never refreshed, StaticTokenSource is only 258 | // useful for tokens that never expire. 259 | func StaticTokenSource(t *Token) TokenSource { 260 | return staticTokenSource{t} 261 | } 262 | 263 | // staticTokenSource is a TokenSource that always returns the same Token. 264 | type staticTokenSource struct { 265 | t *Token 266 | } 267 | 268 | func (s staticTokenSource) Token() (*Token, error) { 269 | return s.t, nil 270 | } 271 | 272 | // HTTPClient is the context key to use with golang.org/x/net/context's 273 | // WithValue function to associate an *http.Client value with a context. 274 | var HTTPClient internal.ContextKey 275 | 276 | // NewClient creates an *http.Client from a Context and TokenSource. 277 | // The returned client is not valid beyond the lifetime of the context. 278 | // 279 | // As a special case, if src is nil, a non-OAuth2 client is returned 280 | // using the provided context. This exists to support related OAuth2 281 | // packages. 282 | func NewClient(ctx context.Context, src TokenSource) *http.Client { 283 | if src == nil { 284 | c, err := internal.ContextClient(ctx) 285 | if err != nil { 286 | return &http.Client{Transport: internal.ErrorTransport{err}} 287 | } 288 | return c 289 | } 290 | return &http.Client{ 291 | Transport: &Transport{ 292 | Base: internal.ContextTransport(ctx), 293 | Source: ReuseTokenSource(nil, src), 294 | }, 295 | } 296 | } 297 | 298 | // ReuseTokenSource returns a TokenSource which repeatedly returns the 299 | // same token as long as it's valid, starting with t. 300 | // When its cached token is invalid, a new token is obtained from src. 301 | // 302 | // ReuseTokenSource is typically used to reuse tokens from a cache 303 | // (such as a file on disk) between runs of a program, rather than 304 | // obtaining new tokens unnecessarily. 305 | // 306 | // The initial token t may be nil, in which case the TokenSource is 307 | // wrapped in a caching version if it isn't one already. This also 308 | // means it's always safe to wrap ReuseTokenSource around any other 309 | // TokenSource without adverse effects. 310 | func ReuseTokenSource(t *Token, src TokenSource) TokenSource { 311 | // Don't wrap a reuseTokenSource in itself. That would work, 312 | // but cause an unnecessary number of mutex operations. 313 | // Just build the equivalent one. 314 | if rt, ok := src.(*reuseTokenSource); ok { 315 | if t == nil { 316 | // Just use it directly. 317 | return rt 318 | } 319 | src = rt.new 320 | } 321 | return &reuseTokenSource{ 322 | t: t, 323 | new: src, 324 | } 325 | } 326 | -------------------------------------------------------------------------------- /oauth2_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The oauth2 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 | package oauth2 6 | 7 | import ( 8 | "encoding/json" 9 | "errors" 10 | "fmt" 11 | "io/ioutil" 12 | "net/http" 13 | "net/http/httptest" 14 | "reflect" 15 | "strconv" 16 | "testing" 17 | "time" 18 | 19 | "golang.org/x/net/context" 20 | ) 21 | 22 | type mockTransport struct { 23 | rt func(req *http.Request) (resp *http.Response, err error) 24 | } 25 | 26 | func (t *mockTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) { 27 | return t.rt(req) 28 | } 29 | 30 | type mockCache struct { 31 | token *Token 32 | readErr error 33 | } 34 | 35 | func (c *mockCache) ReadToken() (*Token, error) { 36 | return c.token, c.readErr 37 | } 38 | 39 | func (c *mockCache) WriteToken(*Token) { 40 | // do nothing 41 | } 42 | 43 | func newConf(url string) *Config { 44 | return &Config{ 45 | ClientID: "CLIENT_ID", 46 | ClientSecret: "CLIENT_SECRET", 47 | RedirectURL: "REDIRECT_URL", 48 | Scopes: []string{"scope1", "scope2"}, 49 | Endpoint: Endpoint{ 50 | AuthURL: url + "/auth", 51 | TokenURL: url + "/token", 52 | }, 53 | } 54 | } 55 | 56 | func TestAuthCodeURL(t *testing.T) { 57 | conf := newConf("server") 58 | url := conf.AuthCodeURL("foo", AccessTypeOffline, ApprovalForce) 59 | if url != "server/auth?access_type=offline&approval_prompt=force&client_id=CLIENT_ID&redirect_uri=REDIRECT_URL&response_type=code&scope=scope1+scope2&state=foo" { 60 | t.Errorf("Auth code URL doesn't match the expected, found: %v", url) 61 | } 62 | } 63 | 64 | func TestAuthCodeURL_CustomParam(t *testing.T) { 65 | conf := newConf("server") 66 | param := SetAuthURLParam("foo", "bar") 67 | url := conf.AuthCodeURL("baz", param) 68 | if url != "server/auth?client_id=CLIENT_ID&foo=bar&redirect_uri=REDIRECT_URL&response_type=code&scope=scope1+scope2&state=baz" { 69 | t.Errorf("Auth code URL doesn't match the expected, found: %v", url) 70 | } 71 | } 72 | 73 | func TestAuthCodeURL_Optional(t *testing.T) { 74 | conf := &Config{ 75 | ClientID: "CLIENT_ID", 76 | Endpoint: Endpoint{ 77 | AuthURL: "/auth-url", 78 | TokenURL: "/token-url", 79 | }, 80 | } 81 | url := conf.AuthCodeURL("") 82 | if url != "/auth-url?client_id=CLIENT_ID&response_type=code" { 83 | t.Fatalf("Auth code URL doesn't match the expected, found: %v", url) 84 | } 85 | } 86 | 87 | func TestExchangeRequest(t *testing.T) { 88 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 89 | if r.URL.String() != "/token" { 90 | t.Errorf("Unexpected exchange request URL, %v is found.", r.URL) 91 | } 92 | headerAuth := r.Header.Get("Authorization") 93 | if headerAuth != "Basic Q0xJRU5UX0lEOkNMSUVOVF9TRUNSRVQ=" { 94 | t.Errorf("Unexpected authorization header, %v is found.", headerAuth) 95 | } 96 | headerContentType := r.Header.Get("Content-Type") 97 | if headerContentType != "application/x-www-form-urlencoded" { 98 | t.Errorf("Unexpected Content-Type header, %v is found.", headerContentType) 99 | } 100 | body, err := ioutil.ReadAll(r.Body) 101 | if err != nil { 102 | t.Errorf("Failed reading request body: %s.", err) 103 | } 104 | if string(body) != "client_id=CLIENT_ID&code=exchange-code&grant_type=authorization_code&redirect_uri=REDIRECT_URL&scope=scope1+scope2" { 105 | t.Errorf("Unexpected exchange payload, %v is found.", string(body)) 106 | } 107 | w.Header().Set("Content-Type", "application/x-www-form-urlencoded") 108 | w.Write([]byte("access_token=90d64460d14870c08c81352a05dedd3465940a7c&scope=user&token_type=bearer")) 109 | })) 110 | defer ts.Close() 111 | conf := newConf(ts.URL) 112 | tok, err := conf.Exchange(NoContext, "exchange-code") 113 | if err != nil { 114 | t.Error(err) 115 | } 116 | if !tok.Valid() { 117 | t.Fatalf("Token invalid. Got: %#v", tok) 118 | } 119 | if tok.AccessToken != "90d64460d14870c08c81352a05dedd3465940a7c" { 120 | t.Errorf("Unexpected access token, %#v.", tok.AccessToken) 121 | } 122 | if tok.TokenType != "bearer" { 123 | t.Errorf("Unexpected token type, %#v.", tok.TokenType) 124 | } 125 | scope := tok.Extra("scope") 126 | if scope != "user" { 127 | t.Errorf("Unexpected value for scope: %v", scope) 128 | } 129 | } 130 | 131 | func TestExchangeRequest_JSONResponse(t *testing.T) { 132 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 133 | if r.URL.String() != "/token" { 134 | t.Errorf("Unexpected exchange request URL, %v is found.", r.URL) 135 | } 136 | headerAuth := r.Header.Get("Authorization") 137 | if headerAuth != "Basic Q0xJRU5UX0lEOkNMSUVOVF9TRUNSRVQ=" { 138 | t.Errorf("Unexpected authorization header, %v is found.", headerAuth) 139 | } 140 | headerContentType := r.Header.Get("Content-Type") 141 | if headerContentType != "application/x-www-form-urlencoded" { 142 | t.Errorf("Unexpected Content-Type header, %v is found.", headerContentType) 143 | } 144 | body, err := ioutil.ReadAll(r.Body) 145 | if err != nil { 146 | t.Errorf("Failed reading request body: %s.", err) 147 | } 148 | if string(body) != "client_id=CLIENT_ID&code=exchange-code&grant_type=authorization_code&redirect_uri=REDIRECT_URL&scope=scope1+scope2" { 149 | t.Errorf("Unexpected exchange payload, %v is found.", string(body)) 150 | } 151 | w.Header().Set("Content-Type", "application/json") 152 | w.Write([]byte(`{"access_token": "90d64460d14870c08c81352a05dedd3465940a7c", "scope": "user", "token_type": "bearer", "expires_in": 86400}`)) 153 | })) 154 | defer ts.Close() 155 | conf := newConf(ts.URL) 156 | tok, err := conf.Exchange(NoContext, "exchange-code") 157 | if err != nil { 158 | t.Error(err) 159 | } 160 | if !tok.Valid() { 161 | t.Fatalf("Token invalid. Got: %#v", tok) 162 | } 163 | if tok.AccessToken != "90d64460d14870c08c81352a05dedd3465940a7c" { 164 | t.Errorf("Unexpected access token, %#v.", tok.AccessToken) 165 | } 166 | if tok.TokenType != "bearer" { 167 | t.Errorf("Unexpected token type, %#v.", tok.TokenType) 168 | } 169 | scope := tok.Extra("scope") 170 | if scope != "user" { 171 | t.Errorf("Unexpected value for scope: %v", scope) 172 | } 173 | } 174 | 175 | const day = 24 * time.Hour 176 | 177 | func TestExchangeRequest_JSONResponse_Expiry(t *testing.T) { 178 | seconds := int32(day.Seconds()) 179 | jsonNumberType := reflect.TypeOf(json.Number("0")) 180 | for _, c := range []struct { 181 | expires string 182 | expect error 183 | }{ 184 | {fmt.Sprintf(`"expires_in": %d`, seconds), nil}, 185 | {fmt.Sprintf(`"expires_in": "%d"`, seconds), nil}, // PayPal case 186 | {fmt.Sprintf(`"expires": %d`, seconds), nil}, // Facebook case 187 | {`"expires": false`, &json.UnmarshalTypeError{Value: "bool", Type: jsonNumberType}}, // wrong type 188 | {`"expires": {}`, &json.UnmarshalTypeError{Value: "object", Type: jsonNumberType}}, // wrong type 189 | {`"expires": "zzz"`, &strconv.NumError{Func: "ParseInt", Num: "zzz", Err: strconv.ErrSyntax}}, // wrong value 190 | } { 191 | testExchangeRequest_JSONResponse_expiry(t, c.expires, c.expect) 192 | } 193 | } 194 | 195 | func testExchangeRequest_JSONResponse_expiry(t *testing.T, exp string, expect error) { 196 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 197 | w.Header().Set("Content-Type", "application/json") 198 | w.Write([]byte(fmt.Sprintf(`{"access_token": "90d", "scope": "user", "token_type": "bearer", %s}`, exp))) 199 | })) 200 | defer ts.Close() 201 | conf := newConf(ts.URL) 202 | t1 := time.Now().Add(day) 203 | tok, err := conf.Exchange(NoContext, "exchange-code") 204 | t2 := time.Now().Add(day) 205 | // Do a fmt.Sprint comparison so either side can be 206 | // nil. fmt.Sprint just stringifies them to "", and no 207 | // non-nil expected error ever stringifies as "", so this 208 | // isn't terribly disgusting. We do this because Go 1.4 and 209 | // Go 1.5 return a different deep value for 210 | // json.UnmarshalTypeError. In Go 1.5, the 211 | // json.UnmarshalTypeError contains a new field with a new 212 | // non-zero value. Rather than ignore it here with reflect or 213 | // add new files and +build tags, just look at the strings. 214 | if fmt.Sprint(err) != fmt.Sprint(expect) { 215 | t.Errorf("Error = %v; want %v", err, expect) 216 | } 217 | if err != nil { 218 | return 219 | } 220 | if !tok.Valid() { 221 | t.Fatalf("Token invalid. Got: %#v", tok) 222 | } 223 | expiry := tok.Expiry 224 | if expiry.Before(t1) || expiry.After(t2) { 225 | t.Errorf("Unexpected value for Expiry: %v (shold be between %v and %v)", expiry, t1, t2) 226 | } 227 | } 228 | 229 | func TestExchangeRequest_BadResponse(t *testing.T) { 230 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 231 | w.Header().Set("Content-Type", "application/json") 232 | w.Write([]byte(`{"scope": "user", "token_type": "bearer"}`)) 233 | })) 234 | defer ts.Close() 235 | conf := newConf(ts.URL) 236 | tok, err := conf.Exchange(NoContext, "code") 237 | if err != nil { 238 | t.Fatal(err) 239 | } 240 | if tok.AccessToken != "" { 241 | t.Errorf("Unexpected access token, %#v.", tok.AccessToken) 242 | } 243 | } 244 | 245 | func TestExchangeRequest_BadResponseType(t *testing.T) { 246 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 247 | w.Header().Set("Content-Type", "application/json") 248 | w.Write([]byte(`{"access_token":123, "scope": "user", "token_type": "bearer"}`)) 249 | })) 250 | defer ts.Close() 251 | conf := newConf(ts.URL) 252 | _, err := conf.Exchange(NoContext, "exchange-code") 253 | if err == nil { 254 | t.Error("expected error from invalid access_token type") 255 | } 256 | } 257 | 258 | func TestExchangeRequest_NonBasicAuth(t *testing.T) { 259 | tr := &mockTransport{ 260 | rt: func(r *http.Request) (w *http.Response, err error) { 261 | headerAuth := r.Header.Get("Authorization") 262 | if headerAuth != "" { 263 | t.Errorf("Unexpected authorization header, %v is found.", headerAuth) 264 | } 265 | return nil, errors.New("no response") 266 | }, 267 | } 268 | c := &http.Client{Transport: tr} 269 | conf := &Config{ 270 | ClientID: "CLIENT_ID", 271 | Endpoint: Endpoint{ 272 | AuthURL: "https://accounts.google.com/auth", 273 | TokenURL: "https://accounts.google.com/token", 274 | }, 275 | } 276 | 277 | ctx := context.WithValue(context.Background(), HTTPClient, c) 278 | conf.Exchange(ctx, "code") 279 | } 280 | 281 | func TestPasswordCredentialsTokenRequest(t *testing.T) { 282 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 283 | defer r.Body.Close() 284 | expected := "/token" 285 | if r.URL.String() != expected { 286 | t.Errorf("URL = %q; want %q", r.URL, expected) 287 | } 288 | headerAuth := r.Header.Get("Authorization") 289 | expected = "Basic Q0xJRU5UX0lEOkNMSUVOVF9TRUNSRVQ=" 290 | if headerAuth != expected { 291 | t.Errorf("Authorization header = %q; want %q", headerAuth, expected) 292 | } 293 | headerContentType := r.Header.Get("Content-Type") 294 | expected = "application/x-www-form-urlencoded" 295 | if headerContentType != expected { 296 | t.Errorf("Content-Type header = %q; want %q", headerContentType, expected) 297 | } 298 | body, err := ioutil.ReadAll(r.Body) 299 | if err != nil { 300 | t.Errorf("Failed reading request body: %s.", err) 301 | } 302 | expected = "client_id=CLIENT_ID&grant_type=password&password=password1&scope=scope1+scope2&username=user1" 303 | if string(body) != expected { 304 | t.Errorf("res.Body = %q; want %q", string(body), expected) 305 | } 306 | w.Header().Set("Content-Type", "application/x-www-form-urlencoded") 307 | w.Write([]byte("access_token=90d64460d14870c08c81352a05dedd3465940a7c&scope=user&token_type=bearer")) 308 | })) 309 | defer ts.Close() 310 | conf := newConf(ts.URL) 311 | tok, err := conf.PasswordCredentialsToken(NoContext, "user1", "password1") 312 | if err != nil { 313 | t.Error(err) 314 | } 315 | if !tok.Valid() { 316 | t.Fatalf("Token invalid. Got: %#v", tok) 317 | } 318 | expected := "90d64460d14870c08c81352a05dedd3465940a7c" 319 | if tok.AccessToken != expected { 320 | t.Errorf("AccessToken = %q; want %q", tok.AccessToken, expected) 321 | } 322 | expected = "bearer" 323 | if tok.TokenType != expected { 324 | t.Errorf("TokenType = %q; want %q", tok.TokenType, expected) 325 | } 326 | } 327 | 328 | func TestTokenRefreshRequest(t *testing.T) { 329 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 330 | if r.URL.String() == "/somethingelse" { 331 | return 332 | } 333 | if r.URL.String() != "/token" { 334 | t.Errorf("Unexpected token refresh request URL, %v is found.", r.URL) 335 | } 336 | headerContentType := r.Header.Get("Content-Type") 337 | if headerContentType != "application/x-www-form-urlencoded" { 338 | t.Errorf("Unexpected Content-Type header, %v is found.", headerContentType) 339 | } 340 | body, _ := ioutil.ReadAll(r.Body) 341 | if string(body) != "client_id=CLIENT_ID&grant_type=refresh_token&refresh_token=REFRESH_TOKEN" { 342 | t.Errorf("Unexpected refresh token payload, %v is found.", string(body)) 343 | } 344 | })) 345 | defer ts.Close() 346 | conf := newConf(ts.URL) 347 | c := conf.Client(NoContext, &Token{RefreshToken: "REFRESH_TOKEN"}) 348 | c.Get(ts.URL + "/somethingelse") 349 | } 350 | 351 | func TestFetchWithNoRefreshToken(t *testing.T) { 352 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 353 | if r.URL.String() == "/somethingelse" { 354 | return 355 | } 356 | if r.URL.String() != "/token" { 357 | t.Errorf("Unexpected token refresh request URL, %v is found.", r.URL) 358 | } 359 | headerContentType := r.Header.Get("Content-Type") 360 | if headerContentType != "application/x-www-form-urlencoded" { 361 | t.Errorf("Unexpected Content-Type header, %v is found.", headerContentType) 362 | } 363 | body, _ := ioutil.ReadAll(r.Body) 364 | if string(body) != "client_id=CLIENT_ID&grant_type=refresh_token&refresh_token=REFRESH_TOKEN" { 365 | t.Errorf("Unexpected refresh token payload, %v is found.", string(body)) 366 | } 367 | })) 368 | defer ts.Close() 369 | conf := newConf(ts.URL) 370 | c := conf.Client(NoContext, nil) 371 | _, err := c.Get(ts.URL + "/somethingelse") 372 | if err == nil { 373 | t.Errorf("Fetch should return an error if no refresh token is set") 374 | } 375 | } 376 | 377 | func TestRefreshToken_RefreshTokenReplacement(t *testing.T) { 378 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 379 | w.Header().Set("Content-Type", "application/json") 380 | w.Write([]byte(`{"access_token":"ACCESS TOKEN", "scope": "user", "token_type": "bearer", "refresh_token": "NEW REFRESH TOKEN"}`)) 381 | return 382 | })) 383 | defer ts.Close() 384 | conf := newConf(ts.URL) 385 | tkr := tokenRefresher{ 386 | conf: conf, 387 | ctx: NoContext, 388 | refreshToken: "OLD REFRESH TOKEN", 389 | } 390 | tk, err := tkr.Token() 391 | if err != nil { 392 | t.Errorf("Unexpected refreshToken error returned: %v", err) 393 | return 394 | } 395 | if tk.RefreshToken != tkr.refreshToken { 396 | t.Errorf("tokenRefresher.refresh_token = %s; want %s", tkr.refreshToken, tk.RefreshToken) 397 | } 398 | } 399 | 400 | func TestConfigClientWithToken(t *testing.T) { 401 | tok := &Token{ 402 | AccessToken: "abc123", 403 | } 404 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 405 | if got, want := r.Header.Get("Authorization"), fmt.Sprintf("Bearer %s", tok.AccessToken); got != want { 406 | t.Errorf("Authorization header = %q; want %q", got, want) 407 | } 408 | return 409 | })) 410 | defer ts.Close() 411 | conf := newConf(ts.URL) 412 | 413 | c := conf.Client(NoContext, tok) 414 | req, err := http.NewRequest("GET", ts.URL, nil) 415 | if err != nil { 416 | t.Error(err) 417 | } 418 | _, err = c.Do(req) 419 | if err != nil { 420 | t.Error(err) 421 | } 422 | } 423 | --------------------------------------------------------------------------------