├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── amazon └── amazon.go ├── authhandler ├── authhandler.go └── authhandler_test.go ├── bitbucket └── bitbucket.go ├── cern └── cern.go ├── clientcredentials ├── clientcredentials.go └── clientcredentials_test.go ├── deviceauth.go ├── deviceauth_test.go ├── endpoints ├── endpoints.go └── endpoints_test.go ├── example_test.go ├── facebook └── facebook.go ├── fitbit └── fitbit.go ├── foursquare └── foursquare.go ├── github └── github.go ├── gitlab └── gitlab.go ├── go.mod ├── go.sum ├── google ├── appengine.go ├── default.go ├── default_test.go ├── doc.go ├── downscope │ ├── downscoping.go │ ├── downscoping_test.go │ └── tokenbroker_test.go ├── error.go ├── error_test.go ├── example_test.go ├── externalaccount │ ├── aws.go │ ├── aws_test.go │ ├── basecredentials.go │ ├── basecredentials_test.go │ ├── executablecredsource.go │ ├── executablecredsource_test.go │ ├── filecredsource.go │ ├── filecredsource_test.go │ ├── header.go │ ├── header_test.go │ ├── programmaticrefreshcredsource.go │ ├── programmaticrefreshcredsource_test.go │ ├── testdata │ │ ├── 3pi_cred.json │ │ └── 3pi_cred.txt │ ├── urlcredsource.go │ └── urlcredsource_test.go ├── google.go ├── google_test.go ├── internal │ ├── externalaccountauthorizeduser │ │ ├── externalaccountauthorizeduser.go │ │ └── externalaccountauthorizeduser_test.go │ ├── impersonate │ │ └── impersonate.go │ └── stsexchange │ │ ├── clientauth.go │ │ ├── clientauth_test.go │ │ ├── sts_exchange.go │ │ └── sts_exchange_test.go ├── jwt.go ├── jwt_test.go ├── sdk.go ├── sdk_test.go └── testdata │ └── gcloud │ ├── credentials │ └── properties ├── heroku └── heroku.go ├── hipchat └── hipchat.go ├── instagram └── instagram.go ├── internal ├── doc.go ├── oauth2.go ├── token.go ├── token_test.go └── transport.go ├── jira ├── jira.go └── jira_test.go ├── jws ├── jws.go └── jws_test.go ├── jwt ├── example_test.go ├── jwt.go └── jwt_test.go ├── kakao └── kakao.go ├── linkedin └── linkedin.go ├── mailchimp └── mailchimp.go ├── mailru └── mailru.go ├── mediamath └── mediamath.go ├── microsoft └── microsoft.go ├── nokiahealth └── nokiahealth.go ├── oauth2.go ├── oauth2_test.go ├── odnoklassniki └── odnoklassniki.go ├── paypal └── paypal.go ├── pkce.go ├── slack └── slack.go ├── spotify └── spotify.go ├── stackoverflow └── stackoverflow.go ├── token.go ├── token_test.go ├── transport.go ├── transport_test.go ├── twitch └── twitch.go ├── uber └── uber.go ├── vk └── vk.go ├── yahoo └── yahoo.go └── yandex └── yandex.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - tip 5 | 6 | install: 7 | - export GOPATH="$HOME/gopath" 8 | - mkdir -p "$GOPATH/src/golang.org/x" 9 | - mv "$TRAVIS_BUILD_DIR" "$GOPATH/src/golang.org/x/oauth2" 10 | - go get -v -t -d golang.org/x/oauth2/... 11 | 12 | script: 13 | - go test -v golang.org/x/oauth2/... 14 | -------------------------------------------------------------------------------- /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 | ## Filing issues 8 | 9 | When [filing an issue](https://github.com/golang/oauth2/issues), make sure to answer these five questions: 10 | 11 | 1. What version of Go are you using (`go version`)? 12 | 2. What operating system and processor architecture are you using? 13 | 3. What did you do? 14 | 4. What did you expect to see? 15 | 5. What did you see instead? 16 | 17 | General questions should go to the [golang-nuts mailing list](https://groups.google.com/group/golang-nuts) instead of the issue tracker. 18 | The gophers there will answer or ask you to file an issue if you've tripped over a bug. 19 | 20 | ## Contributing code 21 | 22 | Please read the [Contribution Guidelines](https://golang.org/doc/contribute.html) 23 | before sending patches. 24 | 25 | Unless otherwise noted, the Go source files are distributed under 26 | the BSD-style license found in the LICENSE file. 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2009 The Go Authors. 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 LLC 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OAuth2 for Go 2 | 3 | [![Go Reference](https://pkg.go.dev/badge/golang.org/x/oauth2.svg)](https://pkg.go.dev/golang.org/x/oauth2) 4 | [![Build Status](https://travis-ci.org/golang/oauth2.svg?branch=master)](https://travis-ci.org/golang/oauth2) 5 | 6 | oauth2 package contains a client implementation for OAuth 2.0 spec. 7 | 8 | See pkg.go.dev for further documentation and examples. 9 | 10 | * [pkg.go.dev/golang.org/x/oauth2](https://pkg.go.dev/golang.org/x/oauth2) 11 | * [pkg.go.dev/golang.org/x/oauth2/google](https://pkg.go.dev/golang.org/x/oauth2/google) 12 | 13 | ## Policy for new endpoints 14 | 15 | We no longer accept new provider-specific packages in this repo if all 16 | they do is add a single endpoint variable. If you just want to add a 17 | single endpoint, add it to the 18 | [pkg.go.dev/golang.org/x/oauth2/endpoints](https://pkg.go.dev/golang.org/x/oauth2/endpoints) 19 | package. 20 | 21 | ## Report Issues / Send Patches 22 | 23 | The main issue tracker for the oauth2 repository is located at 24 | https://github.com/golang/oauth2/issues. 25 | 26 | This repository uses Gerrit for code changes. To learn how to submit changes to 27 | this repository, see https://go.dev/doc/contribute. 28 | 29 | The git repository is https://go.googlesource.com/oauth2. 30 | 31 | Note: 32 | 33 | * Excluding trivial changes, all contributions should be connected to an existing issue. 34 | * API changes must go through the [change proposal process](https://go.dev/s/proposal-process) before they can be accepted. 35 | * The code owners are listed at [dev.golang.org/owners](https://dev.golang.org/owners#:~:text=x/oauth2). 36 | -------------------------------------------------------------------------------- /amazon/amazon.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 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 amazon provides constants for using OAuth2 to access Amazon. 6 | package amazon 7 | 8 | import ( 9 | "golang.org/x/oauth2" 10 | ) 11 | 12 | // Endpoint is Amazon's OAuth 2.0 endpoint. 13 | var Endpoint = oauth2.Endpoint{ 14 | AuthURL: "https://www.amazon.com/ap/oa", 15 | TokenURL: "https://api.amazon.com/auth/o2/token", 16 | } 17 | -------------------------------------------------------------------------------- /authhandler/authhandler.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The Go 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 authhandler implements a TokenSource to support 6 | // "three-legged OAuth 2.0" via a custom AuthorizationHandler. 7 | package authhandler 8 | 9 | import ( 10 | "context" 11 | "errors" 12 | 13 | "golang.org/x/oauth2" 14 | ) 15 | 16 | const ( 17 | // Parameter keys for AuthCodeURL method to support PKCE. 18 | codeChallengeKey = "code_challenge" 19 | codeChallengeMethodKey = "code_challenge_method" 20 | 21 | // Parameter key for Exchange method to support PKCE. 22 | codeVerifierKey = "code_verifier" 23 | ) 24 | 25 | // PKCEParams holds parameters to support PKCE. 26 | type PKCEParams struct { 27 | Challenge string // The unpadded, base64-url-encoded string of the encrypted code verifier. 28 | ChallengeMethod string // The encryption method (ex. S256). 29 | Verifier string // The original, non-encrypted secret. 30 | } 31 | 32 | // AuthorizationHandler is a 3-legged-OAuth helper that prompts 33 | // the user for OAuth consent at the specified auth code URL 34 | // and returns an auth code and state upon approval. 35 | type AuthorizationHandler func(authCodeURL string) (code string, state string, err error) 36 | 37 | // TokenSourceWithPKCE is an enhanced version of [oauth2.TokenSource] with PKCE support. 38 | // 39 | // The pkce parameter supports PKCE flow, which uses code challenge and code verifier 40 | // to prevent CSRF attacks. A unique code challenge and code verifier should be generated 41 | // by the caller at runtime. See https://www.oauth.com/oauth2-servers/pkce/ for more info. 42 | func TokenSourceWithPKCE(ctx context.Context, config *oauth2.Config, state string, authHandler AuthorizationHandler, pkce *PKCEParams) oauth2.TokenSource { 43 | return oauth2.ReuseTokenSource(nil, authHandlerSource{config: config, ctx: ctx, authHandler: authHandler, state: state, pkce: pkce}) 44 | } 45 | 46 | // TokenSource returns an [oauth2.TokenSource] that fetches access tokens 47 | // using 3-legged-OAuth flow. 48 | // 49 | // The provided [context.Context] is used for oauth2 Exchange operation. 50 | // 51 | // The provided [oauth2.Config] should be a full configuration containing AuthURL, 52 | // TokenURL, and Scope. 53 | // 54 | // An environment-specific AuthorizationHandler is used to obtain user consent. 55 | // 56 | // Per the OAuth protocol, a unique "state" string should be specified here. 57 | // This token source will verify that the "state" is identical in the request 58 | // and response before exchanging the auth code for OAuth token to prevent CSRF 59 | // attacks. 60 | func TokenSource(ctx context.Context, config *oauth2.Config, state string, authHandler AuthorizationHandler) oauth2.TokenSource { 61 | return TokenSourceWithPKCE(ctx, config, state, authHandler, nil) 62 | } 63 | 64 | type authHandlerSource struct { 65 | ctx context.Context 66 | config *oauth2.Config 67 | authHandler AuthorizationHandler 68 | state string 69 | pkce *PKCEParams 70 | } 71 | 72 | func (source authHandlerSource) Token() (*oauth2.Token, error) { 73 | // Step 1: Obtain auth code. 74 | var authCodeUrlOptions []oauth2.AuthCodeOption 75 | if source.pkce != nil && source.pkce.Challenge != "" && source.pkce.ChallengeMethod != "" { 76 | authCodeUrlOptions = []oauth2.AuthCodeOption{oauth2.SetAuthURLParam(codeChallengeKey, source.pkce.Challenge), 77 | oauth2.SetAuthURLParam(codeChallengeMethodKey, source.pkce.ChallengeMethod)} 78 | } 79 | url := source.config.AuthCodeURL(source.state, authCodeUrlOptions...) 80 | code, state, err := source.authHandler(url) 81 | if err != nil { 82 | return nil, err 83 | } 84 | if state != source.state { 85 | return nil, errors.New("state mismatch in 3-legged-OAuth flow") 86 | } 87 | 88 | // Step 2: Exchange auth code for access token. 89 | var exchangeOptions []oauth2.AuthCodeOption 90 | if source.pkce != nil && source.pkce.Verifier != "" { 91 | exchangeOptions = []oauth2.AuthCodeOption{oauth2.SetAuthURLParam(codeVerifierKey, source.pkce.Verifier)} 92 | } 93 | return source.config.Exchange(source.ctx, code, exchangeOptions...) 94 | } 95 | -------------------------------------------------------------------------------- /authhandler/authhandler_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The Go 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 authhandler 6 | 7 | import ( 8 | "context" 9 | "fmt" 10 | "net/http" 11 | "net/http/httptest" 12 | "testing" 13 | 14 | "golang.org/x/oauth2" 15 | ) 16 | 17 | func TestTokenExchange_Success(t *testing.T) { 18 | authhandler := func(authCodeURL string) (string, string, error) { 19 | if authCodeURL == "testAuthCodeURL?client_id=testClientID&response_type=code&scope=pubsub&state=testState" { 20 | return "testCode", "testState", nil 21 | } 22 | return "", "", fmt.Errorf("invalid authCodeURL: %q", authCodeURL) 23 | } 24 | 25 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 26 | r.ParseForm() 27 | if r.Form.Get("code") == "testCode" { 28 | w.Header().Set("Content-Type", "application/json") 29 | w.Write([]byte(`{ 30 | "access_token": "90d64460d14870c08c81352a05dedd3465940a7c", 31 | "scope": "pubsub", 32 | "token_type": "bearer", 33 | "expires_in": 3600 34 | }`)) 35 | } 36 | })) 37 | defer ts.Close() 38 | 39 | conf := &oauth2.Config{ 40 | ClientID: "testClientID", 41 | Scopes: []string{"pubsub"}, 42 | Endpoint: oauth2.Endpoint{ 43 | AuthURL: "testAuthCodeURL", 44 | TokenURL: ts.URL, 45 | }, 46 | } 47 | 48 | tok, err := TokenSource(context.Background(), conf, "testState", authhandler).Token() 49 | if err != nil { 50 | t.Fatal(err) 51 | } 52 | if !tok.Valid() { 53 | t.Errorf("got invalid token: %v", tok) 54 | } 55 | if got, want := tok.AccessToken, "90d64460d14870c08c81352a05dedd3465940a7c"; got != want { 56 | t.Errorf("access token = %q; want %q", got, want) 57 | } 58 | if got, want := tok.TokenType, "bearer"; got != want { 59 | t.Errorf("token type = %q; want %q", got, want) 60 | } 61 | if got := tok.Expiry.IsZero(); got { 62 | t.Errorf("token expiry is zero = %v, want false", got) 63 | } 64 | scope := tok.Extra("scope") 65 | if got, want := scope, "pubsub"; got != want { 66 | t.Errorf("scope = %q; want %q", got, want) 67 | } 68 | } 69 | 70 | func TestTokenExchange_StateMismatch(t *testing.T) { 71 | authhandler := func(authCodeURL string) (string, string, error) { 72 | return "testCode", "testStateMismatch", nil 73 | } 74 | 75 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 76 | w.Header().Set("Content-Type", "application/json") 77 | w.Write([]byte(`{ 78 | "access_token": "90d64460d14870c08c81352a05dedd3465940a7c", 79 | "scope": "pubsub", 80 | "token_type": "bearer", 81 | "expires_in": 3600 82 | }`)) 83 | })) 84 | defer ts.Close() 85 | 86 | conf := &oauth2.Config{ 87 | ClientID: "testClientID", 88 | Scopes: []string{"pubsub"}, 89 | Endpoint: oauth2.Endpoint{ 90 | AuthURL: "testAuthCodeURL", 91 | TokenURL: ts.URL, 92 | }, 93 | } 94 | 95 | _, err := TokenSource(context.Background(), conf, "testState", authhandler).Token() 96 | if want_err := "state mismatch in 3-legged-OAuth flow"; err == nil || err.Error() != want_err { 97 | t.Errorf("err = %q; want %q", err, want_err) 98 | } 99 | } 100 | 101 | func TestTokenExchangeWithPKCE_Success(t *testing.T) { 102 | authhandler := func(authCodeURL string) (string, string, error) { 103 | if authCodeURL == "testAuthCodeURL?client_id=testClientID&code_challenge=codeChallenge&code_challenge_method=plain&response_type=code&scope=pubsub&state=testState" { 104 | return "testCode", "testState", nil 105 | } 106 | return "", "", fmt.Errorf("invalid authCodeURL: %q", authCodeURL) 107 | } 108 | 109 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 110 | r.ParseForm() 111 | if r.Form.Get("code") == "testCode" && r.Form.Get("code_verifier") == "codeChallenge" { 112 | w.Header().Set("Content-Type", "application/json") 113 | w.Write([]byte(`{ 114 | "access_token": "90d64460d14870c08c81352a05dedd3465940a7c", 115 | "scope": "pubsub", 116 | "token_type": "bearer", 117 | "expires_in": 3600 118 | }`)) 119 | } 120 | })) 121 | defer ts.Close() 122 | 123 | conf := &oauth2.Config{ 124 | ClientID: "testClientID", 125 | Scopes: []string{"pubsub"}, 126 | Endpoint: oauth2.Endpoint{ 127 | AuthURL: "testAuthCodeURL", 128 | TokenURL: ts.URL, 129 | }, 130 | } 131 | pkce := PKCEParams{ 132 | Challenge: "codeChallenge", 133 | ChallengeMethod: "plain", 134 | Verifier: "codeChallenge", 135 | } 136 | 137 | tok, err := TokenSourceWithPKCE(context.Background(), conf, "testState", authhandler, &pkce).Token() 138 | if err != nil { 139 | t.Fatal(err) 140 | } 141 | if !tok.Valid() { 142 | t.Errorf("got invalid token: %v", tok) 143 | } 144 | if got, want := tok.AccessToken, "90d64460d14870c08c81352a05dedd3465940a7c"; got != want { 145 | t.Errorf("access token = %q; want %q", got, want) 146 | } 147 | if got, want := tok.TokenType, "bearer"; got != want { 148 | t.Errorf("token type = %q; want %q", got, want) 149 | } 150 | if got := tok.Expiry.IsZero(); got { 151 | t.Errorf("token expiry is zero = %v, want false", got) 152 | } 153 | scope := tok.Extra("scope") 154 | if got, want := scope, "pubsub"; got != want { 155 | t.Errorf("scope = %q; want %q", got, want) 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /bitbucket/bitbucket.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 bitbucket provides constants for using OAuth2 to access Bitbucket. 6 | package bitbucket 7 | 8 | import ( 9 | "golang.org/x/oauth2" 10 | ) 11 | 12 | // Endpoint is Bitbucket's OAuth 2.0 endpoint. 13 | var Endpoint = oauth2.Endpoint{ 14 | AuthURL: "https://bitbucket.org/site/oauth2/authorize", 15 | TokenURL: "https://bitbucket.org/site/oauth2/access_token", 16 | } 17 | -------------------------------------------------------------------------------- /cern/cern.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Go 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 cern provides constants for using OAuth2 to access CERN services. 6 | package cern // import "golang.org/x/oauth2/cern" 7 | 8 | import ( 9 | "golang.org/x/oauth2" 10 | ) 11 | 12 | // Endpoint is CERN's OAuth 2.0 endpoint. 13 | var Endpoint = oauth2.Endpoint{ 14 | AuthURL: "https://oauth.web.cern.ch/OAuth/Authorize", 15 | TokenURL: "https://oauth.web.cern.ch/OAuth/Token", 16 | } 17 | -------------------------------------------------------------------------------- /clientcredentials/clientcredentials.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Go 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 https://tools.ietf.org/html/rfc6749#section-4.4 14 | package clientcredentials // import "golang.org/x/oauth2/clientcredentials" 15 | 16 | import ( 17 | "context" 18 | "fmt" 19 | "net/http" 20 | "net/url" 21 | "strings" 22 | 23 | "golang.org/x/oauth2" 24 | "golang.org/x/oauth2/internal" 25 | ) 26 | 27 | // Config describes a 2-legged OAuth2 flow, with both the 28 | // client application information and the server's endpoint URLs. 29 | type Config struct { 30 | // ClientID is the application's ID. 31 | ClientID string 32 | 33 | // ClientSecret is the application's secret. 34 | ClientSecret string 35 | 36 | // TokenURL is the resource server's token endpoint 37 | // URL. This is a constant specific to each server. 38 | TokenURL string 39 | 40 | // Scopes specifies optional requested permissions. 41 | Scopes []string 42 | 43 | // EndpointParams specifies additional parameters for requests to the token endpoint. 44 | EndpointParams url.Values 45 | 46 | // AuthStyle optionally specifies how the endpoint wants the 47 | // client ID & client secret sent. The zero value means to 48 | // auto-detect. 49 | AuthStyle oauth2.AuthStyle 50 | 51 | // authStyleCache caches which auth style to use when Endpoint.AuthStyle is 52 | // the zero value (AuthStyleAutoDetect). 53 | authStyleCache internal.LazyAuthStyleCache 54 | } 55 | 56 | // Token uses client credentials to retrieve a token. 57 | // 58 | // The provided context optionally controls which HTTP client is used. See the [oauth2.HTTPClient] variable. 59 | func (c *Config) Token(ctx context.Context) (*oauth2.Token, error) { 60 | return c.TokenSource(ctx).Token() 61 | } 62 | 63 | // Client returns an HTTP client using the provided token. 64 | // The token will auto-refresh as necessary. 65 | // 66 | // The provided context optionally controls which HTTP client 67 | // is returned. See the [oauth2.HTTPClient] variable. 68 | // 69 | // The returned [http.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 | // TokenSource returns a [oauth2.TokenSource] that returns t until t expires, 75 | // automatically refreshing it as necessary using the provided context and the 76 | // client ID and client secret. 77 | // 78 | // Most users will use [Config.Client] instead. 79 | func (c *Config) TokenSource(ctx context.Context) oauth2.TokenSource { 80 | source := &tokenSource{ 81 | ctx: ctx, 82 | conf: c, 83 | } 84 | return oauth2.ReuseTokenSource(nil, source) 85 | } 86 | 87 | type tokenSource struct { 88 | ctx context.Context 89 | conf *Config 90 | } 91 | 92 | // Token refreshes the token by using a new client credentials request. 93 | // tokens received this way do not include a refresh token 94 | func (c *tokenSource) Token() (*oauth2.Token, error) { 95 | v := url.Values{ 96 | "grant_type": {"client_credentials"}, 97 | } 98 | if len(c.conf.Scopes) > 0 { 99 | v.Set("scope", strings.Join(c.conf.Scopes, " ")) 100 | } 101 | for k, p := range c.conf.EndpointParams { 102 | // Allow grant_type to be overridden to allow interoperability with 103 | // non-compliant implementations. 104 | if _, ok := v[k]; ok && k != "grant_type" { 105 | return nil, fmt.Errorf("oauth2: cannot overwrite parameter %q", k) 106 | } 107 | v[k] = p 108 | } 109 | 110 | tk, err := internal.RetrieveToken(c.ctx, c.conf.ClientID, c.conf.ClientSecret, c.conf.TokenURL, v, internal.AuthStyle(c.conf.AuthStyle), c.conf.authStyleCache.Get()) 111 | if err != nil { 112 | if rErr, ok := err.(*internal.RetrieveError); ok { 113 | return nil, (*oauth2.RetrieveError)(rErr) 114 | } 115 | return nil, err 116 | } 117 | t := &oauth2.Token{ 118 | AccessToken: tk.AccessToken, 119 | TokenType: tk.TokenType, 120 | RefreshToken: tk.RefreshToken, 121 | Expiry: tk.Expiry, 122 | } 123 | return t.WithExtra(tk.Raw), nil 124 | } 125 | -------------------------------------------------------------------------------- /clientcredentials/clientcredentials_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Go 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 | "context" 9 | "io" 10 | "net/http" 11 | "net/http/httptest" 12 | "net/url" 13 | "testing" 14 | ) 15 | 16 | func newConf(serverURL string) *Config { 17 | return &Config{ 18 | ClientID: "CLIENT_ID", 19 | ClientSecret: "CLIENT_SECRET", 20 | Scopes: []string{"scope1", "scope2"}, 21 | TokenURL: serverURL + "/token", 22 | EndpointParams: url.Values{"audience": {"audience1"}}, 23 | } 24 | } 25 | 26 | type mockTransport struct { 27 | rt func(req *http.Request) (resp *http.Response, err error) 28 | } 29 | 30 | func (t *mockTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) { 31 | return t.rt(req) 32 | } 33 | 34 | func TestTokenSourceGrantTypeOverride(t *testing.T) { 35 | wantGrantType := "password" 36 | var gotGrantType string 37 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 38 | body, err := io.ReadAll(r.Body) 39 | if err != nil { 40 | t.Errorf("io.ReadAll(r.Body) == %v, %v, want _, ", body, err) 41 | } 42 | if err := r.Body.Close(); err != nil { 43 | t.Errorf("r.Body.Close() == %v, want ", err) 44 | } 45 | values, err := url.ParseQuery(string(body)) 46 | if err != nil { 47 | t.Errorf("url.ParseQuery(%q) == %v, %v, want _, ", body, values, err) 48 | } 49 | gotGrantType = values.Get("grant_type") 50 | w.Header().Set("Content-Type", "application/x-www-form-urlencoded") 51 | w.Write([]byte("access_token=90d64460d14870c08c81352a05dedd3465940a7c&token_type=bearer")) 52 | })) 53 | config := &Config{ 54 | ClientID: "CLIENT_ID", 55 | ClientSecret: "CLIENT_SECRET", 56 | Scopes: []string{"scope"}, 57 | TokenURL: ts.URL + "/token", 58 | EndpointParams: url.Values{ 59 | "grant_type": {wantGrantType}, 60 | }, 61 | } 62 | token, err := config.TokenSource(context.Background()).Token() 63 | if err != nil { 64 | t.Errorf("config.TokenSource(_).Token() == %v, %v, want !, ", token, err) 65 | } 66 | if gotGrantType != wantGrantType { 67 | t.Errorf("grant_type == %q, want %q", gotGrantType, wantGrantType) 68 | } 69 | } 70 | 71 | func TestTokenRequest(t *testing.T) { 72 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 73 | if r.URL.String() != "/token" { 74 | t.Errorf("authenticate client request URL = %q; want %q", r.URL, "/token") 75 | } 76 | headerAuth := r.Header.Get("Authorization") 77 | if headerAuth != "Basic Q0xJRU5UX0lEOkNMSUVOVF9TRUNSRVQ=" { 78 | t.Errorf("Unexpected authorization header, %v is found.", headerAuth) 79 | } 80 | if got, want := r.Header.Get("Content-Type"), "application/x-www-form-urlencoded"; got != want { 81 | t.Errorf("Content-Type header = %q; want %q", got, want) 82 | } 83 | body, err := io.ReadAll(r.Body) 84 | if err != nil { 85 | r.Body.Close() 86 | } 87 | if err != nil { 88 | t.Errorf("failed reading request body: %s.", err) 89 | } 90 | if string(body) != "audience=audience1&grant_type=client_credentials&scope=scope1+scope2" { 91 | t.Errorf("payload = %q; want %q", string(body), "grant_type=client_credentials&scope=scope1+scope2") 92 | } 93 | w.Header().Set("Content-Type", "application/x-www-form-urlencoded") 94 | w.Write([]byte("access_token=90d64460d14870c08c81352a05dedd3465940a7c&token_type=bearer")) 95 | })) 96 | defer ts.Close() 97 | conf := newConf(ts.URL) 98 | tok, err := conf.Token(context.Background()) 99 | if err != nil { 100 | t.Error(err) 101 | } 102 | if !tok.Valid() { 103 | t.Fatalf("token invalid. got: %#v", tok) 104 | } 105 | if tok.AccessToken != "90d64460d14870c08c81352a05dedd3465940a7c" { 106 | t.Errorf("Access token = %q; want %q", tok.AccessToken, "90d64460d14870c08c81352a05dedd3465940a7c") 107 | } 108 | if tok.TokenType != "bearer" { 109 | t.Errorf("token type = %q; want %q", tok.TokenType, "bearer") 110 | } 111 | } 112 | 113 | func TestTokenRefreshRequest(t *testing.T) { 114 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 115 | if r.URL.String() == "/somethingelse" { 116 | return 117 | } 118 | if r.URL.String() != "/token" { 119 | t.Errorf("Unexpected token refresh request URL: %q", r.URL) 120 | } 121 | headerContentType := r.Header.Get("Content-Type") 122 | if got, want := headerContentType, "application/x-www-form-urlencoded"; got != want { 123 | t.Errorf("Content-Type = %q; want %q", got, want) 124 | } 125 | body, _ := io.ReadAll(r.Body) 126 | const want = "audience=audience1&grant_type=client_credentials&scope=scope1+scope2" 127 | if string(body) != want { 128 | t.Errorf("Unexpected refresh token payload.\n got: %s\nwant: %s\n", body, want) 129 | } 130 | w.Header().Set("Content-Type", "application/json") 131 | io.WriteString(w, `{"access_token": "foo", "refresh_token": "bar"}`) 132 | })) 133 | defer ts.Close() 134 | conf := newConf(ts.URL) 135 | c := conf.Client(context.Background()) 136 | c.Get(ts.URL + "/somethingelse") 137 | } 138 | -------------------------------------------------------------------------------- /deviceauth.go: -------------------------------------------------------------------------------- 1 | package oauth2 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | "net/url" 11 | "strings" 12 | "time" 13 | 14 | "golang.org/x/oauth2/internal" 15 | ) 16 | 17 | // https://datatracker.ietf.org/doc/html/rfc8628#section-3.5 18 | const ( 19 | errAuthorizationPending = "authorization_pending" 20 | errSlowDown = "slow_down" 21 | errAccessDenied = "access_denied" 22 | errExpiredToken = "expired_token" 23 | ) 24 | 25 | // DeviceAuthResponse describes a successful RFC 8628 Device Authorization Response 26 | // https://datatracker.ietf.org/doc/html/rfc8628#section-3.2 27 | type DeviceAuthResponse struct { 28 | // DeviceCode 29 | DeviceCode string `json:"device_code"` 30 | // UserCode is the code the user should enter at the verification uri 31 | UserCode string `json:"user_code"` 32 | // VerificationURI is where user should enter the user code 33 | VerificationURI string `json:"verification_uri"` 34 | // VerificationURIComplete (if populated) includes the user code in the verification URI. This is typically shown to the user in non-textual form, such as a QR code. 35 | VerificationURIComplete string `json:"verification_uri_complete,omitempty"` 36 | // Expiry is when the device code and user code expire 37 | Expiry time.Time `json:"expires_in,omitempty"` 38 | // Interval is the duration in seconds that Poll should wait between requests 39 | Interval int64 `json:"interval,omitempty"` 40 | } 41 | 42 | func (d DeviceAuthResponse) MarshalJSON() ([]byte, error) { 43 | type Alias DeviceAuthResponse 44 | var expiresIn int64 45 | if !d.Expiry.IsZero() { 46 | expiresIn = int64(time.Until(d.Expiry).Seconds()) 47 | } 48 | return json.Marshal(&struct { 49 | ExpiresIn int64 `json:"expires_in,omitempty"` 50 | *Alias 51 | }{ 52 | ExpiresIn: expiresIn, 53 | Alias: (*Alias)(&d), 54 | }) 55 | 56 | } 57 | 58 | func (c *DeviceAuthResponse) UnmarshalJSON(data []byte) error { 59 | type Alias DeviceAuthResponse 60 | aux := &struct { 61 | ExpiresIn int64 `json:"expires_in"` 62 | // workaround misspelling of verification_uri 63 | VerificationURL string `json:"verification_url"` 64 | *Alias 65 | }{ 66 | Alias: (*Alias)(c), 67 | } 68 | if err := json.Unmarshal(data, &aux); err != nil { 69 | return err 70 | } 71 | if aux.ExpiresIn != 0 { 72 | c.Expiry = time.Now().UTC().Add(time.Second * time.Duration(aux.ExpiresIn)) 73 | } 74 | if c.VerificationURI == "" { 75 | c.VerificationURI = aux.VerificationURL 76 | } 77 | return nil 78 | } 79 | 80 | // DeviceAuth returns a device auth struct which contains a device code 81 | // and authorization information provided for users to enter on another device. 82 | func (c *Config) DeviceAuth(ctx context.Context, opts ...AuthCodeOption) (*DeviceAuthResponse, error) { 83 | // https://datatracker.ietf.org/doc/html/rfc8628#section-3.1 84 | v := url.Values{ 85 | "client_id": {c.ClientID}, 86 | } 87 | if len(c.Scopes) > 0 { 88 | v.Set("scope", strings.Join(c.Scopes, " ")) 89 | } 90 | for _, opt := range opts { 91 | opt.setValue(v) 92 | } 93 | return retrieveDeviceAuth(ctx, c, v) 94 | } 95 | 96 | func retrieveDeviceAuth(ctx context.Context, c *Config, v url.Values) (*DeviceAuthResponse, error) { 97 | if c.Endpoint.DeviceAuthURL == "" { 98 | return nil, errors.New("endpoint missing DeviceAuthURL") 99 | } 100 | 101 | req, err := http.NewRequest("POST", c.Endpoint.DeviceAuthURL, strings.NewReader(v.Encode())) 102 | if err != nil { 103 | return nil, err 104 | } 105 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 106 | req.Header.Set("Accept", "application/json") 107 | 108 | t := time.Now() 109 | r, err := internal.ContextClient(ctx).Do(req) 110 | if err != nil { 111 | return nil, err 112 | } 113 | 114 | body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20)) 115 | if err != nil { 116 | return nil, fmt.Errorf("oauth2: cannot auth device: %v", err) 117 | } 118 | if code := r.StatusCode; code < 200 || code > 299 { 119 | return nil, &RetrieveError{ 120 | Response: r, 121 | Body: body, 122 | } 123 | } 124 | 125 | da := &DeviceAuthResponse{} 126 | err = json.Unmarshal(body, &da) 127 | if err != nil { 128 | return nil, fmt.Errorf("unmarshal %s", err) 129 | } 130 | 131 | if !da.Expiry.IsZero() { 132 | // Make a small adjustment to account for time taken by the request 133 | da.Expiry = da.Expiry.Add(-time.Since(t)) 134 | } 135 | 136 | return da, nil 137 | } 138 | 139 | // DeviceAccessToken polls the server to exchange a device code for a token. 140 | func (c *Config) DeviceAccessToken(ctx context.Context, da *DeviceAuthResponse, opts ...AuthCodeOption) (*Token, error) { 141 | if !da.Expiry.IsZero() { 142 | var cancel context.CancelFunc 143 | ctx, cancel = context.WithDeadline(ctx, da.Expiry) 144 | defer cancel() 145 | } 146 | 147 | // https://datatracker.ietf.org/doc/html/rfc8628#section-3.4 148 | v := url.Values{ 149 | "client_id": {c.ClientID}, 150 | "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, 151 | "device_code": {da.DeviceCode}, 152 | } 153 | if len(c.Scopes) > 0 { 154 | v.Set("scope", strings.Join(c.Scopes, " ")) 155 | } 156 | for _, opt := range opts { 157 | opt.setValue(v) 158 | } 159 | 160 | // "If no value is provided, clients MUST use 5 as the default." 161 | // https://datatracker.ietf.org/doc/html/rfc8628#section-3.2 162 | interval := da.Interval 163 | if interval == 0 { 164 | interval = 5 165 | } 166 | 167 | ticker := time.NewTicker(time.Duration(interval) * time.Second) 168 | defer ticker.Stop() 169 | for { 170 | select { 171 | case <-ctx.Done(): 172 | return nil, ctx.Err() 173 | case <-ticker.C: 174 | tok, err := retrieveToken(ctx, c, v) 175 | if err == nil { 176 | return tok, nil 177 | } 178 | 179 | e, ok := err.(*RetrieveError) 180 | if !ok { 181 | return nil, err 182 | } 183 | switch e.ErrorCode { 184 | case errSlowDown: 185 | // https://datatracker.ietf.org/doc/html/rfc8628#section-3.5 186 | // "the interval MUST be increased by 5 seconds for this and all subsequent requests" 187 | interval += 5 188 | ticker.Reset(time.Duration(interval) * time.Second) 189 | case errAuthorizationPending: 190 | // Do nothing. 191 | case errAccessDenied, errExpiredToken: 192 | fallthrough 193 | default: 194 | return tok, err 195 | } 196 | } 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /deviceauth_test.go: -------------------------------------------------------------------------------- 1 | package oauth2 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "strings" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | func TestDeviceAuthResponseMarshalJson(t *testing.T) { 13 | tests := []struct { 14 | name string 15 | response DeviceAuthResponse 16 | want string 17 | }{ 18 | { 19 | name: "empty", 20 | response: DeviceAuthResponse{}, 21 | want: `{"device_code":"","user_code":"","verification_uri":""}`, 22 | }, 23 | { 24 | name: "soon", 25 | response: DeviceAuthResponse{ 26 | Expiry: time.Now().Add(100*time.Second + 999*time.Millisecond), 27 | }, 28 | want: `{"expires_in":100,"device_code":"","user_code":"","verification_uri":""}`, 29 | }, 30 | } 31 | for _, tc := range tests { 32 | t.Run(tc.name, func(t *testing.T) { 33 | begin := time.Now() 34 | gotBytes, err := json.Marshal(tc.response) 35 | if err != nil { 36 | t.Fatal(err) 37 | } 38 | if strings.Contains(tc.want, "expires_in") && time.Since(begin) > 999*time.Millisecond { 39 | t.Skip("test ran too slowly to compare `expires_in`") 40 | } 41 | got := string(gotBytes) 42 | if got != tc.want { 43 | t.Errorf("want=%s, got=%s", tc.want, got) 44 | } 45 | }) 46 | } 47 | } 48 | 49 | func TestDeviceAuthResponseUnmarshalJson(t *testing.T) { 50 | tests := []struct { 51 | name string 52 | data string 53 | want DeviceAuthResponse 54 | }{ 55 | { 56 | name: "empty", 57 | data: `{}`, 58 | want: DeviceAuthResponse{}, 59 | }, 60 | { 61 | name: "soon", 62 | data: `{"expires_in":100}`, 63 | want: DeviceAuthResponse{Expiry: time.Now().UTC().Add(100 * time.Second)}, 64 | }, 65 | } 66 | for _, tc := range tests { 67 | t.Run(tc.name, func(t *testing.T) { 68 | begin := time.Now() 69 | got := DeviceAuthResponse{} 70 | err := json.Unmarshal([]byte(tc.data), &got) 71 | if err != nil { 72 | t.Fatal(err) 73 | } 74 | margin := time.Second + time.Since(begin) 75 | timeDiff := got.Expiry.Sub(tc.want.Expiry) 76 | if timeDiff < 0 { 77 | timeDiff *= -1 78 | } 79 | if timeDiff > margin { 80 | t.Errorf("expiry time difference too large, got=%v, want=%v margin=%v", got.Expiry, tc.want.Expiry, margin) 81 | } 82 | got.Expiry, tc.want.Expiry = time.Time{}, time.Time{} 83 | if got != tc.want { 84 | t.Errorf("want=%#v, got=%#v", tc.want, got) 85 | } 86 | }) 87 | } 88 | } 89 | 90 | func ExampleConfig_DeviceAuth() { 91 | var config Config 92 | ctx := context.Background() 93 | response, err := config.DeviceAuth(ctx) 94 | if err != nil { 95 | panic(err) 96 | } 97 | fmt.Printf("please enter code %s at %s\n", response.UserCode, response.VerificationURI) 98 | token, err := config.DeviceAccessToken(ctx, response) 99 | if err != nil { 100 | panic(err) 101 | } 102 | fmt.Println(token) 103 | } 104 | -------------------------------------------------------------------------------- /endpoints/endpoints_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Go 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 endpoints 6 | 7 | import ( 8 | "testing" 9 | 10 | "golang.org/x/oauth2" 11 | ) 12 | 13 | func TestAWSCognitoEndpoint(t *testing.T) { 14 | 15 | var endpointTests = []struct { 16 | in string 17 | out oauth2.Endpoint 18 | }{ 19 | { 20 | in: "https://testing.auth.us-east-1.amazoncognito.com", 21 | out: oauth2.Endpoint{ 22 | AuthURL: "https://testing.auth.us-east-1.amazoncognito.com/oauth2/authorize", 23 | TokenURL: "https://testing.auth.us-east-1.amazoncognito.com/oauth2/token", 24 | }, 25 | }, 26 | { 27 | in: "https://testing.auth.us-east-1.amazoncognito.com/", 28 | out: oauth2.Endpoint{ 29 | AuthURL: "https://testing.auth.us-east-1.amazoncognito.com/oauth2/authorize", 30 | TokenURL: "https://testing.auth.us-east-1.amazoncognito.com/oauth2/token", 31 | }, 32 | }, 33 | } 34 | 35 | for _, tt := range endpointTests { 36 | t.Run(tt.in, func(t *testing.T) { 37 | endpoint := AWSCognito(tt.in) 38 | if endpoint != tt.out { 39 | t.Errorf("got %q, want %q", endpoint, tt.out) 40 | } 41 | }) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Go 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 | "context" 9 | "fmt" 10 | "log" 11 | "net/http" 12 | "time" 13 | 14 | "golang.org/x/oauth2" 15 | ) 16 | 17 | func ExampleConfig() { 18 | ctx := context.Background() 19 | conf := &oauth2.Config{ 20 | ClientID: "YOUR_CLIENT_ID", 21 | ClientSecret: "YOUR_CLIENT_SECRET", 22 | Scopes: []string{"SCOPE1", "SCOPE2"}, 23 | Endpoint: oauth2.Endpoint{ 24 | AuthURL: "https://provider.com/o/oauth2/auth", 25 | TokenURL: "https://provider.com/o/oauth2/token", 26 | }, 27 | } 28 | 29 | // use PKCE to protect against CSRF attacks 30 | // https://www.ietf.org/archive/id/draft-ietf-oauth-security-topics-22.html#name-countermeasures-6 31 | verifier := oauth2.GenerateVerifier() 32 | 33 | // Redirect user to consent page to ask for permission 34 | // for the scopes specified above. 35 | url := conf.AuthCodeURL("state", oauth2.AccessTypeOffline, oauth2.S256ChallengeOption(verifier)) 36 | fmt.Printf("Visit the URL for the auth dialog: %v", url) 37 | 38 | // Use the authorization code that is pushed to the redirect 39 | // URL. Exchange will do the handshake to retrieve the 40 | // initial access token. The HTTP Client returned by 41 | // conf.Client will refresh the token as necessary. 42 | var code string 43 | if _, err := fmt.Scan(&code); err != nil { 44 | log.Fatal(err) 45 | } 46 | tok, err := conf.Exchange(ctx, code, oauth2.VerifierOption(verifier)) 47 | if err != nil { 48 | log.Fatal(err) 49 | } 50 | 51 | client := conf.Client(ctx, tok) 52 | client.Get("...") 53 | } 54 | 55 | func ExampleConfig_customHTTP() { 56 | ctx := context.Background() 57 | 58 | conf := &oauth2.Config{ 59 | ClientID: "YOUR_CLIENT_ID", 60 | ClientSecret: "YOUR_CLIENT_SECRET", 61 | Scopes: []string{"SCOPE1", "SCOPE2"}, 62 | Endpoint: oauth2.Endpoint{ 63 | TokenURL: "https://provider.com/o/oauth2/token", 64 | AuthURL: "https://provider.com/o/oauth2/auth", 65 | }, 66 | } 67 | 68 | // Redirect user to consent page to ask for permission 69 | // for the scopes specified above. 70 | url := conf.AuthCodeURL("state", oauth2.AccessTypeOffline) 71 | fmt.Printf("Visit the URL for the auth dialog: %v", url) 72 | 73 | // Use the authorization code that is pushed to the redirect 74 | // URL. Exchange will do the handshake to retrieve the 75 | // initial access token. The HTTP Client returned by 76 | // conf.Client will refresh the token as necessary. 77 | var code string 78 | if _, err := fmt.Scan(&code); err != nil { 79 | log.Fatal(err) 80 | } 81 | 82 | // Use the custom HTTP client when requesting a token. 83 | httpClient := &http.Client{Timeout: 2 * time.Second} 84 | ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient) 85 | 86 | tok, err := conf.Exchange(ctx, code) 87 | if err != nil { 88 | log.Fatal(err) 89 | } 90 | 91 | client := conf.Client(ctx, tok) 92 | _ = client 93 | } 94 | -------------------------------------------------------------------------------- /facebook/facebook.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Go 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 "golang.org/x/oauth2/facebook" 7 | 8 | import ( 9 | "golang.org/x/oauth2" 10 | ) 11 | 12 | // Endpoint is Facebook's OAuth 2.0 endpoint. 13 | var Endpoint = oauth2.Endpoint{ 14 | AuthURL: "https://www.facebook.com/v3.2/dialog/oauth", 15 | TokenURL: "https://graph.facebook.com/v3.2/oauth/access_token", 16 | } 17 | -------------------------------------------------------------------------------- /fitbit/fitbit.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Go 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 fitbit provides constants for using OAuth2 to access the Fitbit API. 6 | package fitbit // import "golang.org/x/oauth2/fitbit" 7 | 8 | import ( 9 | "golang.org/x/oauth2" 10 | ) 11 | 12 | // Endpoint is the Fitbit API's OAuth 2.0 endpoint. 13 | var Endpoint = oauth2.Endpoint{ 14 | AuthURL: "https://www.fitbit.com/oauth2/authorize", 15 | TokenURL: "https://api.fitbit.com/oauth2/token", 16 | } 17 | -------------------------------------------------------------------------------- /foursquare/foursquare.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Go 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 foursquare provides constants for using OAuth2 to access Foursquare. 6 | package foursquare // import "golang.org/x/oauth2/foursquare" 7 | 8 | import ( 9 | "golang.org/x/oauth2" 10 | ) 11 | 12 | // Endpoint is Foursquare's OAuth 2.0 endpoint. 13 | var Endpoint = oauth2.Endpoint{ 14 | AuthURL: "https://foursquare.com/oauth2/authorize", 15 | TokenURL: "https://foursquare.com/oauth2/access_token", 16 | } 17 | -------------------------------------------------------------------------------- /github/github.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Go 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 "golang.org/x/oauth2/github" 7 | 8 | import ( 9 | "golang.org/x/oauth2/endpoints" 10 | ) 11 | 12 | // Endpoint is Github's OAuth 2.0 endpoint. 13 | var Endpoint = endpoints.GitHub 14 | -------------------------------------------------------------------------------- /gitlab/gitlab.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Go 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 gitlab provides constants for using OAuth2 to access GitLab. 6 | package gitlab // import "golang.org/x/oauth2/gitlab" 7 | 8 | import ( 9 | "golang.org/x/oauth2/endpoints" 10 | ) 11 | 12 | // Endpoint is GitLab's OAuth 2.0 endpoint. 13 | var Endpoint = endpoints.GitLab 14 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module golang.org/x/oauth2 2 | 3 | go 1.23.0 4 | 5 | require cloud.google.com/go/compute/metadata v0.3.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= 2 | cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= 3 | -------------------------------------------------------------------------------- /google/appengine.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Go 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 | "context" 9 | "log" 10 | "sync" 11 | 12 | "golang.org/x/oauth2" 13 | ) 14 | 15 | var logOnce sync.Once // only spam about deprecation once 16 | 17 | // AppEngineTokenSource returns a token source that fetches tokens from either 18 | // the current application's service account or from the metadata server, 19 | // depending on the App Engine environment. See below for environment-specific 20 | // details. If you are implementing a 3-legged OAuth 2.0 flow on App Engine that 21 | // involves user accounts, see oauth2.Config instead. 22 | // 23 | // The current version of this library requires at least Go 1.17 to build, 24 | // so first generation App Engine runtimes (<= Go 1.9) are unsupported. 25 | // Previously, on first generation App Engine runtimes, AppEngineTokenSource 26 | // returned a token source that fetches tokens issued to the 27 | // current App Engine application's service account. The provided context must have 28 | // come from appengine.NewContext. 29 | // 30 | // Second generation App Engine runtimes (>= Go 1.11) and App Engine flexible: 31 | // AppEngineTokenSource is DEPRECATED on second generation runtimes and on the 32 | // flexible environment. It delegates to ComputeTokenSource, and the provided 33 | // context and scopes are not used. Please use DefaultTokenSource (or ComputeTokenSource, 34 | // which DefaultTokenSource will use in this case) instead. 35 | func AppEngineTokenSource(ctx context.Context, scope ...string) oauth2.TokenSource { 36 | logOnce.Do(func() { 37 | log.Print("google: AppEngineTokenSource is deprecated on App Engine standard second generation runtimes (>= Go 1.11) and App Engine flexible. Please use DefaultTokenSource or ComputeTokenSource.") 38 | }) 39 | return ComputeTokenSource("") 40 | } 41 | -------------------------------------------------------------------------------- /google/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Go 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 authenticated 6 | // HTTP requests to Google APIs. It supports the Web server flow, client-side 7 | // credentials, service accounts, Google Compute Engine service accounts, 8 | // Google App Engine service accounts and workload identity federation 9 | // from non-Google cloud platforms. 10 | // 11 | // A brief overview of the package follows. 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 | // For more information on using workload identity federation, refer to 16 | // https://cloud.google.com/iam/docs/how-to#using-workload-identity-federation. 17 | // 18 | // # OAuth2 Configs 19 | // 20 | // Two functions in this package return golang.org/x/oauth2.Config values from Google credential 21 | // data. Google supports two JSON formats for OAuth2 credentials: one is handled by ConfigFromJSON, 22 | // the other by JWTConfigFromJSON. The returned Config can be used to obtain a TokenSource or 23 | // create an http.Client. 24 | // 25 | // # Workload and Workforce Identity Federation 26 | // 27 | // For information on how to use Workload and Workforce Identity Federation, see [golang.org/x/oauth2/google/externalaccount]. 28 | // 29 | // # Credentials 30 | // 31 | // The Credentials type represents Google credentials, including Application Default 32 | // Credentials. 33 | // 34 | // Use FindDefaultCredentials to obtain Application Default Credentials. 35 | // FindDefaultCredentials looks in some well-known places for a credentials file, and 36 | // will call AppEngineTokenSource or ComputeTokenSource as needed. 37 | // 38 | // Application Default Credentials also support workload identity federation to 39 | // access Google Cloud resources from non-Google Cloud platforms including Amazon 40 | // Web Services (AWS), Microsoft Azure or any identity provider that supports 41 | // OpenID Connect (OIDC). Workload identity federation is recommended for 42 | // non-Google Cloud environments as it avoids the need to download, manage and 43 | // store service account private keys locally. 44 | // 45 | // DefaultClient and DefaultTokenSource are convenience methods. They first call FindDefaultCredentials, 46 | // then use the credentials to construct an http.Client or an oauth2.TokenSource. 47 | // 48 | // Use CredentialsFromJSON to obtain credentials from either of the two JSON formats 49 | // described in OAuth2 Configs, above. The TokenSource in the returned value is the 50 | // same as the one obtained from the oauth2.Config returned from ConfigFromJSON or 51 | // JWTConfigFromJSON, but the Credentials may contain additional information 52 | // that is useful is some circumstances. 53 | package google // import "golang.org/x/oauth2/google" 54 | -------------------------------------------------------------------------------- /google/downscope/downscoping.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The Go 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 | /* 6 | Package downscope implements the ability to downscope, or restrict, the 7 | Identity and Access Management permissions that a short-lived Token 8 | can use. Please note that only Google Cloud Storage supports this feature. 9 | For complete documentation, see https://cloud.google.com/iam/docs/downscoping-short-lived-credentials 10 | 11 | To downscope permissions of a source credential, you need to define 12 | a Credential Access Boundary. Said Boundary specifies which resources 13 | the newly created credential can access, an upper bound on the permissions 14 | it has over those resources, and optionally attribute-based conditional 15 | access to the aforementioned resources. For more information on IAM 16 | Conditions, see https://cloud.google.com/iam/docs/conditions-overview. 17 | 18 | This functionality can be used to provide a third party with 19 | limited access to and permissions on resources held by the owner of the root 20 | credential or internally in conjunction with the principle of least privilege 21 | to ensure that internal services only hold the minimum necessary privileges 22 | for their function. 23 | 24 | For example, a token broker can be set up on a server in a private network. 25 | Various workloads (token consumers) in the same network will send authenticated 26 | requests to that broker for downscoped tokens to access or modify specific google 27 | cloud storage buckets. See the NewTokenSource example for an example of how a 28 | token broker would use this package. 29 | 30 | The broker will use the functionality in this package to generate a downscoped 31 | token with the requested configuration, and then pass it back to the token 32 | consumer. These downscoped access tokens can then be used to access Google 33 | Storage resources. For instance, you can create a NewClient from the 34 | "cloud.google.com/go/storage" package and pass in option.WithTokenSource(yourTokenSource)) 35 | */ 36 | package downscope 37 | 38 | import ( 39 | "context" 40 | "encoding/json" 41 | "fmt" 42 | "io" 43 | "net/http" 44 | "net/url" 45 | "strings" 46 | "time" 47 | 48 | "golang.org/x/oauth2" 49 | ) 50 | 51 | const ( 52 | universeDomainPlaceholder = "UNIVERSE_DOMAIN" 53 | identityBindingEndpointTemplate = "https://sts.UNIVERSE_DOMAIN/v1/token" 54 | defaultUniverseDomain = "googleapis.com" 55 | ) 56 | 57 | type accessBoundary struct { 58 | AccessBoundaryRules []AccessBoundaryRule `json:"accessBoundaryRules"` 59 | } 60 | 61 | // An AvailabilityCondition restricts access to a given Resource. 62 | type AvailabilityCondition struct { 63 | // An Expression specifies the Cloud Storage objects where 64 | // permissions are available. For further documentation, see 65 | // https://cloud.google.com/iam/docs/conditions-overview 66 | Expression string `json:"expression"` 67 | // Title is short string that identifies the purpose of the condition. Optional. 68 | Title string `json:"title,omitempty"` 69 | // Description details about the purpose of the condition. Optional. 70 | Description string `json:"description,omitempty"` 71 | } 72 | 73 | // An AccessBoundaryRule Sets the permissions (and optionally conditions) 74 | // that the new token has on given resource. 75 | type AccessBoundaryRule struct { 76 | // AvailableResource is the full resource name of the Cloud Storage bucket that the rule applies to. 77 | // Use the format //storage.googleapis.com/projects/_/buckets/bucket-name. 78 | AvailableResource string `json:"availableResource"` 79 | // AvailablePermissions is a list that defines the upper bound on the available permissions 80 | // for the resource. Each value is the identifier for an IAM predefined role or custom role, 81 | // with the prefix inRole:. For example: inRole:roles/storage.objectViewer. 82 | // Only the permissions in these roles will be available. 83 | AvailablePermissions []string `json:"availablePermissions"` 84 | // An Condition restricts the availability of permissions 85 | // to specific Cloud Storage objects. Optional. 86 | // 87 | // A Condition can be used to make permissions available for specific objects, 88 | // rather than all objects in a Cloud Storage bucket. 89 | Condition *AvailabilityCondition `json:"availabilityCondition,omitempty"` 90 | } 91 | 92 | type downscopedTokenResponse struct { 93 | AccessToken string `json:"access_token"` 94 | IssuedTokenType string `json:"issued_token_type"` 95 | TokenType string `json:"token_type"` 96 | ExpiresIn int `json:"expires_in"` 97 | } 98 | 99 | // DownscopingConfig specifies the information necessary to request a downscoped token. 100 | type DownscopingConfig struct { 101 | // RootSource is the TokenSource used to create the downscoped token. 102 | // The downscoped token therefore has some subset of the accesses of 103 | // the original RootSource. 104 | RootSource oauth2.TokenSource 105 | // Rules defines the accesses held by the new 106 | // downscoped Token. One or more AccessBoundaryRules are required to 107 | // define permissions for the new downscoped token. Each one defines an 108 | // access (or set of accesses) that the new token has to a given resource. 109 | // There can be a maximum of 10 AccessBoundaryRules. 110 | Rules []AccessBoundaryRule 111 | // UniverseDomain is the default service domain for a given Cloud universe. 112 | // The default value is "googleapis.com". Optional. 113 | UniverseDomain string 114 | } 115 | 116 | // identityBindingEndpoint returns the identity binding endpoint with the 117 | // configured universe domain. 118 | func (dc *DownscopingConfig) identityBindingEndpoint() string { 119 | if dc.UniverseDomain == "" { 120 | return strings.Replace(identityBindingEndpointTemplate, universeDomainPlaceholder, defaultUniverseDomain, 1) 121 | } 122 | return strings.Replace(identityBindingEndpointTemplate, universeDomainPlaceholder, dc.UniverseDomain, 1) 123 | } 124 | 125 | // A downscopingTokenSource is used to retrieve a downscoped token with restricted 126 | // permissions compared to the root Token that is used to generate it. 127 | type downscopingTokenSource struct { 128 | // ctx is the context used to query the API to retrieve a downscoped Token. 129 | ctx context.Context 130 | // config holds the information necessary to generate a downscoped Token. 131 | config DownscopingConfig 132 | // identityBindingEndpoint is the identity binding endpoint with the 133 | // configured universe domain. 134 | identityBindingEndpoint string 135 | } 136 | 137 | // NewTokenSource returns a configured downscopingTokenSource. 138 | func NewTokenSource(ctx context.Context, conf DownscopingConfig) (oauth2.TokenSource, error) { 139 | if conf.RootSource == nil { 140 | return nil, fmt.Errorf("downscope: rootSource cannot be nil") 141 | } 142 | if len(conf.Rules) == 0 { 143 | return nil, fmt.Errorf("downscope: length of AccessBoundaryRules must be at least 1") 144 | } 145 | if len(conf.Rules) > 10 { 146 | return nil, fmt.Errorf("downscope: length of AccessBoundaryRules may not be greater than 10") 147 | } 148 | for _, val := range conf.Rules { 149 | if val.AvailableResource == "" { 150 | return nil, fmt.Errorf("downscope: all rules must have a nonempty AvailableResource: %+v", val) 151 | } 152 | if len(val.AvailablePermissions) == 0 { 153 | return nil, fmt.Errorf("downscope: all rules must provide at least one permission: %+v", val) 154 | } 155 | } 156 | return downscopingTokenSource{ 157 | ctx: ctx, 158 | config: conf, 159 | identityBindingEndpoint: conf.identityBindingEndpoint(), 160 | }, nil 161 | } 162 | 163 | // Token() uses a downscopingTokenSource to generate an oauth2 Token. 164 | // Do note that the returned TokenSource is an oauth2.StaticTokenSource. If you wish 165 | // to refresh this token automatically, then initialize a locally defined 166 | // TokenSource struct with the Token held by the StaticTokenSource and wrap 167 | // that TokenSource in an oauth2.ReuseTokenSource. 168 | func (dts downscopingTokenSource) Token() (*oauth2.Token, error) { 169 | 170 | downscopedOptions := struct { 171 | Boundary accessBoundary `json:"accessBoundary"` 172 | }{ 173 | Boundary: accessBoundary{ 174 | AccessBoundaryRules: dts.config.Rules, 175 | }, 176 | } 177 | 178 | tok, err := dts.config.RootSource.Token() 179 | if err != nil { 180 | return nil, fmt.Errorf("downscope: unable to obtain root token: %v", err) 181 | } 182 | 183 | b, err := json.Marshal(downscopedOptions) 184 | if err != nil { 185 | return nil, fmt.Errorf("downscope: unable to marshal AccessBoundary payload %v", err) 186 | } 187 | 188 | form := url.Values{} 189 | form.Add("grant_type", "urn:ietf:params:oauth:grant-type:token-exchange") 190 | form.Add("subject_token_type", "urn:ietf:params:oauth:token-type:access_token") 191 | form.Add("requested_token_type", "urn:ietf:params:oauth:token-type:access_token") 192 | form.Add("subject_token", tok.AccessToken) 193 | form.Add("options", string(b)) 194 | 195 | myClient := oauth2.NewClient(dts.ctx, nil) 196 | resp, err := myClient.PostForm(dts.identityBindingEndpoint, form) 197 | if err != nil { 198 | return nil, fmt.Errorf("unable to generate POST Request %v", err) 199 | } 200 | defer resp.Body.Close() 201 | respBody, err := io.ReadAll(resp.Body) 202 | if err != nil { 203 | return nil, fmt.Errorf("downscope: unable to read response body: %v", err) 204 | } 205 | if resp.StatusCode != http.StatusOK { 206 | return nil, fmt.Errorf("downscope: unable to exchange token; %v. Server responded: %s", resp.StatusCode, respBody) 207 | } 208 | 209 | var tresp downscopedTokenResponse 210 | 211 | err = json.Unmarshal(respBody, &tresp) 212 | if err != nil { 213 | return nil, fmt.Errorf("downscope: unable to unmarshal response body: %v", err) 214 | } 215 | 216 | // an exchanged token that is derived from a service account (2LO) has an expired_in value 217 | // a token derived from a users token (3LO) does not. 218 | // The following code uses the time remaining on rootToken for a user as the value for the 219 | // derived token's lifetime 220 | var expiryTime time.Time 221 | if tresp.ExpiresIn > 0 { 222 | expiryTime = time.Now().Add(time.Duration(tresp.ExpiresIn) * time.Second) 223 | } else { 224 | expiryTime = tok.Expiry 225 | } 226 | 227 | newToken := &oauth2.Token{ 228 | AccessToken: tresp.AccessToken, 229 | TokenType: tresp.TokenType, 230 | Expiry: expiryTime, 231 | } 232 | return newToken, nil 233 | } 234 | -------------------------------------------------------------------------------- /google/downscope/downscoping_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The Go 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 downscope 6 | 7 | import ( 8 | "context" 9 | "io" 10 | "net/http" 11 | "net/http/httptest" 12 | "testing" 13 | 14 | "golang.org/x/oauth2" 15 | ) 16 | 17 | var ( 18 | standardReqBody = "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&options=%7B%22accessBoundary%22%3A%7B%22accessBoundaryRules%22%3A%5B%7B%22availableResource%22%3A%22test1%22%2C%22availablePermissions%22%3A%5B%22Perm1%22%2C%22Perm2%22%5D%7D%5D%7D%7D&requested_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token&subject_token=Mellon&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token" 19 | standardRespBody = `{"access_token":"Open Sesame","expires_in":432,"issued_token_type":"urn:ietf:params:oauth:token-type:access_token","token_type":"Bearer"}` 20 | ) 21 | 22 | func Test_DownscopedTokenSource(t *testing.T) { 23 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 24 | if r.Method != "POST" { 25 | t.Errorf("Unexpected request method, %v is found", r.Method) 26 | } 27 | if r.URL.String() != "/" { 28 | t.Errorf("Unexpected request URL, %v is found", r.URL) 29 | } 30 | body, err := io.ReadAll(r.Body) 31 | if err != nil { 32 | t.Fatalf("Failed to read request body: %v", err) 33 | } 34 | if got, want := string(body), standardReqBody; got != want { 35 | t.Errorf("Unexpected exchange payload: got %v but want %v,", got, want) 36 | } 37 | w.Header().Set("Content-Type", "application/json") 38 | w.Write([]byte(standardRespBody)) 39 | 40 | })) 41 | myTok := oauth2.Token{AccessToken: "Mellon"} 42 | tmpSrc := oauth2.StaticTokenSource(&myTok) 43 | rules := []AccessBoundaryRule{ 44 | { 45 | AvailableResource: "test1", 46 | AvailablePermissions: []string{"Perm1", "Perm2"}, 47 | }, 48 | } 49 | dts := downscopingTokenSource{ 50 | ctx: context.Background(), 51 | config: DownscopingConfig{ 52 | RootSource: tmpSrc, 53 | Rules: rules, 54 | }, 55 | identityBindingEndpoint: ts.URL, 56 | } 57 | _, err := dts.Token() 58 | if err != nil { 59 | t.Fatalf("NewDownscopedTokenSource failed with error: %v", err) 60 | } 61 | } 62 | 63 | func Test_DownscopingConfig(t *testing.T) { 64 | tests := []struct { 65 | universeDomain string 66 | want string 67 | }{ 68 | {"", "https://sts.googleapis.com/v1/token"}, 69 | {"googleapis.com", "https://sts.googleapis.com/v1/token"}, 70 | {"example.com", "https://sts.example.com/v1/token"}, 71 | } 72 | for _, tt := range tests { 73 | c := DownscopingConfig{ 74 | UniverseDomain: tt.universeDomain, 75 | } 76 | if got := c.identityBindingEndpoint(); got != tt.want { 77 | t.Errorf("got %q, want %q", got, tt.want) 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /google/downscope/tokenbroker_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The Go 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 downscope_test 6 | 7 | import ( 8 | "context" 9 | "fmt" 10 | 11 | "golang.org/x/oauth2/google" 12 | 13 | "golang.org/x/oauth2" 14 | "golang.org/x/oauth2/google/downscope" 15 | ) 16 | 17 | func ExampleNewTokenSource() { 18 | // This shows how to generate a downscoped token. This code would be run on the 19 | // token broker, which holds the root token used to generate the downscoped token. 20 | ctx := context.Background() 21 | // Initializes an accessBoundary with one Rule which restricts the downscoped 22 | // token to only be able to access the bucket "foo" and only grants it the 23 | // permission "storage.objectViewer". 24 | accessBoundary := []downscope.AccessBoundaryRule{ 25 | { 26 | AvailableResource: "//storage.googleapis.com/projects/_/buckets/foo", 27 | AvailablePermissions: []string{"inRole:roles/storage.objectViewer"}, 28 | }, 29 | } 30 | 31 | var rootSource oauth2.TokenSource 32 | // This Source can be initialized in multiple ways; the following example uses 33 | // Application Default Credentials. 34 | 35 | rootSource, err := google.DefaultTokenSource(ctx, "https://www.googleapis.com/auth/cloud-platform") 36 | 37 | dts, err := downscope.NewTokenSource(ctx, downscope.DownscopingConfig{RootSource: rootSource, Rules: accessBoundary}) 38 | if err != nil { 39 | fmt.Printf("failed to generate downscoped token source: %v", err) 40 | return 41 | } 42 | 43 | tok, err := dts.Token() 44 | if err != nil { 45 | fmt.Printf("failed to generate token: %v", err) 46 | return 47 | } 48 | _ = tok 49 | // You can now pass tok to a token consumer however you wish, such as exposing 50 | // a REST API and sending it over HTTP. 51 | 52 | // You can instead use the token held in dts to make 53 | // Google Cloud Storage calls, as follows: 54 | 55 | // storageClient, err := storage.NewClient(ctx, option.WithTokenSource(dts)) 56 | 57 | } 58 | -------------------------------------------------------------------------------- /google/error.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Go 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 | "errors" 9 | 10 | "golang.org/x/oauth2" 11 | ) 12 | 13 | // AuthenticationError indicates there was an error in the authentication flow. 14 | // 15 | // Use (*AuthenticationError).Temporary to check if the error can be retried. 16 | type AuthenticationError struct { 17 | err *oauth2.RetrieveError 18 | } 19 | 20 | func newAuthenticationError(err error) error { 21 | re := &oauth2.RetrieveError{} 22 | if !errors.As(err, &re) { 23 | return err 24 | } 25 | return &AuthenticationError{ 26 | err: re, 27 | } 28 | } 29 | 30 | // Temporary indicates that the network error has one of the following status codes and may be retried: 500, 503, 408, or 429. 31 | func (e *AuthenticationError) Temporary() bool { 32 | if e.err.Response == nil { 33 | return false 34 | } 35 | sc := e.err.Response.StatusCode 36 | return sc == 500 || sc == 503 || sc == 408 || sc == 429 37 | } 38 | 39 | func (e *AuthenticationError) Error() string { 40 | return e.err.Error() 41 | } 42 | 43 | func (e *AuthenticationError) Unwrap() error { 44 | return e.err 45 | } 46 | 47 | type errWrappingTokenSource struct { 48 | src oauth2.TokenSource 49 | } 50 | 51 | func newErrWrappingTokenSource(ts oauth2.TokenSource) oauth2.TokenSource { 52 | return &errWrappingTokenSource{src: ts} 53 | } 54 | 55 | // Token returns the current token if it's still valid, else will 56 | // refresh the current token (using r.Context for HTTP client 57 | // information) and return the new one. 58 | func (s *errWrappingTokenSource) Token() (*oauth2.Token, error) { 59 | t, err := s.src.Token() 60 | if err != nil { 61 | return nil, newAuthenticationError(err) 62 | } 63 | return t, nil 64 | } 65 | -------------------------------------------------------------------------------- /google/error_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Go 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 | "net/http" 9 | "testing" 10 | 11 | "golang.org/x/oauth2" 12 | ) 13 | 14 | func TestAuthenticationError_Temporary(t *testing.T) { 15 | tests := []struct { 16 | name string 17 | code int 18 | want bool 19 | }{ 20 | { 21 | name: "temporary with 500", 22 | code: 500, 23 | want: true, 24 | }, 25 | { 26 | name: "temporary with 503", 27 | code: 503, 28 | want: true, 29 | }, 30 | { 31 | name: "temporary with 408", 32 | code: 408, 33 | want: true, 34 | }, 35 | { 36 | name: "temporary with 429", 37 | code: 429, 38 | want: true, 39 | }, 40 | { 41 | name: "temporary with 418", 42 | code: 418, 43 | want: false, 44 | }, 45 | } 46 | for _, tt := range tests { 47 | t.Run(tt.name, func(t *testing.T) { 48 | ae := &AuthenticationError{ 49 | err: &oauth2.RetrieveError{ 50 | Response: &http.Response{ 51 | StatusCode: tt.code, 52 | }, 53 | }, 54 | } 55 | if got := ae.Temporary(); got != tt.want { 56 | t.Errorf("Temporary() = %v; want %v", got, tt.want) 57 | } 58 | }) 59 | } 60 | } 61 | 62 | func TestErrWrappingTokenSource_Token(t *testing.T) { 63 | tok := oauth2.Token{AccessToken: "MyAccessToken"} 64 | ts := errWrappingTokenSource{ 65 | src: oauth2.StaticTokenSource(&tok), 66 | } 67 | got, err := ts.Token() 68 | if *got != tok { 69 | t.Errorf("Token() = %v; want %v", got, tok) 70 | } 71 | if err != nil { 72 | t.Error(err) 73 | } 74 | } 75 | 76 | type errTokenSource struct { 77 | err error 78 | } 79 | 80 | func (s *errTokenSource) Token() (*oauth2.Token, error) { 81 | return nil, s.err 82 | } 83 | 84 | func TestErrWrappingTokenSource_TokenError(t *testing.T) { 85 | re := &oauth2.RetrieveError{ 86 | Response: &http.Response{ 87 | StatusCode: 500, 88 | }, 89 | } 90 | ts := errWrappingTokenSource{ 91 | src: &errTokenSource{ 92 | err: re, 93 | }, 94 | } 95 | _, err := ts.Token() 96 | if err == nil { 97 | t.Fatalf("errWrappingTokenSource.Token() err = nil, want *AuthenticationError") 98 | } 99 | ae, ok := err.(*AuthenticationError) 100 | if !ok { 101 | t.Fatalf("errWrappingTokenSource.Token() err = %T, want *AuthenticationError", err) 102 | } 103 | wrappedErr := ae.Unwrap() 104 | if wrappedErr == nil { 105 | t.Fatalf("AuthenticationError.Unwrap() err = nil, want *oauth2.RetrieveError") 106 | } 107 | _, ok = wrappedErr.(*oauth2.RetrieveError) 108 | if !ok { 109 | t.Errorf("AuthenticationError.Unwrap() err = %T, want *oauth2.RetrieveError", err) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /google/example_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Go 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_test 6 | 7 | import ( 8 | "context" 9 | "fmt" 10 | "log" 11 | "net/http" 12 | "os" 13 | 14 | "golang.org/x/oauth2" 15 | "golang.org/x/oauth2/google" 16 | "golang.org/x/oauth2/jwt" 17 | ) 18 | 19 | func ExampleDefaultClient() { 20 | client, err := google.DefaultClient(oauth2.NoContext, 21 | "https://www.googleapis.com/auth/devstorage.full_control") 22 | if err != nil { 23 | log.Fatal(err) 24 | } 25 | client.Get("...") 26 | } 27 | 28 | func Example_webServer() { 29 | // Your credentials should be obtained from the Google 30 | // Developer Console (https://console.developers.google.com). 31 | conf := &oauth2.Config{ 32 | ClientID: "YOUR_CLIENT_ID", 33 | ClientSecret: "YOUR_CLIENT_SECRET", 34 | RedirectURL: "YOUR_REDIRECT_URL", 35 | Scopes: []string{ 36 | "https://www.googleapis.com/auth/bigquery", 37 | "https://www.googleapis.com/auth/blogger", 38 | }, 39 | Endpoint: google.Endpoint, 40 | } 41 | // Redirect user to Google's consent page to ask for permission 42 | // for the scopes specified above. 43 | url := conf.AuthCodeURL("state") 44 | fmt.Printf("Visit the URL for the auth dialog: %v", url) 45 | 46 | // Handle the exchange code to initiate a transport. 47 | tok, err := conf.Exchange(oauth2.NoContext, "authorization-code") 48 | if err != nil { 49 | log.Fatal(err) 50 | } 51 | client := conf.Client(oauth2.NoContext, tok) 52 | client.Get("...") 53 | } 54 | 55 | func ExampleJWTConfigFromJSON() { 56 | // Your credentials should be obtained from the Google 57 | // Developer Console (https://console.developers.google.com). 58 | // Navigate to your project, then see the "Credentials" page 59 | // under "APIs & Auth". 60 | // To create a service account client, click "Create new Client ID", 61 | // select "Service Account", and click "Create Client ID". A JSON 62 | // key file will then be downloaded to your computer. 63 | data, err := os.ReadFile("/path/to/your-project-key.json") 64 | if err != nil { 65 | log.Fatal(err) 66 | } 67 | conf, err := google.JWTConfigFromJSON(data, "https://www.googleapis.com/auth/bigquery") 68 | if err != nil { 69 | log.Fatal(err) 70 | } 71 | // Initiate an http.Client. The following GET request will be 72 | // authorized and authenticated on the behalf of 73 | // your service account. 74 | client := conf.Client(oauth2.NoContext) 75 | client.Get("...") 76 | } 77 | 78 | func ExampleSDKConfig() { 79 | // The credentials will be obtained from the first account that 80 | // has been authorized with `gcloud auth login`. 81 | conf, err := google.NewSDKConfig("") 82 | if err != nil { 83 | log.Fatal(err) 84 | } 85 | // Initiate an http.Client. The following GET request will be 86 | // authorized and authenticated on the behalf of the SDK user. 87 | client := conf.Client(oauth2.NoContext) 88 | client.Get("...") 89 | } 90 | 91 | func Example_serviceAccount() { 92 | // Your credentials should be obtained from the Google 93 | // Developer Console (https://console.developers.google.com). 94 | conf := &jwt.Config{ 95 | Email: "xxx@developer.gserviceaccount.com", 96 | // The contents of your RSA private key or your PEM file 97 | // that contains a private key. 98 | // If you have a p12 file instead, you 99 | // can use `openssl` to export the private key into a pem file. 100 | // 101 | // $ openssl pkcs12 -in key.p12 -passin pass:notasecret -out key.pem -nodes 102 | // 103 | // The field only supports PEM containers with no passphrase. 104 | // The openssl command will convert p12 keys to passphrase-less PEM containers. 105 | PrivateKey: []byte("-----BEGIN RSA PRIVATE KEY-----..."), 106 | Scopes: []string{ 107 | "https://www.googleapis.com/auth/bigquery", 108 | "https://www.googleapis.com/auth/blogger", 109 | }, 110 | TokenURL: google.JWTTokenURL, 111 | // If you would like to impersonate a user, you can 112 | // create a transport with a subject. The following GET 113 | // request will be made on the behalf of user@example.com. 114 | // Optional. 115 | Subject: "user@example.com", 116 | } 117 | // Initiate an http.Client, the following GET request will be 118 | // authorized and authenticated on the behalf of user@example.com. 119 | client := conf.Client(oauth2.NoContext) 120 | client.Get("...") 121 | } 122 | 123 | func ExampleComputeTokenSource() { 124 | client := &http.Client{ 125 | Transport: &oauth2.Transport{ 126 | // Fetch from Google Compute Engine's metadata server to retrieve 127 | // an access token for the provided account. 128 | // If no account is specified, "default" is used. 129 | // If no scopes are specified, a set of default scopes 130 | // are automatically granted. 131 | Source: google.ComputeTokenSource("", "https://www.googleapis.com/auth/bigquery"), 132 | }, 133 | } 134 | client.Get("...") 135 | } 136 | 137 | func ExampleCredentialsFromJSON() { 138 | ctx := context.Background() 139 | data, err := os.ReadFile("/path/to/key-file.json") 140 | if err != nil { 141 | log.Fatal(err) 142 | } 143 | creds, err := google.CredentialsFromJSON(ctx, data, "https://www.googleapis.com/auth/bigquery") 144 | if err != nil { 145 | log.Fatal(err) 146 | } 147 | _ = creds // TODO: Use creds. 148 | } 149 | -------------------------------------------------------------------------------- /google/externalaccount/filecredsource.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Go 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 externalaccount 6 | 7 | import ( 8 | "bytes" 9 | "encoding/json" 10 | "errors" 11 | "fmt" 12 | "io" 13 | "os" 14 | ) 15 | 16 | type fileCredentialSource struct { 17 | File string 18 | Format Format 19 | } 20 | 21 | func (cs fileCredentialSource) credentialSourceType() string { 22 | return "file" 23 | } 24 | 25 | func (cs fileCredentialSource) subjectToken() (string, error) { 26 | tokenFile, err := os.Open(cs.File) 27 | if err != nil { 28 | return "", fmt.Errorf("oauth2/google/externalaccount: failed to open credential file %q", cs.File) 29 | } 30 | defer tokenFile.Close() 31 | tokenBytes, err := io.ReadAll(io.LimitReader(tokenFile, 1<<20)) 32 | if err != nil { 33 | return "", fmt.Errorf("oauth2/google/externalaccount: failed to read credential file: %v", err) 34 | } 35 | tokenBytes = bytes.TrimSpace(tokenBytes) 36 | switch cs.Format.Type { 37 | case "json": 38 | jsonData := make(map[string]any) 39 | err = json.Unmarshal(tokenBytes, &jsonData) 40 | if err != nil { 41 | return "", fmt.Errorf("oauth2/google/externalaccount: failed to unmarshal subject token file: %v", err) 42 | } 43 | val, ok := jsonData[cs.Format.SubjectTokenFieldName] 44 | if !ok { 45 | return "", errors.New("oauth2/google/externalaccount: provided subject_token_field_name not found in credentials") 46 | } 47 | token, ok := val.(string) 48 | if !ok { 49 | return "", errors.New("oauth2/google/externalaccount: improperly formatted subject token") 50 | } 51 | return token, nil 52 | case "text": 53 | return string(tokenBytes), nil 54 | case "": 55 | return string(tokenBytes), nil 56 | default: 57 | return "", errors.New("oauth2/google/externalaccount: invalid credential_source file format type") 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /google/externalaccount/filecredsource_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Go 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 externalaccount 6 | 7 | import ( 8 | "context" 9 | "testing" 10 | ) 11 | 12 | var testFileConfig = Config{ 13 | Audience: "32555940559.apps.googleusercontent.com", 14 | SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt", 15 | TokenURL: "http://localhost:8080/v1/token", 16 | TokenInfoURL: "http://localhost:8080/v1/tokeninfo", 17 | ServiceAccountImpersonationURL: "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/service-gcs-admin@$PROJECT_ID.iam.gserviceaccount.com:generateAccessToken", 18 | ClientSecret: "notsosecret", 19 | ClientID: "rbrgnognrhongo3bi4gb9ghg9g", 20 | } 21 | 22 | func TestRetrieveFileSubjectToken(t *testing.T) { 23 | var fileSourceTests = []struct { 24 | name string 25 | cs CredentialSource 26 | want string 27 | }{ 28 | { 29 | name: "UntypedFileSource", 30 | cs: CredentialSource{ 31 | File: textBaseCredPath, 32 | }, 33 | want: "street123", 34 | }, 35 | { 36 | name: "TextFileSource", 37 | cs: CredentialSource{ 38 | File: textBaseCredPath, 39 | Format: Format{Type: fileTypeText}, 40 | }, 41 | want: "street123", 42 | }, 43 | { 44 | name: "JSONFileSource", 45 | cs: CredentialSource{ 46 | File: jsonBaseCredPath, 47 | Format: Format{Type: fileTypeJSON, SubjectTokenFieldName: "SubjToken"}, 48 | }, 49 | want: "321road", 50 | }, 51 | } 52 | 53 | for _, test := range fileSourceTests { 54 | test := test 55 | tfc := testFileConfig 56 | tfc.CredentialSource = &test.cs 57 | 58 | t.Run(test.name, func(t *testing.T) { 59 | base, err := tfc.parse(context.Background()) 60 | if err != nil { 61 | t.Fatalf("parse() failed %v", err) 62 | } 63 | 64 | out, err := base.subjectToken() 65 | if err != nil { 66 | t.Errorf("Method subjectToken() errored.") 67 | } else if test.want != out { 68 | t.Errorf("got %v but want %v", out, test.want) 69 | } 70 | 71 | if got, want := base.credentialSourceType(), "file"; got != want { 72 | t.Errorf("got %v but want %v", got, want) 73 | } 74 | }) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /google/externalaccount/header.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The Go 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 externalaccount 6 | 7 | import ( 8 | "runtime" 9 | "strings" 10 | "unicode" 11 | ) 12 | 13 | var ( 14 | // version is a package internal global variable for testing purposes. 15 | version = runtime.Version 16 | ) 17 | 18 | // versionUnknown is only used when the runtime version cannot be determined. 19 | const versionUnknown = "UNKNOWN" 20 | 21 | // goVersion returns a Go runtime version derived from the runtime environment 22 | // that is modified to be suitable for reporting in a header, meaning it has no 23 | // whitespace. If it is unable to determine the Go runtime version, it returns 24 | // versionUnknown. 25 | func goVersion() string { 26 | const develPrefix = "devel +" 27 | 28 | s := version() 29 | if strings.HasPrefix(s, develPrefix) { 30 | s = s[len(develPrefix):] 31 | if p := strings.IndexFunc(s, unicode.IsSpace); p >= 0 { 32 | s = s[:p] 33 | } 34 | return s 35 | } else if p := strings.IndexFunc(s, unicode.IsSpace); p >= 0 { 36 | s = s[:p] 37 | } 38 | 39 | notSemverRune := func(r rune) bool { 40 | return !strings.ContainsRune("0123456789.", r) 41 | } 42 | 43 | if strings.HasPrefix(s, "go1") { 44 | s = s[2:] 45 | var prerelease string 46 | if p := strings.IndexFunc(s, notSemverRune); p >= 0 { 47 | s, prerelease = s[:p], s[p:] 48 | } 49 | if strings.HasSuffix(s, ".") { 50 | s += "0" 51 | } else if strings.Count(s, ".") < 2 { 52 | s += ".0" 53 | } 54 | if prerelease != "" { 55 | // Some release candidates already have a dash in them. 56 | if !strings.HasPrefix(prerelease, "-") { 57 | prerelease = "-" + prerelease 58 | } 59 | s += prerelease 60 | } 61 | return s 62 | } 63 | return "UNKNOWN" 64 | } 65 | -------------------------------------------------------------------------------- /google/externalaccount/header_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The Go 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 externalaccount 6 | 7 | import ( 8 | "runtime" 9 | "testing" 10 | ) 11 | 12 | func TestGoVersion(t *testing.T) { 13 | testVersion := func(v string) func() string { 14 | return func() string { 15 | return v 16 | } 17 | } 18 | for _, tst := range []struct { 19 | v func() string 20 | want string 21 | }{ 22 | { 23 | testVersion("go1.19"), 24 | "1.19.0", 25 | }, 26 | { 27 | testVersion("go1.21-20230317-RC01"), 28 | "1.21.0-20230317-RC01", 29 | }, 30 | { 31 | testVersion("devel +abc1234"), 32 | "abc1234", 33 | }, 34 | { 35 | testVersion("this should be unknown"), 36 | versionUnknown, 37 | }, 38 | } { 39 | version = tst.v 40 | got := goVersion() 41 | if got != tst.want { 42 | t.Errorf("go version = %q, want = %q", got, tst.want) 43 | } 44 | } 45 | version = runtime.Version 46 | } 47 | -------------------------------------------------------------------------------- /google/externalaccount/programmaticrefreshcredsource.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Go 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 externalaccount 6 | 7 | import "context" 8 | 9 | type programmaticRefreshCredentialSource struct { 10 | supplierOptions SupplierOptions 11 | subjectTokenSupplier SubjectTokenSupplier 12 | ctx context.Context 13 | } 14 | 15 | func (cs programmaticRefreshCredentialSource) credentialSourceType() string { 16 | return "programmatic" 17 | } 18 | 19 | func (cs programmaticRefreshCredentialSource) subjectToken() (string, error) { 20 | return cs.subjectTokenSupplier.SubjectToken(cs.ctx, cs.supplierOptions) 21 | } 22 | -------------------------------------------------------------------------------- /google/externalaccount/programmaticrefreshcredsource_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Go 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 externalaccount 6 | 7 | import ( 8 | "context" 9 | "errors" 10 | "testing" 11 | ) 12 | 13 | func TestRetrieveSubjectToken_ProgrammaticAuth(t *testing.T) { 14 | tfc := testConfig 15 | 16 | tfc.SubjectTokenSupplier = testSubjectTokenSupplier{ 17 | subjectToken: "subjectToken", 18 | } 19 | 20 | base, err := tfc.parse(context.Background()) 21 | if err != nil { 22 | t.Fatalf("parse() failed %v", err) 23 | } 24 | 25 | out, err := base.subjectToken() 26 | if err != nil { 27 | t.Fatalf("retrieveSubjectToken() failed: %v", err) 28 | } 29 | 30 | if out != "subjectToken" { 31 | t.Errorf("subjectToken = \n%q\n want \nSubjectToken", out) 32 | } 33 | } 34 | 35 | func TestRetrieveSubjectToken_ProgrammaticAuthFails(t *testing.T) { 36 | tfc := testConfig 37 | testError := errors.New("test error") 38 | 39 | tfc.SubjectTokenSupplier = testSubjectTokenSupplier{ 40 | err: testError, 41 | } 42 | 43 | base, err := tfc.parse(context.Background()) 44 | if err != nil { 45 | t.Fatalf("parse() failed %v", err) 46 | } 47 | 48 | _, err = base.subjectToken() 49 | if err == nil { 50 | t.Fatalf("subjectToken() should have failed") 51 | } 52 | if testError != err { 53 | t.Errorf("subjectToken = %e, want %e", err, testError) 54 | } 55 | } 56 | 57 | func TestRetrieveSubjectToken_ProgrammaticAuthOptions(t *testing.T) { 58 | tfc := testConfig 59 | expectedOptions := SupplierOptions{Audience: tfc.Audience, SubjectTokenType: tfc.SubjectTokenType} 60 | 61 | tfc.SubjectTokenSupplier = testSubjectTokenSupplier{ 62 | subjectToken: "subjectToken", 63 | expectedOptions: &expectedOptions, 64 | } 65 | 66 | base, err := tfc.parse(context.Background()) 67 | if err != nil { 68 | t.Fatalf("parse() failed %v", err) 69 | } 70 | 71 | _, err = base.subjectToken() 72 | if err != nil { 73 | t.Fatalf("retrieveSubjectToken() failed: %v", err) 74 | } 75 | } 76 | 77 | func TestRetrieveSubjectToken_ProgrammaticAuthContext(t *testing.T) { 78 | tfc := testConfig 79 | ctx := context.Background() 80 | 81 | tfc.SubjectTokenSupplier = testSubjectTokenSupplier{ 82 | subjectToken: "subjectToken", 83 | expectedContext: ctx, 84 | } 85 | 86 | base, err := tfc.parse(ctx) 87 | if err != nil { 88 | t.Fatalf("parse() failed %v", err) 89 | } 90 | 91 | _, err = base.subjectToken() 92 | if err != nil { 93 | t.Fatalf("retrieveSubjectToken() failed: %v", err) 94 | } 95 | } 96 | 97 | type testSubjectTokenSupplier struct { 98 | err error 99 | subjectToken string 100 | expectedOptions *SupplierOptions 101 | expectedContext context.Context 102 | } 103 | 104 | func (supp testSubjectTokenSupplier) SubjectToken(ctx context.Context, options SupplierOptions) (string, error) { 105 | if supp.err != nil { 106 | return "", supp.err 107 | } 108 | if supp.expectedOptions != nil { 109 | if supp.expectedOptions.Audience != options.Audience { 110 | return "", errors.New("Audience does not match") 111 | } 112 | if supp.expectedOptions.SubjectTokenType != options.SubjectTokenType { 113 | return "", errors.New("Audience does not match") 114 | } 115 | } 116 | if supp.expectedContext != nil { 117 | if supp.expectedContext != ctx { 118 | return "", errors.New("Context does not match") 119 | } 120 | } 121 | return supp.subjectToken, nil 122 | } 123 | -------------------------------------------------------------------------------- /google/externalaccount/testdata/3pi_cred.json: -------------------------------------------------------------------------------- 1 | { 2 | "SubjToken": "321road" 3 | } 4 | -------------------------------------------------------------------------------- /google/externalaccount/testdata/3pi_cred.txt: -------------------------------------------------------------------------------- 1 | street123 2 | -------------------------------------------------------------------------------- /google/externalaccount/urlcredsource.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Go 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 externalaccount 6 | 7 | import ( 8 | "context" 9 | "encoding/json" 10 | "errors" 11 | "fmt" 12 | "io" 13 | "net/http" 14 | 15 | "golang.org/x/oauth2" 16 | ) 17 | 18 | type urlCredentialSource struct { 19 | URL string 20 | Headers map[string]string 21 | Format Format 22 | ctx context.Context 23 | } 24 | 25 | func (cs urlCredentialSource) credentialSourceType() string { 26 | return "url" 27 | } 28 | 29 | func (cs urlCredentialSource) subjectToken() (string, error) { 30 | client := oauth2.NewClient(cs.ctx, nil) 31 | req, err := http.NewRequest("GET", cs.URL, nil) 32 | if err != nil { 33 | return "", fmt.Errorf("oauth2/google/externalaccount: HTTP request for URL-sourced credential failed: %v", err) 34 | } 35 | req = req.WithContext(cs.ctx) 36 | 37 | for key, val := range cs.Headers { 38 | req.Header.Add(key, val) 39 | } 40 | resp, err := client.Do(req) 41 | if err != nil { 42 | return "", fmt.Errorf("oauth2/google/externalaccount: invalid response when retrieving subject token: %v", err) 43 | } 44 | defer resp.Body.Close() 45 | 46 | respBody, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) 47 | if err != nil { 48 | return "", fmt.Errorf("oauth2/google/externalaccount: invalid body in subject token URL query: %v", err) 49 | } 50 | if c := resp.StatusCode; c < 200 || c > 299 { 51 | return "", fmt.Errorf("oauth2/google/externalaccount: status code %d: %s", c, respBody) 52 | } 53 | 54 | switch cs.Format.Type { 55 | case "json": 56 | jsonData := make(map[string]any) 57 | err = json.Unmarshal(respBody, &jsonData) 58 | if err != nil { 59 | return "", fmt.Errorf("oauth2/google/externalaccount: failed to unmarshal subject token file: %v", err) 60 | } 61 | val, ok := jsonData[cs.Format.SubjectTokenFieldName] 62 | if !ok { 63 | return "", errors.New("oauth2/google/externalaccount: provided subject_token_field_name not found in credentials") 64 | } 65 | token, ok := val.(string) 66 | if !ok { 67 | return "", errors.New("oauth2/google/externalaccount: improperly formatted subject token") 68 | } 69 | return token, nil 70 | case "text": 71 | return string(respBody), nil 72 | case "": 73 | return string(respBody), nil 74 | default: 75 | return "", errors.New("oauth2/google/externalaccount: invalid credential_source file format type") 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /google/externalaccount/urlcredsource_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Go 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 externalaccount 6 | 7 | import ( 8 | "context" 9 | "encoding/json" 10 | "net/http" 11 | "net/http/httptest" 12 | "testing" 13 | ) 14 | 15 | var myURLToken = "testTokenValue" 16 | 17 | func TestRetrieveURLSubjectToken_Text(t *testing.T) { 18 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 19 | if r.Method != "GET" { 20 | t.Errorf("Unexpected request method, %v is found", r.Method) 21 | } 22 | if r.Header.Get("Metadata") != "True" { 23 | t.Errorf("Metadata header not properly included.") 24 | } 25 | w.Write([]byte("testTokenValue")) 26 | })) 27 | heads := make(map[string]string) 28 | heads["Metadata"] = "True" 29 | cs := CredentialSource{ 30 | URL: ts.URL, 31 | Format: Format{Type: fileTypeText}, 32 | Headers: heads, 33 | } 34 | tfc := testFileConfig 35 | tfc.CredentialSource = &cs 36 | 37 | base, err := tfc.parse(context.Background()) 38 | if err != nil { 39 | t.Fatalf("parse() failed %v", err) 40 | } 41 | 42 | out, err := base.subjectToken() 43 | if err != nil { 44 | t.Fatalf("retrieveSubjectToken() failed: %v", err) 45 | } 46 | if out != myURLToken { 47 | t.Errorf("got %v but want %v", out, myURLToken) 48 | } 49 | } 50 | 51 | // Checking that retrieveSubjectToken properly defaults to type text 52 | func TestRetrieveURLSubjectToken_Untyped(t *testing.T) { 53 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 54 | if r.Method != "GET" { 55 | t.Errorf("Unexpected request method, %v is found", r.Method) 56 | } 57 | w.Write([]byte("testTokenValue")) 58 | })) 59 | cs := CredentialSource{ 60 | URL: ts.URL, 61 | } 62 | tfc := testFileConfig 63 | tfc.CredentialSource = &cs 64 | 65 | base, err := tfc.parse(context.Background()) 66 | if err != nil { 67 | t.Fatalf("parse() failed %v", err) 68 | } 69 | 70 | out, err := base.subjectToken() 71 | if err != nil { 72 | t.Fatalf("Failed to retrieve URL subject token: %v", err) 73 | } 74 | if out != myURLToken { 75 | t.Errorf("got %v but want %v", out, myURLToken) 76 | } 77 | } 78 | 79 | func TestRetrieveURLSubjectToken_JSON(t *testing.T) { 80 | type tokenResponse struct { 81 | TestToken string `json:"SubjToken"` 82 | } 83 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 84 | if got, want := r.Method, "GET"; got != want { 85 | t.Errorf("got %v, but want %v", r.Method, want) 86 | } 87 | resp := tokenResponse{TestToken: "testTokenValue"} 88 | jsonResp, err := json.Marshal(resp) 89 | if err != nil { 90 | t.Errorf("Failed to marshal values: %v", err) 91 | } 92 | w.Write(jsonResp) 93 | })) 94 | cs := CredentialSource{ 95 | URL: ts.URL, 96 | Format: Format{Type: fileTypeJSON, SubjectTokenFieldName: "SubjToken"}, 97 | } 98 | tfc := testFileConfig 99 | tfc.CredentialSource = &cs 100 | 101 | base, err := tfc.parse(context.Background()) 102 | if err != nil { 103 | t.Fatalf("parse() failed %v", err) 104 | } 105 | 106 | out, err := base.subjectToken() 107 | if err != nil { 108 | t.Fatalf("%v", err) 109 | } 110 | if out != myURLToken { 111 | t.Errorf("got %v but want %v", out, myURLToken) 112 | } 113 | } 114 | 115 | func TestURLCredential_CredentialSourceType(t *testing.T) { 116 | cs := CredentialSource{ 117 | URL: "http://example.com", 118 | Format: Format{Type: fileTypeText}, 119 | } 120 | tfc := testFileConfig 121 | tfc.CredentialSource = &cs 122 | 123 | base, err := tfc.parse(context.Background()) 124 | if err != nil { 125 | t.Fatalf("parse() failed %v", err) 126 | } 127 | 128 | if got, want := base.credentialSourceType(), "url"; got != want { 129 | t.Errorf("got %v but want %v", got, want) 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /google/google_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Go 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 | "net/http" 9 | "net/http/httptest" 10 | "strings" 11 | "testing" 12 | ) 13 | 14 | var webJSONKey = []byte(` 15 | { 16 | "web": { 17 | "auth_uri": "https://google.com/o/oauth2/auth", 18 | "client_secret": "3Oknc4jS_wA2r9i", 19 | "token_uri": "https://google.com/o/oauth2/token", 20 | "client_email": "222-nprqovg5k43uum874cs9osjt2koe97g8@developer.gserviceaccount.com", 21 | "redirect_uris": ["https://www.example.com/oauth2callback"], 22 | "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/222-nprqovg5k43uum874cs9osjt2koe97g8@developer.gserviceaccount.com", 23 | "client_id": "222-nprqovg5k43uum874cs9osjt2koe97g8.apps.googleusercontent.com", 24 | "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", 25 | "javascript_origins": ["https://www.example.com"] 26 | } 27 | }`) 28 | 29 | var installedJSONKey = []byte(`{ 30 | "installed": { 31 | "client_id": "222-installed.apps.googleusercontent.com", 32 | "redirect_uris": ["https://www.example.com/oauth2callback"] 33 | } 34 | }`) 35 | 36 | var jwtJSONKey = []byte(`{ 37 | "private_key_id": "268f54e43a1af97cfc71731688434f45aca15c8b", 38 | "private_key": "super secret key", 39 | "client_email": "gopher@developer.gserviceaccount.com", 40 | "client_id": "gopher.apps.googleusercontent.com", 41 | "token_uri": "https://accounts.google.com/o/gophers/token", 42 | "type": "service_account", 43 | "audience": "https://testservice.googleapis.com/" 44 | }`) 45 | 46 | var jwtJSONKeyNoTokenURL = []byte(`{ 47 | "private_key_id": "268f54e43a1af97cfc71731688434f45aca15c8b", 48 | "private_key": "super secret key", 49 | "client_email": "gopher@developer.gserviceaccount.com", 50 | "client_id": "gopher.apps.googleusercontent.com", 51 | "type": "service_account" 52 | }`) 53 | 54 | var jwtJSONKeyNoAudience = []byte(`{ 55 | "private_key_id": "268f54e43a1af97cfc71731688434f45aca15c8b", 56 | "private_key": "super secret key", 57 | "client_email": "gopher@developer.gserviceaccount.com", 58 | "client_id": "gopher.apps.googleusercontent.com", 59 | "token_uri": "https://accounts.google.com/o/gophers/token", 60 | "type": "service_account" 61 | }`) 62 | 63 | func TestConfigFromJSON(t *testing.T) { 64 | conf, err := ConfigFromJSON(webJSONKey, "scope1", "scope2") 65 | if err != nil { 66 | t.Error(err) 67 | } 68 | if got, want := conf.ClientID, "222-nprqovg5k43uum874cs9osjt2koe97g8.apps.googleusercontent.com"; got != want { 69 | t.Errorf("ClientID = %q; want %q", got, want) 70 | } 71 | if got, want := conf.ClientSecret, "3Oknc4jS_wA2r9i"; got != want { 72 | t.Errorf("ClientSecret = %q; want %q", got, want) 73 | } 74 | if got, want := conf.RedirectURL, "https://www.example.com/oauth2callback"; got != want { 75 | t.Errorf("RedirectURL = %q; want %q", got, want) 76 | } 77 | if got, want := strings.Join(conf.Scopes, ","), "scope1,scope2"; got != want { 78 | t.Errorf("Scopes = %q; want %q", got, want) 79 | } 80 | if got, want := conf.Endpoint.AuthURL, "https://google.com/o/oauth2/auth"; got != want { 81 | t.Errorf("AuthURL = %q; want %q", got, want) 82 | } 83 | if got, want := conf.Endpoint.TokenURL, "https://google.com/o/oauth2/token"; got != want { 84 | t.Errorf("TokenURL = %q; want %q", got, want) 85 | } 86 | } 87 | 88 | func TestConfigFromJSON_Installed(t *testing.T) { 89 | conf, err := ConfigFromJSON(installedJSONKey) 90 | if err != nil { 91 | t.Error(err) 92 | } 93 | if got, want := conf.ClientID, "222-installed.apps.googleusercontent.com"; got != want { 94 | t.Errorf("ClientID = %q; want %q", got, want) 95 | } 96 | } 97 | 98 | func TestJWTConfigFromJSON(t *testing.T) { 99 | conf, err := JWTConfigFromJSON(jwtJSONKey, "scope1", "scope2") 100 | if err != nil { 101 | t.Fatal(err) 102 | } 103 | if got, want := conf.Email, "gopher@developer.gserviceaccount.com"; got != want { 104 | t.Errorf("Email = %q, want %q", got, want) 105 | } 106 | if got, want := string(conf.PrivateKey), "super secret key"; got != want { 107 | t.Errorf("PrivateKey = %q, want %q", got, want) 108 | } 109 | if got, want := conf.PrivateKeyID, "268f54e43a1af97cfc71731688434f45aca15c8b"; got != want { 110 | t.Errorf("PrivateKeyID = %q, want %q", got, want) 111 | } 112 | if got, want := strings.Join(conf.Scopes, ","), "scope1,scope2"; got != want { 113 | t.Errorf("Scopes = %q; want %q", got, want) 114 | } 115 | if got, want := conf.TokenURL, "https://accounts.google.com/o/gophers/token"; got != want { 116 | t.Errorf("TokenURL = %q; want %q", got, want) 117 | } 118 | if got, want := conf.Audience, "https://testservice.googleapis.com/"; got != want { 119 | t.Errorf("Audience = %q; want %q", got, want) 120 | } 121 | } 122 | 123 | func TestJWTConfigFromJSONNoTokenURL(t *testing.T) { 124 | conf, err := JWTConfigFromJSON(jwtJSONKeyNoTokenURL, "scope1", "scope2") 125 | if err != nil { 126 | t.Fatal(err) 127 | } 128 | if got, want := conf.TokenURL, "https://oauth2.googleapis.com/token"; got != want { 129 | t.Errorf("TokenURL = %q; want %q", got, want) 130 | } 131 | } 132 | 133 | func TestJWTConfigFromJSONNoAudience(t *testing.T) { 134 | conf, err := JWTConfigFromJSON(jwtJSONKeyNoAudience, "scope1", "scope2") 135 | if err != nil { 136 | t.Fatal(err) 137 | } 138 | if got, want := conf.Audience, ""; got != want { 139 | t.Errorf("Audience = %q; want %q", got, want) 140 | } 141 | } 142 | 143 | func TestComputeTokenSource(t *testing.T) { 144 | tokenPath := "/computeMetadata/v1/instance/service-accounts/default/token" 145 | tokenResponseBody := `{"access_token":"Sample.Access.Token","token_type":"Bearer","expires_in":3600}` 146 | s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 147 | if r.URL.Path != tokenPath { 148 | t.Errorf("got %s, want %s", r.URL.Path, tokenPath) 149 | } 150 | w.Write([]byte(tokenResponseBody)) 151 | })) 152 | defer s.Close() 153 | t.Setenv("GCE_METADATA_HOST", strings.TrimPrefix(s.URL, "http://")) 154 | ts := ComputeTokenSource("") 155 | _, err := ts.Token() 156 | if err != nil { 157 | t.Errorf("ts.Token() = %v", err) 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /google/internal/externalaccountauthorizeduser/externalaccountauthorizeduser.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The Go 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 externalaccountauthorizeduser 6 | 7 | import ( 8 | "context" 9 | "errors" 10 | "time" 11 | 12 | "golang.org/x/oauth2" 13 | "golang.org/x/oauth2/google/internal/stsexchange" 14 | ) 15 | 16 | // now aliases time.Now for testing. 17 | var now = func() time.Time { 18 | return time.Now().UTC() 19 | } 20 | 21 | var tokenValid = func(token oauth2.Token) bool { 22 | return token.Valid() 23 | } 24 | 25 | type Config struct { 26 | // Audience is the Secure Token Service (STS) audience which contains the resource name for the workforce pool and 27 | // the provider identifier in that pool. 28 | Audience string 29 | // RefreshToken is the optional OAuth 2.0 refresh token. If specified, credentials can be refreshed. 30 | RefreshToken string 31 | // TokenURL is the optional STS token exchange endpoint for refresh. Must be specified for refresh, can be left as 32 | // None if the token can not be refreshed. 33 | TokenURL string 34 | // TokenInfoURL is the optional STS endpoint URL for token introspection. 35 | TokenInfoURL string 36 | // ClientID is only required in conjunction with ClientSecret, as described above. 37 | ClientID string 38 | // ClientSecret is currently only required if token_info endpoint also needs to be called with the generated GCP 39 | // access token. When provided, STS will be called with additional basic authentication using client_id as username 40 | // and client_secret as password. 41 | ClientSecret string 42 | // Token is the OAuth2.0 access token. Can be nil if refresh information is provided. 43 | Token string 44 | // Expiry is the optional expiration datetime of the OAuth 2.0 access token. 45 | Expiry time.Time 46 | // RevokeURL is the optional STS endpoint URL for revoking tokens. 47 | RevokeURL string 48 | // QuotaProjectID is the optional project ID used for quota and billing. This project may be different from the 49 | // project used to create the credentials. 50 | QuotaProjectID string 51 | Scopes []string 52 | } 53 | 54 | func (c *Config) canRefresh() bool { 55 | return c.ClientID != "" && c.ClientSecret != "" && c.RefreshToken != "" && c.TokenURL != "" 56 | } 57 | 58 | func (c *Config) TokenSource(ctx context.Context) (oauth2.TokenSource, error) { 59 | var token oauth2.Token 60 | if c.Token != "" && !c.Expiry.IsZero() { 61 | token = oauth2.Token{ 62 | AccessToken: c.Token, 63 | Expiry: c.Expiry, 64 | TokenType: "Bearer", 65 | } 66 | } 67 | if !tokenValid(token) && !c.canRefresh() { 68 | return nil, errors.New("oauth2/google: Token should be created with fields to make it valid (`token` and `expiry`), or fields to allow it to refresh (`refresh_token`, `token_url`, `client_id`, `client_secret`).") 69 | } 70 | 71 | ts := tokenSource{ 72 | ctx: ctx, 73 | conf: c, 74 | } 75 | 76 | return oauth2.ReuseTokenSource(&token, ts), nil 77 | } 78 | 79 | type tokenSource struct { 80 | ctx context.Context 81 | conf *Config 82 | } 83 | 84 | func (ts tokenSource) Token() (*oauth2.Token, error) { 85 | conf := ts.conf 86 | if !conf.canRefresh() { 87 | return nil, errors.New("oauth2/google: The credentials do not contain the necessary fields need to refresh the access token. You must specify refresh_token, token_url, client_id, and client_secret.") 88 | } 89 | 90 | clientAuth := stsexchange.ClientAuthentication{ 91 | AuthStyle: oauth2.AuthStyleInHeader, 92 | ClientID: conf.ClientID, 93 | ClientSecret: conf.ClientSecret, 94 | } 95 | 96 | stsResponse, err := stsexchange.RefreshAccessToken(ts.ctx, conf.TokenURL, conf.RefreshToken, clientAuth, nil) 97 | if err != nil { 98 | return nil, err 99 | } 100 | if stsResponse.ExpiresIn < 0 { 101 | return nil, errors.New("oauth2/google: got invalid expiry from security token service") 102 | } 103 | 104 | if stsResponse.RefreshToken != "" { 105 | conf.RefreshToken = stsResponse.RefreshToken 106 | } 107 | 108 | token := &oauth2.Token{ 109 | AccessToken: stsResponse.AccessToken, 110 | Expiry: now().Add(time.Duration(stsResponse.ExpiresIn) * time.Second), 111 | TokenType: "Bearer", 112 | } 113 | return token, nil 114 | } 115 | -------------------------------------------------------------------------------- /google/internal/externalaccountauthorizeduser/externalaccountauthorizeduser_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The Go 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 externalaccountauthorizeduser 6 | 7 | import ( 8 | "context" 9 | "encoding/json" 10 | "errors" 11 | "io" 12 | "net/http" 13 | "net/http/httptest" 14 | "testing" 15 | "time" 16 | 17 | "golang.org/x/oauth2" 18 | "golang.org/x/oauth2/google/internal/stsexchange" 19 | ) 20 | 21 | const expiryDelta = 10 * time.Second 22 | 23 | var ( 24 | expiry = time.Unix(234852, 0) 25 | testNow = func() time.Time { return expiry } 26 | testValid = func(t oauth2.Token) bool { 27 | return t.AccessToken != "" && !t.Expiry.Round(0).Add(-expiryDelta).Before(testNow()) 28 | } 29 | ) 30 | 31 | type testRefreshTokenServer struct { 32 | URL string 33 | Authorization string 34 | ContentType string 35 | Body string 36 | ResponsePayload *stsexchange.Response 37 | Response string 38 | server *httptest.Server 39 | } 40 | 41 | func TestExternalAccountAuthorizedUser_JustToken(t *testing.T) { 42 | config := &Config{ 43 | Token: "AAAAAAA", 44 | Expiry: now().Add(time.Hour), 45 | } 46 | ts, err := config.TokenSource(context.Background()) 47 | if err != nil { 48 | t.Fatalf("Error getting token source: %v", err) 49 | } 50 | 51 | token, err := ts.Token() 52 | if err != nil { 53 | t.Fatalf("Error retrieving Token: %v", err) 54 | } 55 | if got, want := token.AccessToken, "AAAAAAA"; got != want { 56 | t.Fatalf("Unexpected access token, got %v, want %v", got, want) 57 | } 58 | } 59 | 60 | func TestExternalAccountAuthorizedUser_TokenRefreshWithRefreshTokenInResponse(t *testing.T) { 61 | server := &testRefreshTokenServer{ 62 | URL: "/", 63 | Authorization: "Basic Q0xJRU5UX0lEOkNMSUVOVF9TRUNSRVQ=", 64 | ContentType: "application/x-www-form-urlencoded", 65 | Body: "grant_type=refresh_token&refresh_token=BBBBBBBBB", 66 | ResponsePayload: &stsexchange.Response{ 67 | ExpiresIn: 3600, 68 | AccessToken: "AAAAAAA", 69 | RefreshToken: "CCCCCCC", 70 | }, 71 | } 72 | 73 | url, err := server.run(t) 74 | if err != nil { 75 | t.Fatalf("Error starting server") 76 | } 77 | defer server.close(t) 78 | 79 | config := &Config{ 80 | RefreshToken: "BBBBBBBBB", 81 | TokenURL: url, 82 | ClientID: "CLIENT_ID", 83 | ClientSecret: "CLIENT_SECRET", 84 | } 85 | ts, err := config.TokenSource(context.Background()) 86 | if err != nil { 87 | t.Fatalf("Error getting token source: %v", err) 88 | } 89 | 90 | token, err := ts.Token() 91 | if err != nil { 92 | t.Fatalf("Error retrieving Token: %v", err) 93 | } 94 | if got, want := token.AccessToken, "AAAAAAA"; got != want { 95 | t.Fatalf("Unexpected access token, got %v, want %v", got, want) 96 | } 97 | if config.RefreshToken != "CCCCCCC" { 98 | t.Fatalf("Refresh token not updated") 99 | } 100 | } 101 | 102 | func TestExternalAccountAuthorizedUser_MinimumFieldsRequiredForRefresh(t *testing.T) { 103 | server := &testRefreshTokenServer{ 104 | URL: "/", 105 | Authorization: "Basic Q0xJRU5UX0lEOkNMSUVOVF9TRUNSRVQ=", 106 | ContentType: "application/x-www-form-urlencoded", 107 | Body: "grant_type=refresh_token&refresh_token=BBBBBBBBB", 108 | ResponsePayload: &stsexchange.Response{ 109 | ExpiresIn: 3600, 110 | AccessToken: "AAAAAAA", 111 | }, 112 | } 113 | 114 | url, err := server.run(t) 115 | if err != nil { 116 | t.Fatalf("Error starting server") 117 | } 118 | defer server.close(t) 119 | 120 | config := &Config{ 121 | RefreshToken: "BBBBBBBBB", 122 | TokenURL: url, 123 | ClientID: "CLIENT_ID", 124 | ClientSecret: "CLIENT_SECRET", 125 | } 126 | ts, err := config.TokenSource(context.Background()) 127 | if err != nil { 128 | t.Fatalf("Error getting token source: %v", err) 129 | } 130 | 131 | token, err := ts.Token() 132 | if err != nil { 133 | t.Fatalf("Error retrieving Token: %v", err) 134 | } 135 | if got, want := token.AccessToken, "AAAAAAA"; got != want { 136 | t.Fatalf("Unexpected access token, got %v, want %v", got, want) 137 | } 138 | } 139 | 140 | func TestExternalAccountAuthorizedUser_MissingRefreshFields(t *testing.T) { 141 | server := &testRefreshTokenServer{ 142 | URL: "/", 143 | Authorization: "Basic Q0xJRU5UX0lEOkNMSUVOVF9TRUNSRVQ=", 144 | ContentType: "application/x-www-form-urlencoded", 145 | Body: "grant_type=refresh_token&refresh_token=BBBBBBBBB", 146 | ResponsePayload: &stsexchange.Response{ 147 | ExpiresIn: 3600, 148 | AccessToken: "AAAAAAA", 149 | }, 150 | } 151 | 152 | url, err := server.run(t) 153 | if err != nil { 154 | t.Fatalf("Error starting server") 155 | } 156 | defer server.close(t) 157 | testCases := []struct { 158 | name string 159 | config Config 160 | }{ 161 | { 162 | name: "empty config", 163 | config: Config{}, 164 | }, 165 | { 166 | name: "missing refresh token", 167 | config: Config{ 168 | TokenURL: url, 169 | ClientID: "CLIENT_ID", 170 | ClientSecret: "CLIENT_SECRET", 171 | }, 172 | }, 173 | { 174 | name: "missing token url", 175 | config: Config{ 176 | RefreshToken: "BBBBBBBBB", 177 | ClientID: "CLIENT_ID", 178 | ClientSecret: "CLIENT_SECRET", 179 | }, 180 | }, 181 | { 182 | name: "missing client id", 183 | config: Config{ 184 | RefreshToken: "BBBBBBBBB", 185 | TokenURL: url, 186 | ClientSecret: "CLIENT_SECRET", 187 | }, 188 | }, 189 | { 190 | name: "missing client secret", 191 | config: Config{ 192 | RefreshToken: "BBBBBBBBB", 193 | TokenURL: url, 194 | ClientID: "CLIENT_ID", 195 | }, 196 | }, 197 | } 198 | for _, tc := range testCases { 199 | t.Run(tc.name, func(t *testing.T) { 200 | 201 | expectErrMsg := "oauth2/google: Token should be created with fields to make it valid (`token` and `expiry`), or fields to allow it to refresh (`refresh_token`, `token_url`, `client_id`, `client_secret`)." 202 | _, err := tc.config.TokenSource((context.Background())) 203 | if err == nil { 204 | t.Fatalf("Expected error, but received none") 205 | } 206 | if got := err.Error(); got != expectErrMsg { 207 | t.Fatalf("Unexpected error, got %v, want %v", got, expectErrMsg) 208 | } 209 | }) 210 | } 211 | } 212 | 213 | func (trts *testRefreshTokenServer) run(t *testing.T) (string, error) { 214 | t.Helper() 215 | if trts.server != nil { 216 | return "", errors.New("Server is already running") 217 | } 218 | trts.server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 219 | if got, want := r.URL.String(), trts.URL; got != want { 220 | t.Errorf("URL.String(): got %v but want %v", got, want) 221 | } 222 | headerAuth := r.Header.Get("Authorization") 223 | if got, want := headerAuth, trts.Authorization; got != want { 224 | t.Errorf("got %v but want %v", got, want) 225 | } 226 | headerContentType := r.Header.Get("Content-Type") 227 | if got, want := headerContentType, trts.ContentType; got != want { 228 | t.Errorf("got %v but want %v", got, want) 229 | } 230 | body, err := io.ReadAll(r.Body) 231 | if err != nil { 232 | t.Fatalf("Failed reading request body: %s.", err) 233 | } 234 | if got, want := string(body), trts.Body; got != want { 235 | t.Errorf("Unexpected exchange payload: got %v but want %v", got, want) 236 | } 237 | w.Header().Set("Content-Type", "application/json") 238 | if trts.ResponsePayload != nil { 239 | content, err := json.Marshal(trts.ResponsePayload) 240 | if err != nil { 241 | t.Fatalf("unable to marshall response JSON") 242 | } 243 | w.Write(content) 244 | } else { 245 | w.Write([]byte(trts.Response)) 246 | } 247 | })) 248 | return trts.server.URL, nil 249 | } 250 | 251 | func (trts *testRefreshTokenServer) close(t *testing.T) error { 252 | t.Helper() 253 | if trts.server == nil { 254 | return errors.New("No server is running") 255 | } 256 | trts.server.Close() 257 | trts.server = nil 258 | return nil 259 | } 260 | -------------------------------------------------------------------------------- /google/internal/impersonate/impersonate.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The Go 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 impersonate 6 | 7 | import ( 8 | "bytes" 9 | "context" 10 | "encoding/json" 11 | "fmt" 12 | "io" 13 | "net/http" 14 | "time" 15 | 16 | "golang.org/x/oauth2" 17 | ) 18 | 19 | // generateAccesstokenReq is used for service account impersonation 20 | type generateAccessTokenReq struct { 21 | Delegates []string `json:"delegates,omitempty"` 22 | Lifetime string `json:"lifetime,omitempty"` 23 | Scope []string `json:"scope,omitempty"` 24 | } 25 | 26 | type impersonateTokenResponse struct { 27 | AccessToken string `json:"accessToken"` 28 | ExpireTime string `json:"expireTime"` 29 | } 30 | 31 | // ImpersonateTokenSource uses a source credential, stored in Ts, to request an access token to the provided URL. 32 | // Scopes can be defined when the access token is requested. 33 | type ImpersonateTokenSource struct { 34 | // Ctx is the execution context of the impersonation process 35 | // used to perform http call to the URL. Required 36 | Ctx context.Context 37 | // Ts is the source credential used to generate a token on the 38 | // impersonated service account. Required. 39 | Ts oauth2.TokenSource 40 | 41 | // URL is the endpoint to call to generate a token 42 | // on behalf the service account. Required. 43 | URL string 44 | // Scopes that the impersonated credential should have. Required. 45 | Scopes []string 46 | // Delegates are the service account email addresses in a delegation chain. 47 | // Each service account must be granted roles/iam.serviceAccountTokenCreator 48 | // on the next service account in the chain. Optional. 49 | Delegates []string 50 | // TokenLifetimeSeconds is the number of seconds the impersonation token will 51 | // be valid for. 52 | TokenLifetimeSeconds int 53 | } 54 | 55 | // Token performs the exchange to get a temporary service account token to allow access to GCP. 56 | func (its ImpersonateTokenSource) Token() (*oauth2.Token, error) { 57 | lifetimeString := "3600s" 58 | if its.TokenLifetimeSeconds != 0 { 59 | lifetimeString = fmt.Sprintf("%ds", its.TokenLifetimeSeconds) 60 | } 61 | reqBody := generateAccessTokenReq{ 62 | Lifetime: lifetimeString, 63 | Scope: its.Scopes, 64 | Delegates: its.Delegates, 65 | } 66 | b, err := json.Marshal(reqBody) 67 | if err != nil { 68 | return nil, fmt.Errorf("oauth2/google: unable to marshal request: %v", err) 69 | } 70 | client := oauth2.NewClient(its.Ctx, its.Ts) 71 | req, err := http.NewRequest("POST", its.URL, bytes.NewReader(b)) 72 | if err != nil { 73 | return nil, fmt.Errorf("oauth2/google: unable to create impersonation request: %v", err) 74 | } 75 | req = req.WithContext(its.Ctx) 76 | req.Header.Set("Content-Type", "application/json") 77 | 78 | resp, err := client.Do(req) 79 | if err != nil { 80 | return nil, fmt.Errorf("oauth2/google: unable to generate access token: %v", err) 81 | } 82 | defer resp.Body.Close() 83 | body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) 84 | if err != nil { 85 | return nil, fmt.Errorf("oauth2/google: unable to read body: %v", err) 86 | } 87 | if c := resp.StatusCode; c < 200 || c > 299 { 88 | return nil, fmt.Errorf("oauth2/google: status code %d: %s", c, body) 89 | } 90 | 91 | var accessTokenResp impersonateTokenResponse 92 | if err := json.Unmarshal(body, &accessTokenResp); err != nil { 93 | return nil, fmt.Errorf("oauth2/google: unable to parse response: %v", err) 94 | } 95 | expiry, err := time.Parse(time.RFC3339, accessTokenResp.ExpireTime) 96 | if err != nil { 97 | return nil, fmt.Errorf("oauth2/google: unable to parse expiry: %v", err) 98 | } 99 | return &oauth2.Token{ 100 | AccessToken: accessTokenResp.AccessToken, 101 | Expiry: expiry, 102 | TokenType: "Bearer", 103 | }, nil 104 | } 105 | -------------------------------------------------------------------------------- /google/internal/stsexchange/clientauth.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Go 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 stsexchange 6 | 7 | import ( 8 | "encoding/base64" 9 | "net/http" 10 | "net/url" 11 | 12 | "golang.org/x/oauth2" 13 | ) 14 | 15 | // ClientAuthentication represents an OAuth client ID and secret and the mechanism for passing these credentials as stated in rfc6749#2.3.1. 16 | type ClientAuthentication struct { 17 | // AuthStyle can be either basic or request-body 18 | AuthStyle oauth2.AuthStyle 19 | ClientID string 20 | ClientSecret string 21 | } 22 | 23 | // InjectAuthentication is used to add authentication to a Secure Token Service exchange 24 | // request. It modifies either the passed url.Values or http.Header depending on the desired 25 | // authentication format. 26 | func (c *ClientAuthentication) InjectAuthentication(values url.Values, headers http.Header) { 27 | if c.ClientID == "" || c.ClientSecret == "" || values == nil || headers == nil { 28 | return 29 | } 30 | 31 | switch c.AuthStyle { 32 | case oauth2.AuthStyleInHeader: // AuthStyleInHeader corresponds to basic authentication as defined in rfc7617#2 33 | plainHeader := c.ClientID + ":" + c.ClientSecret 34 | headers.Add("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(plainHeader))) 35 | case oauth2.AuthStyleInParams: // AuthStyleInParams corresponds to request-body authentication with ClientID and ClientSecret in the message body. 36 | values.Set("client_id", c.ClientID) 37 | values.Set("client_secret", c.ClientSecret) 38 | case oauth2.AuthStyleAutoDetect: 39 | values.Set("client_id", c.ClientID) 40 | values.Set("client_secret", c.ClientSecret) 41 | default: 42 | values.Set("client_id", c.ClientID) 43 | values.Set("client_secret", c.ClientSecret) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /google/internal/stsexchange/clientauth_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Go 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 stsexchange 6 | 7 | import ( 8 | "net/http" 9 | "net/url" 10 | "reflect" 11 | "testing" 12 | 13 | "golang.org/x/oauth2" 14 | ) 15 | 16 | var clientID = "rbrgnognrhongo3bi4gb9ghg9g" 17 | var clientSecret = "notsosecret" 18 | 19 | var audience = []string{"32555940559.apps.googleusercontent.com"} 20 | var grantType = []string{"urn:ietf:params:oauth:grant-type:token-exchange"} 21 | var requestedTokenType = []string{"urn:ietf:params:oauth:token-type:access_token"} 22 | var subjectTokenType = []string{"urn:ietf:params:oauth:token-type:jwt"} 23 | var subjectToken = []string{"eyJhbGciOiJSUzI1NiIsImtpZCI6IjJjNmZhNmY1OTUwYTdjZTQ2NWZjZjI0N2FhMGIwOTQ4MjhhYzk1MmMiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiIzMjU1NTk0MDU1OS5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsImF1ZCI6IjMyNTU1OTQwNTU5LmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwic3ViIjoiMTEzMzE4NTQxMDA5MDU3Mzc4MzI4IiwiaGQiOiJnb29nbGUuY29tIiwiZW1haWwiOiJpdGh1cmllbEBnb29nbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImF0X2hhc2giOiI5OVJVYVFrRHJsVDFZOUV0SzdiYXJnIiwiaWF0IjoxNjAxNTgxMzQ5LCJleHAiOjE2MDE1ODQ5NDl9.SZ-4DyDcogDh_CDUKHqPCiT8AKLg4zLMpPhGQzmcmHQ6cJiV0WRVMf5Lq911qsvuekgxfQpIdKNXlD6yk3FqvC2rjBbuEztMF-OD_2B8CEIYFlMLGuTQimJlUQksLKM-3B2ITRDCxnyEdaZik0OVssiy1CBTsllS5MgTFqic7w8w0Cd6diqNkfPFZRWyRYsrRDRlHHbH5_TUnv2wnLVHBHlNvU4wU2yyjDIoqOvTRp8jtXdq7K31CDhXd47-hXsVFQn2ZgzuUEAkH2Q6NIXACcVyZOrjBcZiOQI9IRWz-g03LzbzPSecO7I8dDrhqUSqMrdNUz_f8Kr8JFhuVMfVug"} 24 | var scope = []string{"https://www.googleapis.com/auth/devstorage.full_control"} 25 | 26 | var ContentType = []string{"application/x-www-form-urlencoded"} 27 | 28 | func TestClientAuthentication_InjectHeaderAuthentication(t *testing.T) { 29 | valuesH := url.Values{ 30 | "audience": audience, 31 | "grant_type": grantType, 32 | "requested_token_type": requestedTokenType, 33 | "subject_token_type": subjectTokenType, 34 | "subject_token": subjectToken, 35 | "scope": scope, 36 | } 37 | headerH := http.Header{ 38 | "Content-Type": ContentType, 39 | } 40 | 41 | headerAuthentication := ClientAuthentication{ 42 | AuthStyle: oauth2.AuthStyleInHeader, 43 | ClientID: clientID, 44 | ClientSecret: clientSecret, 45 | } 46 | headerAuthentication.InjectAuthentication(valuesH, headerH) 47 | 48 | if got, want := valuesH["audience"], audience; !reflect.DeepEqual(got, want) { 49 | t.Errorf("audience = %q, want %q", got, want) 50 | } 51 | if got, want := valuesH["grant_type"], grantType; !reflect.DeepEqual(got, want) { 52 | t.Errorf("grant_type = %q, want %q", got, want) 53 | } 54 | if got, want := valuesH["requested_token_type"], requestedTokenType; !reflect.DeepEqual(got, want) { 55 | t.Errorf("requested_token_type = %q, want %q", got, want) 56 | } 57 | if got, want := valuesH["subject_token_type"], subjectTokenType; !reflect.DeepEqual(got, want) { 58 | t.Errorf("subject_token_type = %q, want %q", got, want) 59 | } 60 | if got, want := valuesH["subject_token"], subjectToken; !reflect.DeepEqual(got, want) { 61 | t.Errorf("subject_token = %q, want %q", got, want) 62 | } 63 | if got, want := valuesH["scope"], scope; !reflect.DeepEqual(got, want) { 64 | t.Errorf("scope = %q, want %q", got, want) 65 | } 66 | if got, want := headerH["Authorization"], []string{"Basic cmJyZ25vZ25yaG9uZ28zYmk0Z2I5Z2hnOWc6bm90c29zZWNyZXQ="}; !reflect.DeepEqual(got, want) { 67 | t.Errorf("Authorization in header = %q, want %q", got, want) 68 | } 69 | } 70 | 71 | func TestClientAuthentication_ParamsAuthentication(t *testing.T) { 72 | valuesP := url.Values{ 73 | "audience": audience, 74 | "grant_type": grantType, 75 | "requested_token_type": requestedTokenType, 76 | "subject_token_type": subjectTokenType, 77 | "subject_token": subjectToken, 78 | "scope": scope, 79 | } 80 | headerP := http.Header{ 81 | "Content-Type": ContentType, 82 | } 83 | paramsAuthentication := ClientAuthentication{ 84 | AuthStyle: oauth2.AuthStyleInParams, 85 | ClientID: clientID, 86 | ClientSecret: clientSecret, 87 | } 88 | paramsAuthentication.InjectAuthentication(valuesP, headerP) 89 | 90 | if got, want := valuesP["audience"], audience; !reflect.DeepEqual(got, want) { 91 | t.Errorf("audience = %q, want %q", got, want) 92 | } 93 | if got, want := valuesP["grant_type"], grantType; !reflect.DeepEqual(got, want) { 94 | t.Errorf("grant_type = %q, want %q", got, want) 95 | } 96 | if got, want := valuesP["requested_token_type"], requestedTokenType; !reflect.DeepEqual(got, want) { 97 | t.Errorf("requested_token_type = %q, want %q", got, want) 98 | } 99 | if got, want := valuesP["subject_token_type"], subjectTokenType; !reflect.DeepEqual(got, want) { 100 | t.Errorf("subject_token_type = %q, want %q", got, want) 101 | } 102 | if got, want := valuesP["subject_token"], subjectToken; !reflect.DeepEqual(got, want) { 103 | t.Errorf("subject_token = %q, want %q", got, want) 104 | } 105 | if got, want := valuesP["scope"], scope; !reflect.DeepEqual(got, want) { 106 | t.Errorf("scope = %q, want %q", got, want) 107 | } 108 | if got, want := valuesP["client_id"], []string{clientID}; !reflect.DeepEqual(got, want) { 109 | t.Errorf("client_id = %q, want %q", got, want) 110 | } 111 | if got, want := valuesP["client_secret"], []string{clientSecret}; !reflect.DeepEqual(got, want) { 112 | t.Errorf("client_secret = %q, want %q", got, want) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /google/internal/stsexchange/sts_exchange.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Go 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 stsexchange 6 | 7 | import ( 8 | "context" 9 | "encoding/json" 10 | "fmt" 11 | "io" 12 | "net/http" 13 | "net/url" 14 | "strconv" 15 | "strings" 16 | 17 | "golang.org/x/oauth2" 18 | ) 19 | 20 | func defaultHeader() http.Header { 21 | header := make(http.Header) 22 | header.Add("Content-Type", "application/x-www-form-urlencoded") 23 | return header 24 | } 25 | 26 | // ExchangeToken performs an oauth2 token exchange with the provided endpoint. 27 | // The first 4 fields are all mandatory. headers can be used to pass additional 28 | // headers beyond the bare minimum required by the token exchange. options can 29 | // be used to pass additional JSON-structured options to the remote server. 30 | func ExchangeToken(ctx context.Context, endpoint string, request *TokenExchangeRequest, authentication ClientAuthentication, headers http.Header, options map[string]any) (*Response, error) { 31 | data := url.Values{} 32 | data.Set("audience", request.Audience) 33 | data.Set("grant_type", "urn:ietf:params:oauth:grant-type:token-exchange") 34 | data.Set("requested_token_type", "urn:ietf:params:oauth:token-type:access_token") 35 | data.Set("subject_token_type", request.SubjectTokenType) 36 | data.Set("subject_token", request.SubjectToken) 37 | data.Set("scope", strings.Join(request.Scope, " ")) 38 | if options != nil { 39 | opts, err := json.Marshal(options) 40 | if err != nil { 41 | return nil, fmt.Errorf("oauth2/google: failed to marshal additional options: %v", err) 42 | } 43 | data.Set("options", string(opts)) 44 | } 45 | 46 | return makeRequest(ctx, endpoint, data, authentication, headers) 47 | } 48 | 49 | func RefreshAccessToken(ctx context.Context, endpoint string, refreshToken string, authentication ClientAuthentication, headers http.Header) (*Response, error) { 50 | data := url.Values{} 51 | data.Set("grant_type", "refresh_token") 52 | data.Set("refresh_token", refreshToken) 53 | 54 | return makeRequest(ctx, endpoint, data, authentication, headers) 55 | } 56 | 57 | func makeRequest(ctx context.Context, endpoint string, data url.Values, authentication ClientAuthentication, headers http.Header) (*Response, error) { 58 | if headers == nil { 59 | headers = defaultHeader() 60 | } 61 | client := oauth2.NewClient(ctx, nil) 62 | authentication.InjectAuthentication(data, headers) 63 | encodedData := data.Encode() 64 | 65 | req, err := http.NewRequest("POST", endpoint, strings.NewReader(encodedData)) 66 | if err != nil { 67 | return nil, fmt.Errorf("oauth2/google: failed to properly build http request: %v", err) 68 | } 69 | req = req.WithContext(ctx) 70 | for key, list := range headers { 71 | for _, val := range list { 72 | req.Header.Add(key, val) 73 | } 74 | } 75 | req.Header.Add("Content-Length", strconv.Itoa(len(encodedData))) 76 | 77 | resp, err := client.Do(req) 78 | 79 | if err != nil { 80 | return nil, fmt.Errorf("oauth2/google: invalid response from Secure Token Server: %v", err) 81 | } 82 | defer resp.Body.Close() 83 | 84 | body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) 85 | if err != nil { 86 | return nil, err 87 | } 88 | if c := resp.StatusCode; c < 200 || c > 299 { 89 | return nil, fmt.Errorf("oauth2/google: status code %d: %s", c, body) 90 | } 91 | var stsResp Response 92 | err = json.Unmarshal(body, &stsResp) 93 | if err != nil { 94 | return nil, fmt.Errorf("oauth2/google: failed to unmarshal response body from Secure Token Server: %v", err) 95 | 96 | } 97 | 98 | return &stsResp, nil 99 | } 100 | 101 | // TokenExchangeRequest contains fields necessary to make an oauth2 token exchange. 102 | type TokenExchangeRequest struct { 103 | ActingParty struct { 104 | ActorToken string 105 | ActorTokenType string 106 | } 107 | GrantType string 108 | Resource string 109 | Audience string 110 | Scope []string 111 | RequestedTokenType string 112 | SubjectToken string 113 | SubjectTokenType string 114 | } 115 | 116 | // Response is used to decode the remote server response during an oauth2 token exchange. 117 | type Response struct { 118 | AccessToken string `json:"access_token"` 119 | IssuedTokenType string `json:"issued_token_type"` 120 | TokenType string `json:"token_type"` 121 | ExpiresIn int `json:"expires_in"` 122 | Scope string `json:"scope"` 123 | RefreshToken string `json:"refresh_token"` 124 | } 125 | -------------------------------------------------------------------------------- /google/jwt.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Go 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 | "strings" 11 | "time" 12 | 13 | "golang.org/x/oauth2" 14 | "golang.org/x/oauth2/internal" 15 | "golang.org/x/oauth2/jws" 16 | ) 17 | 18 | // JWTAccessTokenSourceFromJSON uses a Google Developers service account JSON 19 | // key file to read the credentials that authorize and authenticate the 20 | // requests, and returns a TokenSource that does not use any OAuth2 flow but 21 | // instead creates a JWT and sends that as the access token. 22 | // The audience is typically a URL that specifies the scope of the credentials. 23 | // 24 | // Note that this is not a standard OAuth flow, but rather an 25 | // optimization supported by a few Google services. 26 | // Unless you know otherwise, you should use JWTConfigFromJSON instead. 27 | func JWTAccessTokenSourceFromJSON(jsonKey []byte, audience string) (oauth2.TokenSource, error) { 28 | return newJWTSource(jsonKey, audience, nil) 29 | } 30 | 31 | // JWTAccessTokenSourceWithScope uses a Google Developers service account JSON 32 | // key file to read the credentials that authorize and authenticate the 33 | // requests, and returns a TokenSource that does not use any OAuth2 flow but 34 | // instead creates a JWT and sends that as the access token. 35 | // The scope is typically a list of URLs that specifies the scope of the 36 | // credentials. 37 | // 38 | // Note that this is not a standard OAuth flow, but rather an 39 | // optimization supported by a few Google services. 40 | // Unless you know otherwise, you should use JWTConfigFromJSON instead. 41 | func JWTAccessTokenSourceWithScope(jsonKey []byte, scope ...string) (oauth2.TokenSource, error) { 42 | return newJWTSource(jsonKey, "", scope) 43 | } 44 | 45 | func newJWTSource(jsonKey []byte, audience string, scopes []string) (oauth2.TokenSource, error) { 46 | if len(scopes) == 0 && audience == "" { 47 | return nil, fmt.Errorf("google: missing scope/audience for JWT access token") 48 | } 49 | 50 | cfg, err := JWTConfigFromJSON(jsonKey) 51 | if err != nil { 52 | return nil, fmt.Errorf("google: could not parse JSON key: %v", err) 53 | } 54 | pk, err := internal.ParseKey(cfg.PrivateKey) 55 | if err != nil { 56 | return nil, fmt.Errorf("google: could not parse key: %v", err) 57 | } 58 | ts := &jwtAccessTokenSource{ 59 | email: cfg.Email, 60 | audience: audience, 61 | scopes: scopes, 62 | pk: pk, 63 | pkID: cfg.PrivateKeyID, 64 | } 65 | tok, err := ts.Token() 66 | if err != nil { 67 | return nil, err 68 | } 69 | rts := newErrWrappingTokenSource(oauth2.ReuseTokenSource(tok, ts)) 70 | return rts, nil 71 | } 72 | 73 | type jwtAccessTokenSource struct { 74 | email, audience string 75 | scopes []string 76 | pk *rsa.PrivateKey 77 | pkID string 78 | } 79 | 80 | func (ts *jwtAccessTokenSource) Token() (*oauth2.Token, error) { 81 | iat := time.Now() 82 | exp := iat.Add(time.Hour) 83 | scope := strings.Join(ts.scopes, " ") 84 | cs := &jws.ClaimSet{ 85 | Iss: ts.email, 86 | Sub: ts.email, 87 | Aud: ts.audience, 88 | Scope: scope, 89 | Iat: iat.Unix(), 90 | Exp: exp.Unix(), 91 | } 92 | hdr := &jws.Header{ 93 | Algorithm: "RS256", 94 | Typ: "JWT", 95 | KeyID: string(ts.pkID), 96 | } 97 | msg, err := jws.Encode(hdr, cs, ts.pk) 98 | if err != nil { 99 | return nil, fmt.Errorf("google: could not encode JWT: %v", err) 100 | } 101 | return &oauth2.Token{AccessToken: msg, TokenType: "Bearer", Expiry: exp}, nil 102 | } 103 | -------------------------------------------------------------------------------- /google/jwt_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Go 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 | "bytes" 9 | "crypto/rand" 10 | "crypto/rsa" 11 | "crypto/x509" 12 | "encoding/base64" 13 | "encoding/json" 14 | "encoding/pem" 15 | "strings" 16 | "sync" 17 | "testing" 18 | "time" 19 | 20 | "golang.org/x/oauth2/jws" 21 | ) 22 | 23 | var ( 24 | privateKey *rsa.PrivateKey 25 | jsonKey []byte 26 | once sync.Once 27 | ) 28 | 29 | func TestJWTAccessTokenSourceFromJSON(t *testing.T) { 30 | setupDummyKey(t) 31 | 32 | ts, err := JWTAccessTokenSourceFromJSON(jsonKey, "audience") 33 | if err != nil { 34 | t.Fatalf("JWTAccessTokenSourceFromJSON: %v\nJSON: %s", err, string(jsonKey)) 35 | } 36 | 37 | tok, err := ts.Token() 38 | if err != nil { 39 | t.Fatalf("Token: %v", err) 40 | } 41 | 42 | if got, want := tok.TokenType, "Bearer"; got != want { 43 | t.Errorf("TokenType = %q, want %q", got, want) 44 | } 45 | if got := tok.Expiry; tok.Expiry.Before(time.Now()) { 46 | t.Errorf("Expiry = %v, should not be expired", got) 47 | } 48 | 49 | err = jws.Verify(tok.AccessToken, &privateKey.PublicKey) 50 | if err != nil { 51 | t.Errorf("jws.Verify on AccessToken: %v", err) 52 | } 53 | 54 | claim, err := jws.Decode(tok.AccessToken) 55 | if err != nil { 56 | t.Fatalf("jws.Decode on AccessToken: %v", err) 57 | } 58 | 59 | if got, want := claim.Iss, "gopher@developer.gserviceaccount.com"; got != want { 60 | t.Errorf("Iss = %q, want %q", got, want) 61 | } 62 | if got, want := claim.Sub, "gopher@developer.gserviceaccount.com"; got != want { 63 | t.Errorf("Sub = %q, want %q", got, want) 64 | } 65 | if got, want := claim.Aud, "audience"; got != want { 66 | t.Errorf("Aud = %q, want %q", got, want) 67 | } 68 | 69 | // Finally, check the header private key. 70 | parts := strings.Split(tok.AccessToken, ".") 71 | hdrJSON, err := base64.RawURLEncoding.DecodeString(parts[0]) 72 | if err != nil { 73 | t.Fatalf("base64 DecodeString: %v\nString: %q", err, parts[0]) 74 | } 75 | var hdr jws.Header 76 | if err := json.Unmarshal(hdrJSON, &hdr); err != nil { 77 | t.Fatalf("json.Unmarshal: %v (%q)", err, hdrJSON) 78 | } 79 | 80 | if got, want := hdr.KeyID, "268f54e43a1af97cfc71731688434f45aca15c8b"; got != want { 81 | t.Errorf("Header KeyID = %q, want %q", got, want) 82 | } 83 | } 84 | 85 | func TestJWTAccessTokenSourceWithScope(t *testing.T) { 86 | setupDummyKey(t) 87 | 88 | ts, err := JWTAccessTokenSourceWithScope(jsonKey, "scope1", "scope2") 89 | if err != nil { 90 | t.Fatalf("JWTAccessTokenSourceWithScope: %v\nJSON: %s", err, string(jsonKey)) 91 | } 92 | 93 | tok, err := ts.Token() 94 | if err != nil { 95 | t.Fatalf("Token: %v", err) 96 | } 97 | 98 | if got, want := tok.TokenType, "Bearer"; got != want { 99 | t.Errorf("TokenType = %q, want %q", got, want) 100 | } 101 | if got := tok.Expiry; tok.Expiry.Before(time.Now()) { 102 | t.Errorf("Expiry = %v, should not be expired", got) 103 | } 104 | 105 | err = jws.Verify(tok.AccessToken, &privateKey.PublicKey) 106 | if err != nil { 107 | t.Errorf("jws.Verify on AccessToken: %v", err) 108 | } 109 | 110 | claim, err := jws.Decode(tok.AccessToken) 111 | if err != nil { 112 | t.Fatalf("jws.Decode on AccessToken: %v", err) 113 | } 114 | 115 | if got, want := claim.Iss, "gopher@developer.gserviceaccount.com"; got != want { 116 | t.Errorf("Iss = %q, want %q", got, want) 117 | } 118 | if got, want := claim.Sub, "gopher@developer.gserviceaccount.com"; got != want { 119 | t.Errorf("Sub = %q, want %q", got, want) 120 | } 121 | if got, want := claim.Scope, "scope1 scope2"; got != want { 122 | t.Errorf("Aud = %q, want %q", got, want) 123 | } 124 | 125 | // Finally, check the header private key. 126 | parts := strings.Split(tok.AccessToken, ".") 127 | hdrJSON, err := base64.RawURLEncoding.DecodeString(parts[0]) 128 | if err != nil { 129 | t.Fatalf("base64 DecodeString: %v\nString: %q", err, parts[0]) 130 | } 131 | var hdr jws.Header 132 | if err := json.Unmarshal(hdrJSON, &hdr); err != nil { 133 | t.Fatalf("json.Unmarshal: %v (%q)", err, hdrJSON) 134 | } 135 | 136 | if got, want := hdr.KeyID, "268f54e43a1af97cfc71731688434f45aca15c8b"; got != want { 137 | t.Errorf("Header KeyID = %q, want %q", got, want) 138 | } 139 | } 140 | 141 | func setupDummyKey(t *testing.T) { 142 | once.Do(func() { 143 | // Generate a key we can use in the test data. 144 | pk, err := rsa.GenerateKey(rand.Reader, 2048) 145 | if err != nil { 146 | t.Fatal(err) 147 | } 148 | privateKey = pk 149 | // Encode the key and substitute into our example JSON. 150 | enc := pem.EncodeToMemory(&pem.Block{ 151 | Type: "PRIVATE KEY", 152 | Bytes: x509.MarshalPKCS1PrivateKey(privateKey), 153 | }) 154 | enc, err = json.Marshal(string(enc)) 155 | if err != nil { 156 | t.Fatalf("json.Marshal: %v", err) 157 | } 158 | jsonKey = bytes.Replace(jwtJSONKey, []byte(`"super secret key"`), enc, 1) 159 | }) 160 | } 161 | -------------------------------------------------------------------------------- /google/sdk.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Go 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 | "bufio" 9 | "context" 10 | "encoding/json" 11 | "errors" 12 | "fmt" 13 | "io" 14 | "net/http" 15 | "os" 16 | "os/user" 17 | "path/filepath" 18 | "runtime" 19 | "strings" 20 | "time" 21 | 22 | "golang.org/x/oauth2" 23 | ) 24 | 25 | type sdkCredentials struct { 26 | Data []struct { 27 | Credential struct { 28 | ClientID string `json:"client_id"` 29 | ClientSecret string `json:"client_secret"` 30 | AccessToken string `json:"access_token"` 31 | RefreshToken string `json:"refresh_token"` 32 | TokenExpiry *time.Time `json:"token_expiry"` 33 | } `json:"credential"` 34 | Key struct { 35 | Account string `json:"account"` 36 | Scope string `json:"scope"` 37 | } `json:"key"` 38 | } 39 | } 40 | 41 | // An SDKConfig provides access to tokens from an account already 42 | // authorized via the Google Cloud SDK. 43 | type SDKConfig struct { 44 | conf oauth2.Config 45 | initialToken *oauth2.Token 46 | } 47 | 48 | // NewSDKConfig creates an SDKConfig for the given Google Cloud SDK 49 | // account. If account is empty, the account currently active in 50 | // Google Cloud SDK properties is used. 51 | // Google Cloud SDK credentials must be created by running `gcloud auth` 52 | // before using this function. 53 | // The Google Cloud SDK is available at https://cloud.google.com/sdk/. 54 | func NewSDKConfig(account string) (*SDKConfig, error) { 55 | configPath, err := sdkConfigPath() 56 | if err != nil { 57 | return nil, fmt.Errorf("oauth2/google: error getting SDK config path: %v", err) 58 | } 59 | credentialsPath := filepath.Join(configPath, "credentials") 60 | f, err := os.Open(credentialsPath) 61 | if err != nil { 62 | return nil, fmt.Errorf("oauth2/google: failed to load SDK credentials: %v", err) 63 | } 64 | defer f.Close() 65 | 66 | var c sdkCredentials 67 | if err := json.NewDecoder(f).Decode(&c); err != nil { 68 | return nil, fmt.Errorf("oauth2/google: failed to decode SDK credentials from %q: %v", credentialsPath, err) 69 | } 70 | if len(c.Data) == 0 { 71 | return nil, fmt.Errorf("oauth2/google: no credentials found in %q, run `gcloud auth login` to create one", credentialsPath) 72 | } 73 | if account == "" { 74 | propertiesPath := filepath.Join(configPath, "properties") 75 | f, err := os.Open(propertiesPath) 76 | if err != nil { 77 | return nil, fmt.Errorf("oauth2/google: failed to load SDK properties: %v", err) 78 | } 79 | defer f.Close() 80 | ini, err := parseINI(f) 81 | if err != nil { 82 | return nil, fmt.Errorf("oauth2/google: failed to parse SDK properties %q: %v", propertiesPath, err) 83 | } 84 | core, ok := ini["core"] 85 | if !ok { 86 | return nil, fmt.Errorf("oauth2/google: failed to find [core] section in %v", ini) 87 | } 88 | active, ok := core["account"] 89 | if !ok { 90 | return nil, fmt.Errorf("oauth2/google: failed to find %q attribute in %v", "account", core) 91 | } 92 | account = active 93 | } 94 | 95 | for _, d := range c.Data { 96 | if account == "" || d.Key.Account == account { 97 | if d.Credential.AccessToken == "" && d.Credential.RefreshToken == "" { 98 | return nil, fmt.Errorf("oauth2/google: no token available for account %q", account) 99 | } 100 | var expiry time.Time 101 | if d.Credential.TokenExpiry != nil { 102 | expiry = *d.Credential.TokenExpiry 103 | } 104 | return &SDKConfig{ 105 | conf: oauth2.Config{ 106 | ClientID: d.Credential.ClientID, 107 | ClientSecret: d.Credential.ClientSecret, 108 | Scopes: strings.Split(d.Key.Scope, " "), 109 | Endpoint: Endpoint, 110 | RedirectURL: "oob", 111 | }, 112 | initialToken: &oauth2.Token{ 113 | AccessToken: d.Credential.AccessToken, 114 | RefreshToken: d.Credential.RefreshToken, 115 | Expiry: expiry, 116 | }, 117 | }, nil 118 | } 119 | } 120 | return nil, fmt.Errorf("oauth2/google: no such credentials for account %q", account) 121 | } 122 | 123 | // Client returns an HTTP client using Google Cloud SDK credentials to 124 | // authorize requests. The token will auto-refresh as necessary. The 125 | // underlying http.RoundTripper will be obtained using the provided 126 | // context. The returned client and its Transport should not be 127 | // modified. 128 | func (c *SDKConfig) Client(ctx context.Context) *http.Client { 129 | return &http.Client{ 130 | Transport: &oauth2.Transport{ 131 | Source: c.TokenSource(ctx), 132 | }, 133 | } 134 | } 135 | 136 | // TokenSource returns an oauth2.TokenSource that retrieve tokens from 137 | // Google Cloud SDK credentials using the provided context. 138 | // It will returns the current access token stored in the credentials, 139 | // and refresh it when it expires, but it won't update the credentials 140 | // with the new access token. 141 | func (c *SDKConfig) TokenSource(ctx context.Context) oauth2.TokenSource { 142 | return c.conf.TokenSource(ctx, c.initialToken) 143 | } 144 | 145 | // Scopes are the OAuth 2.0 scopes the current account is authorized for. 146 | func (c *SDKConfig) Scopes() []string { 147 | return c.conf.Scopes 148 | } 149 | 150 | func parseINI(ini io.Reader) (map[string]map[string]string, error) { 151 | result := map[string]map[string]string{ 152 | "": {}, // root section 153 | } 154 | scanner := bufio.NewScanner(ini) 155 | currentSection := "" 156 | for scanner.Scan() { 157 | line := strings.TrimSpace(scanner.Text()) 158 | if strings.HasPrefix(line, ";") { 159 | // comment. 160 | continue 161 | } 162 | if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") { 163 | currentSection = strings.TrimSpace(line[1 : len(line)-1]) 164 | result[currentSection] = map[string]string{} 165 | continue 166 | } 167 | parts := strings.SplitN(line, "=", 2) 168 | if len(parts) == 2 && parts[0] != "" { 169 | result[currentSection][strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1]) 170 | } 171 | } 172 | if err := scanner.Err(); err != nil { 173 | return nil, fmt.Errorf("error scanning ini: %v", err) 174 | } 175 | return result, nil 176 | } 177 | 178 | // sdkConfigPath tries to guess where the gcloud config is located. 179 | // It can be overridden during tests. 180 | var sdkConfigPath = func() (string, error) { 181 | if runtime.GOOS == "windows" { 182 | return filepath.Join(os.Getenv("APPDATA"), "gcloud"), nil 183 | } 184 | homeDir := guessUnixHomeDir() 185 | if homeDir == "" { 186 | return "", errors.New("unable to get current user home directory: os/user lookup failed; $HOME is empty") 187 | } 188 | return filepath.Join(homeDir, ".config", "gcloud"), nil 189 | } 190 | 191 | func guessUnixHomeDir() string { 192 | // Prefer $HOME over user.Current due to glibc bug: golang.org/issue/13470 193 | if v := os.Getenv("HOME"); v != "" { 194 | return v 195 | } 196 | // Else, fall back to user.Current: 197 | if u, err := user.Current(); err == nil { 198 | return u.HomeDir 199 | } 200 | return "" 201 | } 202 | -------------------------------------------------------------------------------- /google/sdk_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Go 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 | "reflect" 9 | "strings" 10 | "testing" 11 | ) 12 | 13 | func TestSDKConfig(t *testing.T) { 14 | sdkConfigPath = func() (string, error) { 15 | return "testdata/gcloud", nil 16 | } 17 | 18 | tests := []struct { 19 | account string 20 | accessToken string 21 | err bool 22 | }{ 23 | {"", "bar_access_token", false}, 24 | {"foo@example.com", "foo_access_token", false}, 25 | {"bar@example.com", "bar_access_token", false}, 26 | {"baz@serviceaccount.example.com", "", true}, 27 | } 28 | for _, tt := range tests { 29 | c, err := NewSDKConfig(tt.account) 30 | if got, want := err != nil, tt.err; got != want { 31 | if !tt.err { 32 | t.Errorf("got %v, want nil", err) 33 | } else { 34 | t.Errorf("got nil, want error") 35 | } 36 | continue 37 | } 38 | if err != nil { 39 | continue 40 | } 41 | tok := c.initialToken 42 | if tok == nil { 43 | t.Errorf("got nil, want %q", tt.accessToken) 44 | continue 45 | } 46 | if tok.AccessToken != tt.accessToken { 47 | t.Errorf("got %q, want %q", tok.AccessToken, tt.accessToken) 48 | } 49 | } 50 | } 51 | 52 | func TestParseINI(t *testing.T) { 53 | tests := []struct { 54 | ini string 55 | want map[string]map[string]string 56 | }{ 57 | { 58 | `root = toor 59 | [foo] 60 | bar = hop 61 | ini = nin 62 | `, 63 | map[string]map[string]string{ 64 | "": {"root": "toor"}, 65 | "foo": {"bar": "hop", "ini": "nin"}, 66 | }, 67 | }, 68 | { 69 | "\t extra \t = whitespace \t\r\n \t [everywhere] \t \r\n here \t = \t there \t \r\n", 70 | map[string]map[string]string{ 71 | "": {"extra": "whitespace"}, 72 | "everywhere": {"here": "there"}, 73 | }, 74 | }, 75 | { 76 | `[empty] 77 | [section] 78 | empty= 79 | `, 80 | map[string]map[string]string{ 81 | "": {}, 82 | "empty": {}, 83 | "section": {"empty": ""}, 84 | }, 85 | }, 86 | { 87 | `ignore 88 | [invalid 89 | =stuff 90 | ;comment=true 91 | `, 92 | map[string]map[string]string{ 93 | "": {}, 94 | }, 95 | }, 96 | } 97 | for _, tt := range tests { 98 | result, err := parseINI(strings.NewReader(tt.ini)) 99 | if err != nil { 100 | t.Errorf("parseINI(%q) error %v, want: no error", tt.ini, err) 101 | continue 102 | } 103 | if !reflect.DeepEqual(result, tt.want) { 104 | t.Errorf("parseINI(%q) = %#v, want: %#v", tt.ini, result, tt.want) 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /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://oauth2.googleapis.com/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://oauth2.googleapis.com/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://oauth2.googleapis.com/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://oauth2.googleapis.com/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://oauth2.googleapis.com/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://oauth2.googleapis.com/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://oauth2.googleapis.com/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 | -------------------------------------------------------------------------------- /google/testdata/gcloud/properties: -------------------------------------------------------------------------------- 1 | [core] 2 | account = bar@example.com -------------------------------------------------------------------------------- /heroku/heroku.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Go 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 heroku provides constants for using OAuth2 to access Heroku. 6 | package heroku // import "golang.org/x/oauth2/heroku" 7 | 8 | import ( 9 | "golang.org/x/oauth2" 10 | ) 11 | 12 | // Endpoint is Heroku's OAuth 2.0 endpoint. 13 | var Endpoint = oauth2.Endpoint{ 14 | AuthURL: "https://id.heroku.com/oauth/authorize", 15 | TokenURL: "https://id.heroku.com/oauth/token", 16 | } 17 | -------------------------------------------------------------------------------- /hipchat/hipchat.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Go 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 hipchat provides constants for using OAuth2 to access HipChat. 6 | package hipchat // import "golang.org/x/oauth2/hipchat" 7 | 8 | import ( 9 | "encoding/json" 10 | "errors" 11 | 12 | "golang.org/x/oauth2" 13 | "golang.org/x/oauth2/clientcredentials" 14 | ) 15 | 16 | // Endpoint is HipChat's OAuth 2.0 endpoint. 17 | var Endpoint = oauth2.Endpoint{ 18 | AuthURL: "https://www.hipchat.com/users/authorize", 19 | TokenURL: "https://api.hipchat.com/v2/oauth/token", 20 | } 21 | 22 | // ServerEndpoint returns a new oauth2.Endpoint for a HipChat Server instance 23 | // running on the given domain or host. 24 | func ServerEndpoint(host string) oauth2.Endpoint { 25 | return oauth2.Endpoint{ 26 | AuthURL: "https://" + host + "/users/authorize", 27 | TokenURL: "https://" + host + "/v2/oauth/token", 28 | } 29 | } 30 | 31 | // ClientCredentialsConfigFromCaps generates a Config from a HipChat API 32 | // capabilities descriptor. It does not verify the scopes against the 33 | // capabilities document at this time. 34 | // 35 | // For more information see: https://www.hipchat.com/docs/apiv2/method/get_capabilities 36 | func ClientCredentialsConfigFromCaps(capsJSON []byte, clientID, clientSecret string, scopes ...string) (*clientcredentials.Config, error) { 37 | var caps struct { 38 | Caps struct { 39 | Endpoint struct { 40 | TokenURL string `json:"tokenUrl"` 41 | } `json:"oauth2Provider"` 42 | } `json:"capabilities"` 43 | } 44 | 45 | if err := json.Unmarshal(capsJSON, &caps); err != nil { 46 | return nil, err 47 | } 48 | 49 | // Verify required fields. 50 | if caps.Caps.Endpoint.TokenURL == "" { 51 | return nil, errors.New("oauth2/hipchat: missing OAuth2 token URL in the capabilities descriptor JSON") 52 | } 53 | 54 | return &clientcredentials.Config{ 55 | ClientID: clientID, 56 | ClientSecret: clientSecret, 57 | Scopes: scopes, 58 | TokenURL: caps.Caps.Endpoint.TokenURL, 59 | }, nil 60 | } 61 | -------------------------------------------------------------------------------- /instagram/instagram.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Go 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 instagram provides constants for using OAuth2 to access Instagram. 6 | package instagram // import "golang.org/x/oauth2/instagram" 7 | 8 | import ( 9 | "golang.org/x/oauth2" 10 | ) 11 | 12 | // Endpoint is Instagram's OAuth 2.0 endpoint. 13 | var Endpoint = oauth2.Endpoint{ 14 | AuthURL: "https://api.instagram.com/oauth/authorize", 15 | TokenURL: "https://api.instagram.com/oauth/access_token", 16 | } 17 | -------------------------------------------------------------------------------- /internal/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Go 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 [golang.org/x/oauth2]. 6 | package internal 7 | -------------------------------------------------------------------------------- /internal/oauth2.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Go 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 6 | 7 | import ( 8 | "crypto/rsa" 9 | "crypto/x509" 10 | "encoding/pem" 11 | "errors" 12 | "fmt" 13 | ) 14 | 15 | // ParseKey converts the binary contents of a private key file 16 | // to an [*rsa.PrivateKey]. It detects whether the private key is in a 17 | // PEM container or not. If so, it extracts the private key 18 | // from PEM container before conversion. It only supports PEM 19 | // containers with no passphrase. 20 | func ParseKey(key []byte) (*rsa.PrivateKey, error) { 21 | block, _ := pem.Decode(key) 22 | if block != nil { 23 | key = block.Bytes 24 | } 25 | parsedKey, err := x509.ParsePKCS8PrivateKey(key) 26 | if err != nil { 27 | parsedKey, err = x509.ParsePKCS1PrivateKey(key) 28 | if err != nil { 29 | return nil, fmt.Errorf("private key should be a PEM or plain PKCS1 or PKCS8; parse error: %v", err) 30 | } 31 | } 32 | parsed, ok := parsedKey.(*rsa.PrivateKey) 33 | if !ok { 34 | return nil, errors.New("private key is invalid") 35 | } 36 | return parsed, nil 37 | } 38 | -------------------------------------------------------------------------------- /internal/token_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Go 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 6 | 7 | import ( 8 | "context" 9 | "fmt" 10 | "io" 11 | "math" 12 | "net/http" 13 | "net/http/httptest" 14 | "net/url" 15 | "testing" 16 | ) 17 | 18 | func TestRetrieveToken_InParams(t *testing.T) { 19 | styleCache := new(AuthStyleCache) 20 | const clientID = "client-id" 21 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 22 | if got, want := r.FormValue("client_id"), clientID; got != want { 23 | t.Errorf("client_id = %q; want %q", got, want) 24 | } 25 | if got, want := r.FormValue("client_secret"), ""; got != want { 26 | t.Errorf("client_secret = %q; want empty", got) 27 | } 28 | w.Header().Set("Content-Type", "application/json") 29 | io.WriteString(w, `{"access_token": "ACCESS_TOKEN", "token_type": "bearer"}`) 30 | })) 31 | defer ts.Close() 32 | _, err := RetrieveToken(context.Background(), clientID, "", ts.URL, url.Values{}, AuthStyleInParams, styleCache) 33 | if err != nil { 34 | t.Errorf("RetrieveToken = %v; want no error", err) 35 | } 36 | } 37 | 38 | func TestRetrieveTokenWithContexts(t *testing.T) { 39 | styleCache := new(AuthStyleCache) 40 | const clientID = "client-id" 41 | 42 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 43 | w.Header().Set("Content-Type", "application/json") 44 | io.WriteString(w, `{"access_token": "ACCESS_TOKEN", "token_type": "bearer"}`) 45 | })) 46 | defer ts.Close() 47 | 48 | _, err := RetrieveToken(context.Background(), clientID, "", ts.URL, url.Values{}, AuthStyleUnknown, styleCache) 49 | if err != nil { 50 | t.Errorf("RetrieveToken (with background context) = %v; want no error", err) 51 | } 52 | 53 | retrieved := make(chan struct{}) 54 | cancellingts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 55 | <-retrieved 56 | })) 57 | defer cancellingts.Close() 58 | 59 | ctx, cancel := context.WithCancel(context.Background()) 60 | cancel() 61 | _, err = RetrieveToken(ctx, clientID, "", cancellingts.URL, url.Values{}, AuthStyleUnknown, styleCache) 62 | close(retrieved) 63 | if err == nil { 64 | t.Errorf("RetrieveToken (with cancelled context) = nil; want error") 65 | } 66 | } 67 | 68 | func TestExpiresInUpperBound(t *testing.T) { 69 | var e expirationTime 70 | if err := e.UnmarshalJSON([]byte(fmt.Sprint(int64(math.MaxInt32) + 1))); err != nil { 71 | t.Fatal(err) 72 | } 73 | const want = math.MaxInt32 74 | if e != want { 75 | t.Errorf("expiration time = %v; want %v", e, want) 76 | } 77 | } 78 | 79 | func TestAuthStyleCache(t *testing.T) { 80 | var c LazyAuthStyleCache 81 | 82 | cases := []struct { 83 | url string 84 | clientID string 85 | style AuthStyle 86 | }{ 87 | { 88 | "https://host1.example.com/token", 89 | "client_1", 90 | AuthStyleInHeader, 91 | }, { 92 | "https://host2.example.com/token", 93 | "client_2", 94 | AuthStyleInParams, 95 | }, { 96 | "https://host1.example.com/token", 97 | "client_3", 98 | AuthStyleInParams, 99 | }, 100 | } 101 | 102 | for _, tt := range cases { 103 | t.Run(tt.clientID, func(t *testing.T) { 104 | cc := c.Get() 105 | got, ok := cc.lookupAuthStyle(tt.url, tt.clientID) 106 | if ok { 107 | t.Fatalf("unexpected auth style found on first request: %v", got) 108 | } 109 | 110 | cc.setAuthStyle(tt.url, tt.clientID, tt.style) 111 | 112 | got, ok = cc.lookupAuthStyle(tt.url, tt.clientID) 113 | if !ok { 114 | t.Fatalf("auth style not found in cache") 115 | } 116 | 117 | if got != tt.style { 118 | t.Fatalf("auth style mismatch, got=%v, want=%v", got, tt.style) 119 | } 120 | }) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /internal/transport.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Go 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 6 | 7 | import ( 8 | "context" 9 | "net/http" 10 | ) 11 | 12 | // HTTPClient is the context key to use with [context.WithValue] 13 | // to associate an [*http.Client] value with a context. 14 | var HTTPClient ContextKey 15 | 16 | // ContextKey is just an empty struct. It exists so HTTPClient can be 17 | // an immutable public variable with a unique type. It's immutable 18 | // because nobody else can create a ContextKey, being unexported. 19 | type ContextKey struct{} 20 | 21 | func ContextClient(ctx context.Context) *http.Client { 22 | if ctx != nil { 23 | if hc, ok := ctx.Value(HTTPClient).(*http.Client); ok { 24 | return hc 25 | } 26 | } 27 | return http.DefaultClient 28 | } 29 | -------------------------------------------------------------------------------- /jira/jira.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Go 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 jira provides claims and JWT signing for OAuth2 to access JIRA/Confluence. 6 | package jira 7 | 8 | import ( 9 | "context" 10 | "crypto/hmac" 11 | "crypto/sha256" 12 | "encoding/base64" 13 | "encoding/json" 14 | "fmt" 15 | "io" 16 | "net/http" 17 | "net/url" 18 | "strings" 19 | "time" 20 | 21 | "golang.org/x/oauth2" 22 | ) 23 | 24 | // ClaimSet contains information about the JWT signature according 25 | // to Atlassian's documentation 26 | // https://developer.atlassian.com/cloud/jira/software/oauth-2-jwt-bearer-token-authorization-grant-type/ 27 | type ClaimSet struct { 28 | Issuer string `json:"iss"` 29 | Subject string `json:"sub"` 30 | InstalledURL string `json:"tnt"` // URL of installed app 31 | AuthURL string `json:"aud"` // URL of auth server 32 | ExpiresIn int64 `json:"exp"` // Must be no later that 60 seconds in the future 33 | IssuedAt int64 `json:"iat"` 34 | } 35 | 36 | var ( 37 | defaultGrantType = "urn:ietf:params:oauth:grant-type:jwt-bearer" 38 | defaultHeader = map[string]string{ 39 | "typ": "JWT", 40 | "alg": "HS256", 41 | } 42 | ) 43 | 44 | // Config is the configuration for using JWT to fetch tokens, 45 | // commonly known as "two-legged OAuth 2.0". 46 | type Config struct { 47 | // BaseURL for your app 48 | BaseURL string 49 | 50 | // Subject is the userkey as defined by Atlassian 51 | // Different than username (ex: /rest/api/2/user?username=alex) 52 | Subject string 53 | 54 | oauth2.Config 55 | } 56 | 57 | // TokenSource returns a JWT TokenSource using the configuration 58 | // in c and the HTTP client from the provided context. 59 | func (c *Config) TokenSource(ctx context.Context) oauth2.TokenSource { 60 | return oauth2.ReuseTokenSource(nil, jwtSource{ctx, c}) 61 | } 62 | 63 | // Client returns an HTTP client wrapping the context's 64 | // HTTP transport and adding Authorization headers with tokens 65 | // obtained from c. 66 | // 67 | // The returned client and its Transport should not be modified. 68 | func (c *Config) Client(ctx context.Context) *http.Client { 69 | return oauth2.NewClient(ctx, c.TokenSource(ctx)) 70 | } 71 | 72 | // jwtSource is a source that always does a signed JWT request for a token. 73 | // It should typically be wrapped with a reuseTokenSource. 74 | type jwtSource struct { 75 | ctx context.Context 76 | conf *Config 77 | } 78 | 79 | func (js jwtSource) Token() (*oauth2.Token, error) { 80 | exp := time.Duration(59) * time.Second 81 | claimSet := &ClaimSet{ 82 | Issuer: fmt.Sprintf("urn:atlassian:connect:clientid:%s", js.conf.ClientID), 83 | Subject: fmt.Sprintf("urn:atlassian:connect:useraccountid:%s", js.conf.Subject), 84 | InstalledURL: js.conf.BaseURL, 85 | AuthURL: js.conf.Endpoint.AuthURL, 86 | IssuedAt: time.Now().Unix(), 87 | ExpiresIn: time.Now().Add(exp).Unix(), 88 | } 89 | 90 | v := url.Values{} 91 | v.Set("grant_type", defaultGrantType) 92 | 93 | // Add scopes if they exist; If not, it defaults to app scopes 94 | if scopes := js.conf.Scopes; scopes != nil { 95 | upperScopes := make([]string, len(scopes)) 96 | for i, k := range scopes { 97 | upperScopes[i] = strings.ToUpper(k) 98 | } 99 | v.Set("scope", strings.Join(upperScopes, "+")) 100 | } 101 | 102 | // Sign claims for assertion 103 | assertion, err := sign(js.conf.ClientSecret, claimSet) 104 | if err != nil { 105 | return nil, err 106 | } 107 | v.Set("assertion", assertion) 108 | 109 | // Fetch access token from auth server 110 | hc := oauth2.NewClient(js.ctx, nil) 111 | resp, err := hc.PostForm(js.conf.Endpoint.TokenURL, v) 112 | if err != nil { 113 | return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err) 114 | } 115 | defer resp.Body.Close() 116 | body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) 117 | if err != nil { 118 | return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err) 119 | } 120 | if c := resp.StatusCode; c < 200 || c > 299 { 121 | return nil, fmt.Errorf("oauth2: cannot fetch token: %v\nResponse: %s", resp.Status, body) 122 | } 123 | 124 | // tokenRes is the JSON response body. 125 | var tokenRes oauth2.Token 126 | if err := json.Unmarshal(body, &tokenRes); err != nil { 127 | return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err) 128 | } 129 | token := &oauth2.Token{ 130 | AccessToken: tokenRes.AccessToken, 131 | TokenType: tokenRes.TokenType, 132 | } 133 | 134 | if secs := tokenRes.ExpiresIn; secs > 0 { 135 | token.Expiry = time.Now().Add(time.Duration(secs) * time.Second) 136 | } 137 | return token, nil 138 | } 139 | 140 | // Sign the claim set with the shared secret 141 | // Result to be sent as assertion 142 | func sign(key string, claims *ClaimSet) (string, error) { 143 | b, err := json.Marshal(defaultHeader) 144 | if err != nil { 145 | return "", err 146 | } 147 | header := base64.RawURLEncoding.EncodeToString(b) 148 | 149 | jsonClaims, err := json.Marshal(claims) 150 | if err != nil { 151 | return "", err 152 | } 153 | encodedClaims := strings.TrimRight(base64.URLEncoding.EncodeToString(jsonClaims), "=") 154 | 155 | ss := fmt.Sprintf("%s.%s", header, encodedClaims) 156 | 157 | mac := hmac.New(sha256.New, []byte(key)) 158 | mac.Write([]byte(ss)) 159 | signature := mac.Sum(nil) 160 | 161 | return fmt.Sprintf("%s.%s", ss, base64.RawURLEncoding.EncodeToString(signature)), nil 162 | } 163 | -------------------------------------------------------------------------------- /jira/jira_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Go 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 jira 6 | 7 | import ( 8 | "context" 9 | "encoding/base64" 10 | "encoding/json" 11 | "net/http" 12 | "net/http/httptest" 13 | "strings" 14 | "testing" 15 | 16 | "golang.org/x/oauth2" 17 | "golang.org/x/oauth2/jws" 18 | ) 19 | 20 | func TestJWTFetch_JSONResponse(t *testing.T) { 21 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 22 | w.Header().Set("Content-Type", "application/json") 23 | w.Write([]byte(`{ 24 | "access_token": "90d64460d14870c08c81352a05dedd3465940a7c", 25 | "token_type": "Bearer", 26 | "expires_in": 3600 27 | }`)) 28 | })) 29 | defer ts.Close() 30 | 31 | conf := &Config{ 32 | BaseURL: "https://my.app.com", 33 | Subject: "useraccountId", 34 | Config: oauth2.Config{ 35 | ClientID: "super_secret_client_id", 36 | ClientSecret: "super_shared_secret", 37 | Scopes: []string{"read", "write"}, 38 | Endpoint: oauth2.Endpoint{ 39 | AuthURL: "https://example.com", 40 | TokenURL: ts.URL, 41 | }, 42 | }, 43 | } 44 | 45 | tok, err := conf.TokenSource(context.Background()).Token() 46 | if err != nil { 47 | t.Fatal(err) 48 | } 49 | if !tok.Valid() { 50 | t.Errorf("got invalid token: %v", tok) 51 | } 52 | if got, want := tok.AccessToken, "90d64460d14870c08c81352a05dedd3465940a7c"; got != want { 53 | t.Errorf("access token = %q; want %q", got, want) 54 | } 55 | if got, want := tok.TokenType, "Bearer"; got != want { 56 | t.Errorf("token type = %q; want %q", got, want) 57 | } 58 | if got := tok.Expiry.IsZero(); got { 59 | t.Errorf("token expiry = %v, want none", got) 60 | } 61 | } 62 | 63 | func TestJWTFetch_BadResponse(t *testing.T) { 64 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 65 | w.Header().Set("Content-Type", "application/json") 66 | w.Write([]byte(`{"token_type": "Bearer"}`)) 67 | })) 68 | defer ts.Close() 69 | 70 | conf := &Config{ 71 | BaseURL: "https://my.app.com", 72 | Subject: "useraccountId", 73 | Config: oauth2.Config{ 74 | ClientID: "super_secret_client_id", 75 | ClientSecret: "super_shared_secret", 76 | Scopes: []string{"read", "write"}, 77 | Endpoint: oauth2.Endpoint{ 78 | AuthURL: "https://example.com", 79 | TokenURL: ts.URL, 80 | }, 81 | }, 82 | } 83 | 84 | tok, err := conf.TokenSource(context.Background()).Token() 85 | if err != nil { 86 | t.Fatal(err) 87 | } 88 | if tok == nil { 89 | t.Fatalf("got nil token; want token") 90 | } 91 | if tok.Valid() { 92 | t.Errorf("got invalid token: %v", tok) 93 | } 94 | if got, want := tok.AccessToken, ""; got != want { 95 | t.Errorf("access token = %q; want %q", got, want) 96 | } 97 | if got, want := tok.TokenType, "Bearer"; got != want { 98 | t.Errorf("token type = %q; want %q", got, want) 99 | } 100 | } 101 | 102 | func TestJWTFetch_BadResponseType(t *testing.T) { 103 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 104 | w.Header().Set("Content-Type", "application/json") 105 | w.Write([]byte(`{"access_token":123, "token_type": "Bearer"}`)) 106 | })) 107 | defer ts.Close() 108 | 109 | conf := &Config{ 110 | BaseURL: "https://my.app.com", 111 | Subject: "useraccountId", 112 | Config: oauth2.Config{ 113 | ClientID: "super_secret_client_id", 114 | ClientSecret: "super_shared_secret", 115 | Endpoint: oauth2.Endpoint{ 116 | AuthURL: "https://example.com", 117 | TokenURL: ts.URL, 118 | }, 119 | }, 120 | } 121 | 122 | tok, err := conf.TokenSource(context.Background()).Token() 123 | if err == nil { 124 | t.Error("got a token; expected error") 125 | if got, want := tok.AccessToken, ""; got != want { 126 | t.Errorf("access token = %q; want %q", got, want) 127 | } 128 | } 129 | } 130 | 131 | func TestJWTFetch_Assertion(t *testing.T) { 132 | var assertion string 133 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 134 | r.ParseForm() 135 | assertion = r.Form.Get("assertion") 136 | 137 | w.Header().Set("Content-Type", "application/json") 138 | w.Write([]byte(`{ 139 | "access_token": "90d64460d14870c08c81352a05dedd3465940a7c", 140 | "token_type": "Bearer", 141 | "expires_in": 3600 142 | }`)) 143 | })) 144 | defer ts.Close() 145 | 146 | conf := &Config{ 147 | BaseURL: "https://my.app.com", 148 | Subject: "useraccountId", 149 | Config: oauth2.Config{ 150 | ClientID: "super_secret_client_id", 151 | ClientSecret: "super_shared_secret", 152 | Endpoint: oauth2.Endpoint{ 153 | AuthURL: "https://example.com", 154 | TokenURL: ts.URL, 155 | }, 156 | }, 157 | } 158 | 159 | _, err := conf.TokenSource(context.Background()).Token() 160 | if err != nil { 161 | t.Fatalf("Failed to fetch token: %v", err) 162 | } 163 | 164 | parts := strings.Split(assertion, ".") 165 | if len(parts) != 3 { 166 | t.Fatalf("assertion = %q; want 3 parts", assertion) 167 | } 168 | gotjson, err := base64.RawURLEncoding.DecodeString(parts[0]) 169 | if err != nil { 170 | t.Fatalf("invalid token header; err = %v", err) 171 | } 172 | 173 | got := jws.Header{} 174 | if err := json.Unmarshal(gotjson, &got); err != nil { 175 | t.Errorf("failed to unmarshal json token header = %q; err = %v", gotjson, err) 176 | } 177 | 178 | want := jws.Header{ 179 | Algorithm: "HS256", 180 | Typ: "JWT", 181 | } 182 | if got != want { 183 | t.Errorf("access token header = %q; want %q", got, want) 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /jws/jws.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Go 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 a partial implementation 6 | // of JSON Web Signature encoding and decoding. 7 | // It exists to support the [golang.org/x/oauth2] package. 8 | // 9 | // See RFC 7515. 10 | // 11 | // Deprecated: this package is not intended for public use and might be 12 | // removed in the future. It exists for internal use only. 13 | // Please switch to another JWS package or copy this package into your own 14 | // source tree. 15 | package jws // import "golang.org/x/oauth2/jws" 16 | 17 | import ( 18 | "bytes" 19 | "crypto" 20 | "crypto/rand" 21 | "crypto/rsa" 22 | "crypto/sha256" 23 | "encoding/base64" 24 | "encoding/json" 25 | "errors" 26 | "fmt" 27 | "strings" 28 | "time" 29 | ) 30 | 31 | // ClaimSet contains information about the JWT signature including the 32 | // permissions being requested (scopes), the target of the token, the issuer, 33 | // the time the token was issued, and the lifetime of the token. 34 | type ClaimSet struct { 35 | Iss string `json:"iss"` // email address of the client_id of the application making the access token request 36 | Scope string `json:"scope,omitempty"` // space-delimited list of the permissions the application requests 37 | Aud string `json:"aud"` // descriptor of the intended target of the assertion (Optional). 38 | Exp int64 `json:"exp"` // the expiration time of the assertion (seconds since Unix epoch) 39 | Iat int64 `json:"iat"` // the time the assertion was issued (seconds since Unix epoch) 40 | Typ string `json:"typ,omitempty"` // token type (Optional). 41 | 42 | // Email for which the application is requesting delegated access (Optional). 43 | Sub string `json:"sub,omitempty"` 44 | 45 | // The old name of Sub. Client keeps setting Prn to be 46 | // complaint with legacy OAuth 2.0 providers. (Optional) 47 | Prn string `json:"prn,omitempty"` 48 | 49 | // See http://tools.ietf.org/html/draft-jones-json-web-token-10#section-4.3 50 | // This array is marshalled using custom code (see (c *ClaimSet) encode()). 51 | PrivateClaims map[string]any `json:"-"` 52 | } 53 | 54 | func (c *ClaimSet) encode() (string, error) { 55 | // Reverting time back for machines whose time is not perfectly in sync. 56 | // If client machine's time is in the future according 57 | // to Google servers, an access token will not be issued. 58 | now := time.Now().Add(-10 * time.Second) 59 | if c.Iat == 0 { 60 | c.Iat = now.Unix() 61 | } 62 | if c.Exp == 0 { 63 | c.Exp = now.Add(time.Hour).Unix() 64 | } 65 | if c.Exp < c.Iat { 66 | return "", fmt.Errorf("jws: invalid Exp = %v; must be later than Iat = %v", c.Exp, c.Iat) 67 | } 68 | 69 | b, err := json.Marshal(c) 70 | if err != nil { 71 | return "", err 72 | } 73 | 74 | if len(c.PrivateClaims) == 0 { 75 | return base64.RawURLEncoding.EncodeToString(b), nil 76 | } 77 | 78 | // Marshal private claim set and then append it to b. 79 | prv, err := json.Marshal(c.PrivateClaims) 80 | if err != nil { 81 | return "", fmt.Errorf("jws: invalid map of private claims %v", c.PrivateClaims) 82 | } 83 | 84 | // Concatenate public and private claim JSON objects. 85 | if !bytes.HasSuffix(b, []byte{'}'}) { 86 | return "", fmt.Errorf("jws: invalid JSON %s", b) 87 | } 88 | if !bytes.HasPrefix(prv, []byte{'{'}) { 89 | return "", fmt.Errorf("jws: invalid JSON %s", prv) 90 | } 91 | b[len(b)-1] = ',' // Replace closing curly brace with a comma. 92 | b = append(b, prv[1:]...) // Append private claims. 93 | return base64.RawURLEncoding.EncodeToString(b), nil 94 | } 95 | 96 | // Header represents the header for the signed JWS payloads. 97 | type Header struct { 98 | // The algorithm used for signature. 99 | Algorithm string `json:"alg"` 100 | 101 | // Represents the token type. 102 | Typ string `json:"typ"` 103 | 104 | // The optional hint of which key is being used. 105 | KeyID string `json:"kid,omitempty"` 106 | } 107 | 108 | func (h *Header) encode() (string, error) { 109 | b, err := json.Marshal(h) 110 | if err != nil { 111 | return "", err 112 | } 113 | return base64.RawURLEncoding.EncodeToString(b), nil 114 | } 115 | 116 | // Decode decodes a claim set from a JWS payload. 117 | func Decode(payload string) (*ClaimSet, error) { 118 | // decode returned id token to get expiry 119 | _, claims, _, ok := parseToken(payload) 120 | if !ok { 121 | // TODO(jbd): Provide more context about the error. 122 | return nil, errors.New("jws: invalid token received") 123 | } 124 | decoded, err := base64.RawURLEncoding.DecodeString(claims) 125 | if err != nil { 126 | return nil, err 127 | } 128 | c := &ClaimSet{} 129 | err = json.NewDecoder(bytes.NewBuffer(decoded)).Decode(c) 130 | return c, err 131 | } 132 | 133 | // Signer returns a signature for the given data. 134 | type Signer func(data []byte) (sig []byte, err error) 135 | 136 | // EncodeWithSigner encodes a header and claim set with the provided signer. 137 | func EncodeWithSigner(header *Header, c *ClaimSet, sg Signer) (string, error) { 138 | head, err := header.encode() 139 | if err != nil { 140 | return "", err 141 | } 142 | cs, err := c.encode() 143 | if err != nil { 144 | return "", err 145 | } 146 | ss := fmt.Sprintf("%s.%s", head, cs) 147 | sig, err := sg([]byte(ss)) 148 | if err != nil { 149 | return "", err 150 | } 151 | return fmt.Sprintf("%s.%s", ss, base64.RawURLEncoding.EncodeToString(sig)), nil 152 | } 153 | 154 | // Encode encodes a signed JWS with provided header and claim set. 155 | // This invokes [EncodeWithSigner] using [crypto/rsa.SignPKCS1v15] with the given RSA private key. 156 | func Encode(header *Header, c *ClaimSet, key *rsa.PrivateKey) (string, error) { 157 | sg := func(data []byte) (sig []byte, err error) { 158 | h := sha256.New() 159 | h.Write(data) 160 | return rsa.SignPKCS1v15(rand.Reader, key, crypto.SHA256, h.Sum(nil)) 161 | } 162 | return EncodeWithSigner(header, c, sg) 163 | } 164 | 165 | // Verify tests whether the provided JWT token's signature was produced by the private key 166 | // associated with the supplied public key. 167 | func Verify(token string, key *rsa.PublicKey) error { 168 | header, claims, sig, ok := parseToken(token) 169 | if !ok { 170 | return errors.New("jws: invalid token received, token must have 3 parts") 171 | } 172 | signatureString, err := base64.RawURLEncoding.DecodeString(sig) 173 | if err != nil { 174 | return err 175 | } 176 | 177 | h := sha256.New() 178 | h.Write([]byte(header + tokenDelim + claims)) 179 | return rsa.VerifyPKCS1v15(key, crypto.SHA256, h.Sum(nil), signatureString) 180 | } 181 | 182 | func parseToken(s string) (header, claims, sig string, ok bool) { 183 | header, s, ok = strings.Cut(s, tokenDelim) 184 | if !ok { // no period found 185 | return "", "", "", false 186 | } 187 | claims, s, ok = strings.Cut(s, tokenDelim) 188 | if !ok { // only one period found 189 | return "", "", "", false 190 | } 191 | sig, _, ok = strings.Cut(s, tokenDelim) 192 | if ok { // three periods found 193 | return "", "", "", false 194 | } 195 | return header, claims, sig, true 196 | } 197 | 198 | const tokenDelim = "." 199 | -------------------------------------------------------------------------------- /jws/jws_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Go 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 6 | 7 | import ( 8 | "crypto/rand" 9 | "crypto/rsa" 10 | "net/http" 11 | "strings" 12 | "testing" 13 | ) 14 | 15 | func TestSignAndVerify(t *testing.T) { 16 | header := &Header{ 17 | Algorithm: "RS256", 18 | Typ: "JWT", 19 | } 20 | payload := &ClaimSet{ 21 | Iss: "http://google.com/", 22 | Aud: "", 23 | Exp: 3610, 24 | Iat: 10, 25 | } 26 | 27 | privateKey, err := rsa.GenerateKey(rand.Reader, 2048) 28 | if err != nil { 29 | t.Fatal(err) 30 | } 31 | 32 | token, err := Encode(header, payload, privateKey) 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | 37 | err = Verify(token, &privateKey.PublicKey) 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | } 42 | 43 | func TestVerifyFailsOnMalformedClaim(t *testing.T) { 44 | cases := []struct { 45 | desc string 46 | token string 47 | }{ 48 | { 49 | desc: "no periods", 50 | token: "aa", 51 | }, { 52 | desc: "only one period", 53 | token: "a.a", 54 | }, { 55 | desc: "more than two periods", 56 | token: "a.a.a.a", 57 | }, 58 | } 59 | for _, tc := range cases { 60 | f := func(t *testing.T) { 61 | err := Verify(tc.token, nil) 62 | if err == nil { 63 | t.Error("got no errors; want improperly formed JWT not to be verified") 64 | } 65 | } 66 | t.Run(tc.desc, f) 67 | } 68 | } 69 | 70 | func BenchmarkVerify(b *testing.B) { 71 | cases := []struct { 72 | desc string 73 | token string 74 | }{ 75 | { 76 | desc: "full of periods", 77 | token: strings.Repeat(".", http.DefaultMaxHeaderBytes), 78 | }, { 79 | desc: "two trailing periods", 80 | token: strings.Repeat("a", http.DefaultMaxHeaderBytes-2) + "..", 81 | }, 82 | } 83 | privateKey, err := rsa.GenerateKey(rand.Reader, 2048) 84 | if err != nil { 85 | b.Fatal(err) 86 | } 87 | for _, bc := range cases { 88 | f := func(b *testing.B) { 89 | b.ReportAllocs() 90 | b.ResetTimer() 91 | for range b.N { 92 | Verify(bc.token, &privateKey.PublicKey) 93 | } 94 | } 95 | b.Run(bc.desc, f) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /jwt/example_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Go 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 | "context" 9 | 10 | "golang.org/x/oauth2/jwt" 11 | ) 12 | 13 | func ExampleConfig() { 14 | ctx := context.Background() 15 | conf := &jwt.Config{ 16 | Email: "xxx@developer.com", 17 | // The contents of your RSA private key or your PEM file 18 | // that contains a private key. 19 | // If you have a p12 file instead, you 20 | // can use `openssl` to export the private key into a pem file. 21 | // 22 | // $ openssl pkcs12 -in key.p12 -out key.pem -nodes 23 | // 24 | // It only supports PEM containers with no passphrase. 25 | PrivateKey: []byte("-----BEGIN RSA PRIVATE KEY-----..."), 26 | Subject: "user@example.com", 27 | TokenURL: "https://provider.com/o/oauth2/token", 28 | } 29 | // Initiate an http.Client, the following GET request will be 30 | // authorized and authenticated on the behalf of user@example.com. 31 | client := conf.Client(ctx) 32 | client.Get("...") 33 | } 34 | -------------------------------------------------------------------------------- /jwt/jwt.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Go 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 | "context" 13 | "encoding/json" 14 | "fmt" 15 | "io" 16 | "net/http" 17 | "net/url" 18 | "strings" 19 | "time" 20 | 21 | "golang.org/x/oauth2" 22 | "golang.org/x/oauth2/internal" 23 | "golang.org/x/oauth2/jws" 24 | ) 25 | 26 | var ( 27 | defaultGrantType = "urn:ietf:params:oauth:grant-type:jwt-bearer" 28 | defaultHeader = &jws.Header{Algorithm: "RS256", Typ: "JWT"} 29 | ) 30 | 31 | // Config is the configuration for using JWT to fetch tokens, 32 | // commonly known as "two-legged OAuth 2.0". 33 | type Config struct { 34 | // Email is the OAuth client identifier used when communicating with 35 | // the configured OAuth provider. 36 | Email string 37 | 38 | // PrivateKey contains the contents of an RSA private key or the 39 | // contents of a PEM file that contains a private key. The provided 40 | // private key is used to sign JWT payloads. 41 | // PEM containers with a passphrase are not supported. 42 | // Use the following command to convert a PKCS 12 file into a PEM. 43 | // 44 | // $ openssl pkcs12 -in key.p12 -out key.pem -nodes 45 | // 46 | PrivateKey []byte 47 | 48 | // PrivateKeyID contains an optional hint indicating which key is being 49 | // used. 50 | PrivateKeyID string 51 | 52 | // Subject is the optional user to impersonate. 53 | Subject string 54 | 55 | // Scopes optionally specifies a list of requested permission scopes. 56 | Scopes []string 57 | 58 | // TokenURL is the endpoint required to complete the 2-legged JWT flow. 59 | TokenURL string 60 | 61 | // Expires optionally specifies how long the token is valid for. 62 | Expires time.Duration 63 | 64 | // Audience optionally specifies the intended audience of the 65 | // request. If empty, the value of TokenURL is used as the 66 | // intended audience. 67 | Audience string 68 | 69 | // PrivateClaims optionally specifies custom private claims in the JWT. 70 | // See http://tools.ietf.org/html/draft-jones-json-web-token-10#section-4.3 71 | PrivateClaims map[string]any 72 | 73 | // UseIDToken optionally specifies whether ID token should be used instead 74 | // of access token when the server returns both. 75 | UseIDToken bool 76 | } 77 | 78 | // TokenSource returns a JWT TokenSource using the configuration 79 | // in c and the HTTP client from the provided context. 80 | func (c *Config) TokenSource(ctx context.Context) oauth2.TokenSource { 81 | return oauth2.ReuseTokenSource(nil, jwtSource{ctx, c}) 82 | } 83 | 84 | // Client returns an HTTP client wrapping the context's 85 | // HTTP transport and adding Authorization headers with tokens 86 | // obtained from c. 87 | // 88 | // The returned client and its Transport should not be modified. 89 | func (c *Config) Client(ctx context.Context) *http.Client { 90 | return oauth2.NewClient(ctx, c.TokenSource(ctx)) 91 | } 92 | 93 | // jwtSource is a source that always does a signed JWT request for a token. 94 | // It should typically be wrapped with a reuseTokenSource. 95 | type jwtSource struct { 96 | ctx context.Context 97 | conf *Config 98 | } 99 | 100 | func (js jwtSource) Token() (*oauth2.Token, error) { 101 | pk, err := internal.ParseKey(js.conf.PrivateKey) 102 | if err != nil { 103 | return nil, err 104 | } 105 | hc := oauth2.NewClient(js.ctx, nil) 106 | claimSet := &jws.ClaimSet{ 107 | Iss: js.conf.Email, 108 | Scope: strings.Join(js.conf.Scopes, " "), 109 | Aud: js.conf.TokenURL, 110 | PrivateClaims: js.conf.PrivateClaims, 111 | } 112 | if subject := js.conf.Subject; subject != "" { 113 | claimSet.Sub = subject 114 | // prn is the old name of sub. Keep setting it 115 | // to be compatible with legacy OAuth 2.0 providers. 116 | claimSet.Prn = subject 117 | } 118 | if t := js.conf.Expires; t > 0 { 119 | claimSet.Exp = time.Now().Add(t).Unix() 120 | } 121 | if aud := js.conf.Audience; aud != "" { 122 | claimSet.Aud = aud 123 | } 124 | h := *defaultHeader 125 | h.KeyID = js.conf.PrivateKeyID 126 | payload, err := jws.Encode(&h, claimSet, pk) 127 | if err != nil { 128 | return nil, err 129 | } 130 | v := url.Values{} 131 | v.Set("grant_type", defaultGrantType) 132 | v.Set("assertion", payload) 133 | resp, err := hc.PostForm(js.conf.TokenURL, v) 134 | if err != nil { 135 | return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err) 136 | } 137 | defer resp.Body.Close() 138 | body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) 139 | if err != nil { 140 | return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err) 141 | } 142 | if c := resp.StatusCode; c < 200 || c > 299 { 143 | return nil, &oauth2.RetrieveError{ 144 | Response: resp, 145 | Body: body, 146 | } 147 | } 148 | // tokenRes is the JSON response body. 149 | var tokenRes struct { 150 | oauth2.Token 151 | IDToken string `json:"id_token"` 152 | } 153 | if err := json.Unmarshal(body, &tokenRes); err != nil { 154 | return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err) 155 | } 156 | token := &oauth2.Token{ 157 | AccessToken: tokenRes.AccessToken, 158 | TokenType: tokenRes.TokenType, 159 | } 160 | raw := make(map[string]any) 161 | json.Unmarshal(body, &raw) // no error checks for optional fields 162 | token = token.WithExtra(raw) 163 | 164 | if secs := tokenRes.ExpiresIn; secs > 0 { 165 | token.Expiry = time.Now().Add(time.Duration(secs) * time.Second) 166 | } 167 | if v := tokenRes.IDToken; v != "" { 168 | // decode returned id token to get expiry 169 | claimSet, err := jws.Decode(v) 170 | if err != nil { 171 | return nil, fmt.Errorf("oauth2: error decoding JWT token: %v", err) 172 | } 173 | token.Expiry = time.Unix(claimSet.Exp, 0) 174 | } 175 | if js.conf.UseIDToken { 176 | if tokenRes.IDToken == "" { 177 | return nil, fmt.Errorf("oauth2: response doesn't have JWT token") 178 | } 179 | token.AccessToken = tokenRes.IDToken 180 | } 181 | return token, nil 182 | } 183 | -------------------------------------------------------------------------------- /kakao/kakao.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Go 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 kakao provides constants for using OAuth2 to access Kakao. 6 | package kakao // import "golang.org/x/oauth2/kakao" 7 | 8 | import ( 9 | "golang.org/x/oauth2" 10 | ) 11 | 12 | // Endpoint is Kakao's OAuth 2.0 endpoint. 13 | var Endpoint = oauth2.Endpoint{ 14 | AuthURL: "https://kauth.kakao.com/oauth/authorize", 15 | TokenURL: "https://kauth.kakao.com/oauth/token", 16 | } 17 | -------------------------------------------------------------------------------- /linkedin/linkedin.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Go 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 "golang.org/x/oauth2/linkedin" 7 | 8 | import ( 9 | "golang.org/x/oauth2" 10 | ) 11 | 12 | // Endpoint is LinkedIn's OAuth 2.0 endpoint. 13 | var Endpoint = oauth2.Endpoint{ 14 | AuthURL: "https://www.linkedin.com/oauth/v2/authorization", 15 | TokenURL: "https://www.linkedin.com/oauth/v2/accessToken", 16 | AuthStyle: oauth2.AuthStyleInParams, 17 | } 18 | -------------------------------------------------------------------------------- /mailchimp/mailchimp.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Go 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 mailchimp provides constants for using OAuth2 to access MailChimp. 6 | package mailchimp // import "golang.org/x/oauth2/mailchimp" 7 | 8 | import ( 9 | "golang.org/x/oauth2" 10 | ) 11 | 12 | // Endpoint is MailChimp's OAuth 2.0 endpoint. 13 | // See http://developer.mailchimp.com/documentation/mailchimp/guides/how-to-use-oauth2/ 14 | var Endpoint = oauth2.Endpoint{ 15 | AuthURL: "https://login.mailchimp.com/oauth2/authorize", 16 | TokenURL: "https://login.mailchimp.com/oauth2/token", 17 | } 18 | -------------------------------------------------------------------------------- /mailru/mailru.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Go 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 mailru provides constants for using OAuth2 to access Mail.Ru. 6 | package mailru // import "golang.org/x/oauth2/mailru" 7 | 8 | import ( 9 | "golang.org/x/oauth2" 10 | ) 11 | 12 | // Endpoint is Mail.Ru's OAuth 2.0 endpoint. 13 | var Endpoint = oauth2.Endpoint{ 14 | AuthURL: "https://o2.mail.ru/login", 15 | TokenURL: "https://o2.mail.ru/token", 16 | } 17 | -------------------------------------------------------------------------------- /mediamath/mediamath.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Go 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 mediamath provides constants for using OAuth2 to access MediaMath. 6 | package mediamath // import "golang.org/x/oauth2/mediamath" 7 | 8 | import ( 9 | "golang.org/x/oauth2" 10 | ) 11 | 12 | // Endpoint is MediaMath's OAuth 2.0 endpoint for production. 13 | var Endpoint = oauth2.Endpoint{ 14 | AuthURL: "https://api.mediamath.com/oauth2/v1.0/authorize", 15 | TokenURL: "https://api.mediamath.com/oauth2/v1.0/token", 16 | } 17 | 18 | // SandboxEndpoint is MediaMath's OAuth 2.0 endpoint for sandbox. 19 | var SandboxEndpoint = oauth2.Endpoint{ 20 | AuthURL: "https://t1sandbox.mediamath.com/oauth2/v1.0/authorize", 21 | TokenURL: "https://t1sandbox.mediamath.com/oauth2/v1.0/token", 22 | } 23 | -------------------------------------------------------------------------------- /microsoft/microsoft.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Go 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 microsoft provides constants for using OAuth2 to access Windows Live ID. 6 | package microsoft // import "golang.org/x/oauth2/microsoft" 7 | 8 | import ( 9 | "golang.org/x/oauth2" 10 | ) 11 | 12 | // LiveConnectEndpoint is Windows's Live ID OAuth 2.0 endpoint. 13 | var LiveConnectEndpoint = oauth2.Endpoint{ 14 | AuthURL: "https://login.live.com/oauth20_authorize.srf", 15 | TokenURL: "https://login.live.com/oauth20_token.srf", 16 | } 17 | 18 | // AzureADEndpoint returns a new oauth2.Endpoint for the given tenant at Azure Active Directory. 19 | // If tenant is empty, it uses the tenant called `common`. 20 | // 21 | // For more information see: 22 | // https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-v2-protocols#endpoints 23 | func AzureADEndpoint(tenant string) oauth2.Endpoint { 24 | if tenant == "" { 25 | tenant = "common" 26 | } 27 | return oauth2.Endpoint{ 28 | AuthURL: "https://login.microsoftonline.com/" + tenant + "/oauth2/v2.0/authorize", 29 | TokenURL: "https://login.microsoftonline.com/" + tenant + "/oauth2/v2.0/token", 30 | DeviceAuthURL: "https://login.microsoftonline.com/" + tenant + "/oauth2/v2.0/devicecode", 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /nokiahealth/nokiahealth.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 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 nokiahealth provides constants for using OAuth2 to access the Nokia Health Mate API. 6 | package nokiahealth 7 | 8 | import ( 9 | "golang.org/x/oauth2" 10 | ) 11 | 12 | // Endpoint is Nokia Health Mate's OAuth 2.0 endpoint. 13 | var Endpoint = oauth2.Endpoint{ 14 | AuthURL: "https://account.health.nokia.com/oauth2_user/authorize2", 15 | TokenURL: "https://account.health.nokia.com/oauth2/token", 16 | } 17 | -------------------------------------------------------------------------------- /odnoklassniki/odnoklassniki.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Go 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 "golang.org/x/oauth2/odnoklassniki" 7 | 8 | import ( 9 | "golang.org/x/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 | -------------------------------------------------------------------------------- /paypal/paypal.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Go 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 "golang.org/x/oauth2/paypal" 7 | 8 | import ( 9 | "golang.org/x/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 | -------------------------------------------------------------------------------- /pkce.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The Go 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 | "crypto/rand" 9 | "crypto/sha256" 10 | "encoding/base64" 11 | "net/url" 12 | ) 13 | 14 | const ( 15 | codeChallengeKey = "code_challenge" 16 | codeChallengeMethodKey = "code_challenge_method" 17 | codeVerifierKey = "code_verifier" 18 | ) 19 | 20 | // GenerateVerifier generates a PKCE code verifier with 32 octets of randomness. 21 | // This follows recommendations in RFC 7636. 22 | // 23 | // A fresh verifier should be generated for each authorization. 24 | // The resulting verifier should be passed to [Config.AuthCodeURL] or [Config.DeviceAuth] 25 | // with [S256ChallengeOption], and to [Config.Exchange] or [Config.DeviceAccessToken] 26 | // with [VerifierOption]. 27 | func GenerateVerifier() string { 28 | // "RECOMMENDED that the output of a suitable random number generator be 29 | // used to create a 32-octet sequence. The octet sequence is then 30 | // base64url-encoded to produce a 43-octet URL-safe string to use as the 31 | // code verifier." 32 | // https://datatracker.ietf.org/doc/html/rfc7636#section-4.1 33 | data := make([]byte, 32) 34 | if _, err := rand.Read(data); err != nil { 35 | panic(err) 36 | } 37 | return base64.RawURLEncoding.EncodeToString(data) 38 | } 39 | 40 | // VerifierOption returns a PKCE code verifier [AuthCodeOption]. It should only be 41 | // passed to [Config.Exchange] or [Config.DeviceAccessToken]. 42 | func VerifierOption(verifier string) AuthCodeOption { 43 | return setParam{k: codeVerifierKey, v: verifier} 44 | } 45 | 46 | // S256ChallengeFromVerifier returns a PKCE code challenge derived from verifier with method S256. 47 | // 48 | // Prefer to use [S256ChallengeOption] where possible. 49 | func S256ChallengeFromVerifier(verifier string) string { 50 | sha := sha256.Sum256([]byte(verifier)) 51 | return base64.RawURLEncoding.EncodeToString(sha[:]) 52 | } 53 | 54 | // S256ChallengeOption derives a PKCE code challenge derived from verifier with 55 | // method S256. It should be passed to [Config.AuthCodeURL] or [Config.DeviceAuth] 56 | // only. 57 | func S256ChallengeOption(verifier string) AuthCodeOption { 58 | return challengeOption{ 59 | challenge_method: "S256", 60 | challenge: S256ChallengeFromVerifier(verifier), 61 | } 62 | } 63 | 64 | type challengeOption struct{ challenge_method, challenge string } 65 | 66 | func (p challengeOption) setValue(m url.Values) { 67 | m.Set(codeChallengeMethodKey, p.challenge_method) 68 | m.Set(codeChallengeKey, p.challenge) 69 | } 70 | -------------------------------------------------------------------------------- /slack/slack.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Go 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 slack provides constants for using OAuth2 to access Slack. 6 | package slack // import "golang.org/x/oauth2/slack" 7 | 8 | import ( 9 | "golang.org/x/oauth2" 10 | ) 11 | 12 | // Endpoint is Slack's OAuth 2.0 endpoint. 13 | var Endpoint = oauth2.Endpoint{ 14 | AuthURL: "https://slack.com/oauth/authorize", 15 | TokenURL: "https://slack.com/api/oauth.access", 16 | } 17 | -------------------------------------------------------------------------------- /spotify/spotify.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Go 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 spotify provides constants for using OAuth2 to access Spotify. 6 | package spotify // import "golang.org/x/oauth2/spotify" 7 | 8 | import ( 9 | "golang.org/x/oauth2" 10 | ) 11 | 12 | // Endpoint is Spotify's OAuth 2.0 endpoint. 13 | var Endpoint = oauth2.Endpoint{ 14 | AuthURL: "https://accounts.spotify.com/authorize", 15 | TokenURL: "https://accounts.spotify.com/api/token", 16 | } 17 | -------------------------------------------------------------------------------- /stackoverflow/stackoverflow.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Go 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 stackoverflow provides constants for using OAuth2 to access Stack Overflow. 6 | package stackoverflow // import "golang.org/x/oauth2/stackoverflow" 7 | 8 | import ( 9 | "golang.org/x/oauth2" 10 | ) 11 | 12 | // Endpoint is Stack Overflow's OAuth 2.0 endpoint. 13 | var Endpoint = oauth2.Endpoint{ 14 | AuthURL: "https://stackoverflow.com/oauth", 15 | TokenURL: "https://stackoverflow.com/oauth/access_token", 16 | } 17 | -------------------------------------------------------------------------------- /token.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Go 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 | "context" 9 | "fmt" 10 | "net/http" 11 | "net/url" 12 | "strconv" 13 | "strings" 14 | "time" 15 | 16 | "golang.org/x/oauth2/internal" 17 | ) 18 | 19 | // defaultExpiryDelta determines how earlier a token should be considered 20 | // expired than its actual expiration time. It is used to avoid late 21 | // expirations due to client-server time mismatches. 22 | const defaultExpiryDelta = 10 * time.Second 23 | 24 | // Token represents the credentials used to authorize 25 | // the requests to access protected resources on the OAuth 2.0 26 | // provider's backend. 27 | // 28 | // Most users of this package should not access fields of Token 29 | // directly. They're exported mostly for use by related packages 30 | // implementing derivative OAuth2 flows. 31 | type Token struct { 32 | // AccessToken is the token that authorizes and authenticates 33 | // the requests. 34 | AccessToken string `json:"access_token"` 35 | 36 | // TokenType is the type of token. 37 | // The Type method returns either this or "Bearer", the default. 38 | TokenType string `json:"token_type,omitempty"` 39 | 40 | // RefreshToken is a token that's used by the application 41 | // (as opposed to the user) to refresh the access token 42 | // if it expires. 43 | RefreshToken string `json:"refresh_token,omitempty"` 44 | 45 | // Expiry is the optional expiration time of the access token. 46 | // 47 | // If zero, [TokenSource] implementations will reuse the same 48 | // token forever and RefreshToken or equivalent 49 | // mechanisms for that TokenSource will not be used. 50 | Expiry time.Time `json:"expiry,omitempty"` 51 | 52 | // ExpiresIn is the OAuth2 wire format "expires_in" field, 53 | // which specifies how many seconds later the token expires, 54 | // relative to an unknown time base approximately around "now". 55 | // It is the application's responsibility to populate 56 | // `Expiry` from `ExpiresIn` when required. 57 | ExpiresIn int64 `json:"expires_in,omitempty"` 58 | 59 | // raw optionally contains extra metadata from the server 60 | // when updating a token. 61 | raw any 62 | 63 | // expiryDelta is used to calculate when a token is considered 64 | // expired, by subtracting from Expiry. If zero, defaultExpiryDelta 65 | // is used. 66 | expiryDelta time.Duration 67 | } 68 | 69 | // Type returns t.TokenType if non-empty, else "Bearer". 70 | func (t *Token) Type() string { 71 | if strings.EqualFold(t.TokenType, "bearer") { 72 | return "Bearer" 73 | } 74 | if strings.EqualFold(t.TokenType, "mac") { 75 | return "MAC" 76 | } 77 | if strings.EqualFold(t.TokenType, "basic") { 78 | return "Basic" 79 | } 80 | if t.TokenType != "" { 81 | return t.TokenType 82 | } 83 | return "Bearer" 84 | } 85 | 86 | // SetAuthHeader sets the Authorization header to r using the access 87 | // token in t. 88 | // 89 | // This method is unnecessary when using [Transport] or an HTTP Client 90 | // returned by this package. 91 | func (t *Token) SetAuthHeader(r *http.Request) { 92 | r.Header.Set("Authorization", t.Type()+" "+t.AccessToken) 93 | } 94 | 95 | // WithExtra returns a new [Token] that's a clone of t, but using the 96 | // provided raw extra map. This is only intended for use by packages 97 | // implementing derivative OAuth2 flows. 98 | func (t *Token) WithExtra(extra any) *Token { 99 | t2 := new(Token) 100 | *t2 = *t 101 | t2.raw = extra 102 | return t2 103 | } 104 | 105 | // Extra returns an extra field. 106 | // Extra fields are key-value pairs returned by the server as a 107 | // part of the token retrieval response. 108 | func (t *Token) Extra(key string) any { 109 | if raw, ok := t.raw.(map[string]any); ok { 110 | return raw[key] 111 | } 112 | 113 | vals, ok := t.raw.(url.Values) 114 | if !ok { 115 | return nil 116 | } 117 | 118 | v := vals.Get(key) 119 | switch s := strings.TrimSpace(v); strings.Count(s, ".") { 120 | case 0: // Contains no "."; try to parse as int 121 | if i, err := strconv.ParseInt(s, 10, 64); err == nil { 122 | return i 123 | } 124 | case 1: // Contains a single "."; try to parse as float 125 | if f, err := strconv.ParseFloat(s, 64); err == nil { 126 | return f 127 | } 128 | } 129 | 130 | return v 131 | } 132 | 133 | // timeNow is time.Now but pulled out as a variable for tests. 134 | var timeNow = time.Now 135 | 136 | // expired reports whether the token is expired. 137 | // t must be non-nil. 138 | func (t *Token) expired() bool { 139 | if t.Expiry.IsZero() { 140 | return false 141 | } 142 | 143 | expiryDelta := defaultExpiryDelta 144 | if t.expiryDelta != 0 { 145 | expiryDelta = t.expiryDelta 146 | } 147 | return t.Expiry.Round(0).Add(-expiryDelta).Before(timeNow()) 148 | } 149 | 150 | // Valid reports whether t is non-nil, has an AccessToken, and is not expired. 151 | func (t *Token) Valid() bool { 152 | return t != nil && t.AccessToken != "" && !t.expired() 153 | } 154 | 155 | // tokenFromInternal maps an *internal.Token struct into 156 | // a *Token struct. 157 | func tokenFromInternal(t *internal.Token) *Token { 158 | if t == nil { 159 | return nil 160 | } 161 | return &Token{ 162 | AccessToken: t.AccessToken, 163 | TokenType: t.TokenType, 164 | RefreshToken: t.RefreshToken, 165 | Expiry: t.Expiry, 166 | ExpiresIn: t.ExpiresIn, 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, internal.AuthStyle(c.Endpoint.AuthStyle), c.authStyleCache.Get()) 176 | if err != nil { 177 | if rErr, ok := err.(*internal.RetrieveError); ok { 178 | return nil, (*RetrieveError)(rErr) 179 | } 180 | return nil, err 181 | } 182 | return tokenFromInternal(tk), nil 183 | } 184 | 185 | // RetrieveError is the error returned when the token endpoint returns a 186 | // non-2XX HTTP status code or populates RFC 6749's 'error' parameter. 187 | // https://datatracker.ietf.org/doc/html/rfc6749#section-5.2 188 | type RetrieveError struct { 189 | Response *http.Response 190 | // Body is the body that was consumed by reading Response.Body. 191 | // It may be truncated. 192 | Body []byte 193 | // ErrorCode is RFC 6749's 'error' parameter. 194 | ErrorCode string 195 | // ErrorDescription is RFC 6749's 'error_description' parameter. 196 | ErrorDescription string 197 | // ErrorURI is RFC 6749's 'error_uri' parameter. 198 | ErrorURI string 199 | } 200 | 201 | func (r *RetrieveError) Error() string { 202 | if r.ErrorCode != "" { 203 | s := fmt.Sprintf("oauth2: %q", r.ErrorCode) 204 | if r.ErrorDescription != "" { 205 | s += fmt.Sprintf(" %q", r.ErrorDescription) 206 | } 207 | if r.ErrorURI != "" { 208 | s += fmt.Sprintf(" %q", r.ErrorURI) 209 | } 210 | return s 211 | } 212 | return fmt.Sprintf("oauth2: cannot fetch token: %v\nResponse: %s", r.Response.Status, r.Body) 213 | } 214 | -------------------------------------------------------------------------------- /token_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Go 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 any 16 | want any 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]any) 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 | timeNow = func() time.Time { return now } 38 | defer func() { timeNow = time.Now }() 39 | 40 | cases := []struct { 41 | name string 42 | tok *Token 43 | want bool 44 | }{ 45 | {name: "12 seconds", tok: &Token{Expiry: now.Add(12 * time.Second)}, want: false}, 46 | {name: "10 seconds", tok: &Token{Expiry: now.Add(defaultExpiryDelta)}, want: false}, 47 | {name: "10 seconds-1ns", tok: &Token{Expiry: now.Add(defaultExpiryDelta - 1*time.Nanosecond)}, want: true}, 48 | {name: "-1 hour", tok: &Token{Expiry: now.Add(-1 * time.Hour)}, want: true}, 49 | {name: "12 seconds, custom expiryDelta", tok: &Token{Expiry: now.Add(12 * time.Second), expiryDelta: time.Second * 5}, want: false}, 50 | {name: "5 seconds, custom expiryDelta", tok: &Token{Expiry: now.Add(time.Second * 5), expiryDelta: time.Second * 5}, want: false}, 51 | {name: "5 seconds-1ns, custom expiryDelta", tok: &Token{Expiry: now.Add(time.Second*5 - 1*time.Nanosecond), expiryDelta: time.Second * 5}, want: true}, 52 | {name: "-1 hour, custom expiryDelta", tok: &Token{Expiry: now.Add(-1 * time.Hour), expiryDelta: time.Second * 5}, want: true}, 53 | } 54 | for _, tc := range cases { 55 | if got, want := tc.tok.expired(), tc.want; got != want { 56 | t.Errorf("expired (%q) = %v; want %v", tc.name, got, want) 57 | } 58 | } 59 | } 60 | 61 | func TestTokenTypeMethod(t *testing.T) { 62 | cases := []struct { 63 | name string 64 | tok *Token 65 | want string 66 | }{ 67 | {name: "bearer-mixed_case", tok: &Token{TokenType: "beAREr"}, want: "Bearer"}, 68 | {name: "default-bearer", tok: &Token{}, want: "Bearer"}, 69 | {name: "basic", tok: &Token{TokenType: "basic"}, want: "Basic"}, 70 | {name: "basic-capitalized", tok: &Token{TokenType: "Basic"}, want: "Basic"}, 71 | {name: "mac", tok: &Token{TokenType: "mac"}, want: "MAC"}, 72 | {name: "mac-caps", tok: &Token{TokenType: "MAC"}, want: "MAC"}, 73 | {name: "mac-mixed_case", tok: &Token{TokenType: "mAc"}, want: "MAC"}, 74 | } 75 | for _, tc := range cases { 76 | if got, want := tc.tok.Type(), tc.want; got != want { 77 | t.Errorf("TokenType(%q) = %v; want %v", tc.name, got, want) 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /transport.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Go 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 | "log" 10 | "net/http" 11 | "sync" 12 | ) 13 | 14 | // Transport is an [http.RoundTripper] that makes OAuth 2.0 HTTP requests, 15 | // wrapping a base [http.RoundTripper] and adding an Authorization header 16 | // with a token from the supplied [TokenSource]. 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 | 30 | // RoundTrip authorizes and authenticates the request with an 31 | // access token from Transport's Source. 32 | func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) { 33 | reqBodyClosed := false 34 | if req.Body != nil { 35 | defer func() { 36 | if !reqBodyClosed { 37 | req.Body.Close() 38 | } 39 | }() 40 | } 41 | 42 | if t.Source == nil { 43 | return nil, errors.New("oauth2: Transport's Source is nil") 44 | } 45 | token, err := t.Source.Token() 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | req2 := req.Clone(req.Context()) 51 | token.SetAuthHeader(req2) 52 | 53 | // req.Body is assumed to be closed by the base RoundTripper. 54 | reqBodyClosed = true 55 | return t.base().RoundTrip(req2) 56 | } 57 | 58 | var cancelOnce sync.Once 59 | 60 | // CancelRequest does nothing. It used to be a legacy cancellation mechanism 61 | // but now only it only logs on first use to warn that it's deprecated. 62 | // 63 | // Deprecated: use contexts for cancellation instead. 64 | func (t *Transport) CancelRequest(req *http.Request) { 65 | cancelOnce.Do(func() { 66 | log.Printf("deprecated: golang.org/x/oauth2: Transport.CancelRequest no longer does anything; use contexts") 67 | }) 68 | } 69 | 70 | func (t *Transport) base() http.RoundTripper { 71 | if t.Base != nil { 72 | return t.Base 73 | } 74 | return http.DefaultTransport 75 | } 76 | -------------------------------------------------------------------------------- /transport_test.go: -------------------------------------------------------------------------------- 1 | package oauth2 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | func TestTransportNilTokenSource(t *testing.T) { 13 | tr := &Transport{} 14 | server := newMockServer(func(w http.ResponseWriter, r *http.Request) {}) 15 | defer server.Close() 16 | client := &http.Client{Transport: tr} 17 | resp, err := client.Get(server.URL) 18 | if err == nil { 19 | t.Errorf("got no errors, want an error with nil token source") 20 | } 21 | if resp != nil { 22 | t.Errorf("Response = %v; want nil", resp) 23 | } 24 | } 25 | 26 | type readCloseCounter struct { 27 | CloseCount int 28 | ReadErr error 29 | } 30 | 31 | func (r *readCloseCounter) Read(b []byte) (int, error) { 32 | return 0, r.ReadErr 33 | } 34 | 35 | func (r *readCloseCounter) Close() error { 36 | r.CloseCount++ 37 | return nil 38 | } 39 | 40 | func TestTransportCloseRequestBody(t *testing.T) { 41 | tr := &Transport{} 42 | server := newMockServer(func(w http.ResponseWriter, r *http.Request) {}) 43 | defer server.Close() 44 | client := &http.Client{Transport: tr} 45 | body := &readCloseCounter{ 46 | ReadErr: errors.New("readCloseCounter.Read not implemented"), 47 | } 48 | resp, err := client.Post(server.URL, "application/json", body) 49 | if err == nil { 50 | t.Errorf("got no errors, want an error with nil token source") 51 | } 52 | if resp != nil { 53 | t.Errorf("Response = %v; want nil", resp) 54 | } 55 | if expected := 1; body.CloseCount != expected { 56 | t.Errorf("Body was closed %d times, expected %d", body.CloseCount, expected) 57 | } 58 | } 59 | 60 | func TestTransportCloseRequestBodySuccess(t *testing.T) { 61 | tr := &Transport{ 62 | Source: StaticTokenSource(&Token{ 63 | AccessToken: "abc", 64 | }), 65 | } 66 | server := newMockServer(func(w http.ResponseWriter, r *http.Request) {}) 67 | defer server.Close() 68 | client := &http.Client{Transport: tr} 69 | body := &readCloseCounter{ 70 | ReadErr: io.EOF, 71 | } 72 | resp, err := client.Post(server.URL, "application/json", body) 73 | if err != nil { 74 | t.Errorf("got error %v; expected none", err) 75 | } 76 | if resp == nil { 77 | t.Errorf("Response is nil; expected non-nil") 78 | } 79 | if expected := 1; body.CloseCount != expected { 80 | t.Errorf("Body was closed %d times, expected %d", body.CloseCount, expected) 81 | } 82 | } 83 | 84 | func TestTransportTokenSource(t *testing.T) { 85 | tr := &Transport{ 86 | Source: StaticTokenSource(&Token{ 87 | AccessToken: "abc", 88 | }), 89 | } 90 | server := newMockServer(func(w http.ResponseWriter, r *http.Request) { 91 | if got, want := r.Header.Get("Authorization"), "Bearer abc"; got != want { 92 | t.Errorf("Authorization header = %q; want %q", got, want) 93 | } 94 | }) 95 | defer server.Close() 96 | client := &http.Client{Transport: tr} 97 | res, err := client.Get(server.URL) 98 | if err != nil { 99 | t.Fatal(err) 100 | } 101 | res.Body.Close() 102 | } 103 | 104 | // Test for case-sensitive token types, per https://github.com/golang/oauth2/issues/113 105 | func TestTransportTokenSourceTypes(t *testing.T) { 106 | const val = "abc" 107 | tests := []struct { 108 | key string 109 | val string 110 | want string 111 | }{ 112 | {key: "bearer", val: val, want: "Bearer abc"}, 113 | {key: "mac", val: val, want: "MAC abc"}, 114 | {key: "basic", val: val, want: "Basic abc"}, 115 | } 116 | for _, tc := range tests { 117 | tr := &Transport{ 118 | Source: StaticTokenSource(&Token{ 119 | AccessToken: tc.val, 120 | TokenType: tc.key, 121 | }), 122 | } 123 | server := newMockServer(func(w http.ResponseWriter, r *http.Request) { 124 | if got, want := r.Header.Get("Authorization"), tc.want; got != want { 125 | t.Errorf("Authorization header (%q) = %q; want %q", val, got, want) 126 | } 127 | }) 128 | defer server.Close() 129 | client := &http.Client{Transport: tr} 130 | res, err := client.Get(server.URL) 131 | if err != nil { 132 | t.Fatal(err) 133 | } 134 | res.Body.Close() 135 | } 136 | } 137 | 138 | func TestTokenValidNoAccessToken(t *testing.T) { 139 | token := &Token{} 140 | if token.Valid() { 141 | t.Errorf("got valid with no access token; want invalid") 142 | } 143 | } 144 | 145 | func TestExpiredWithExpiry(t *testing.T) { 146 | token := &Token{ 147 | Expiry: time.Now().Add(-5 * time.Hour), 148 | } 149 | if token.Valid() { 150 | t.Errorf("got valid with expired token; want invalid") 151 | } 152 | } 153 | 154 | func newMockServer(handler func(w http.ResponseWriter, r *http.Request)) *httptest.Server { 155 | return httptest.NewServer(http.HandlerFunc(handler)) 156 | } 157 | -------------------------------------------------------------------------------- /twitch/twitch.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Go 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 twitch provides constants for using OAuth2 to access Twitch. 6 | package twitch // import "golang.org/x/oauth2/twitch" 7 | 8 | import ( 9 | "golang.org/x/oauth2" 10 | ) 11 | 12 | // Endpoint is Twitch's OAuth 2.0 endpoint. 13 | // 14 | // For more information see: 15 | // https://dev.twitch.tv/docs/authentication 16 | var Endpoint = oauth2.Endpoint{ 17 | AuthURL: "https://id.twitch.tv/oauth2/authorize", 18 | TokenURL: "https://id.twitch.tv/oauth2/token", 19 | } 20 | -------------------------------------------------------------------------------- /uber/uber.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Go 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 uber provides constants for using OAuth2 to access Uber. 6 | package uber // import "golang.org/x/oauth2/uber" 7 | 8 | import ( 9 | "golang.org/x/oauth2" 10 | ) 11 | 12 | // Endpoint is Uber's OAuth 2.0 endpoint. 13 | var Endpoint = oauth2.Endpoint{ 14 | AuthURL: "https://login.uber.com/oauth/v2/authorize", 15 | TokenURL: "https://login.uber.com/oauth/v2/token", 16 | } 17 | -------------------------------------------------------------------------------- /vk/vk.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Go 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 "golang.org/x/oauth2/vk" 7 | 8 | import ( 9 | "golang.org/x/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 | -------------------------------------------------------------------------------- /yahoo/yahoo.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Go 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 yahoo provides constants for using OAuth2 to access Yahoo. 6 | package yahoo // import "golang.org/x/oauth2/yahoo" 7 | 8 | import ( 9 | "golang.org/x/oauth2" 10 | ) 11 | 12 | // Endpoint is Yahoo's OAuth 2.0 endpoint. 13 | // See https://developer.yahoo.com/oauth2/guide/ 14 | var Endpoint = oauth2.Endpoint{ 15 | AuthURL: "https://api.login.yahoo.com/oauth2/request_auth", 16 | TokenURL: "https://api.login.yahoo.com/oauth2/get_token", 17 | } 18 | -------------------------------------------------------------------------------- /yandex/yandex.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Go 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 yandex provides constants for using OAuth2 to access Yandex APIs. 6 | package yandex // import "golang.org/x/oauth2/yandex" 7 | 8 | import ( 9 | "golang.org/x/oauth2" 10 | ) 11 | 12 | // Endpoint is the Yandex OAuth 2.0 endpoint. 13 | var Endpoint = oauth2.Endpoint{ 14 | AuthURL: "https://oauth.yandex.com/authorize", 15 | TokenURL: "https://oauth.yandex.com/token", 16 | } 17 | --------------------------------------------------------------------------------