diff --git a/LICENSE b/LICENSE index 6a66aea5e..2a7cf70da 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2009 The Go Authors. All rights reserved. +Copyright 2009 The Go Authors. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are @@ -10,7 +10,7 @@ notice, this list of conditions and the following disclaimer. copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - * Neither the name of Google Inc. nor the names of its + * Neither the name of Google LLC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. diff --git a/README.md b/README.md index 781770c20..48dbb9d84 100644 --- a/README.md +++ b/README.md @@ -5,15 +5,6 @@ oauth2 package contains a client implementation for OAuth 2.0 spec. -## Installation - -~~~~ -go get golang.org/x/oauth2 -~~~~ - -Or you can manually git clone the repository to -`$(go env GOPATH)/src/golang.org/x/oauth2`. - See pkg.go.dev for further documentation and examples. * [pkg.go.dev/golang.org/x/oauth2](https://pkg.go.dev/golang.org/x/oauth2) @@ -33,7 +24,11 @@ The main issue tracker for the oauth2 repository is located at https://github.com/golang/oauth2/issues. This repository uses Gerrit for code changes. To learn how to submit changes to -this repository, see https://golang.org/doc/contribute.html. In particular: +this repository, see https://go.dev/doc/contribute. + +The git repository is https://go.googlesource.com/oauth2. + +Note: * Excluding trivial changes, all contributions should be connected to an existing issue. * API changes must go through the [change proposal process](https://go.dev/s/proposal-process) before they can be accepted. diff --git a/clientcredentials/clientcredentials.go b/clientcredentials/clientcredentials.go index 2459d069f..51121a3d5 100644 --- a/clientcredentials/clientcredentials.go +++ b/clientcredentials/clientcredentials.go @@ -37,7 +37,7 @@ type Config struct { // URL. This is a constant specific to each server. TokenURL string - // Scope specifies optional requested permissions. + // Scopes specifies optional requested permissions. Scopes []string // EndpointParams specifies additional parameters for requests to the token endpoint. diff --git a/endpoints/endpoints.go b/endpoints/endpoints.go index 7fb3314bf..d6e575e1f 100644 --- a/endpoints/endpoints.go +++ b/endpoints/endpoints.go @@ -35,6 +35,12 @@ var Cern = oauth2.Endpoint{ TokenURL: "https://oauth.web.cern.ch/OAuth/Token", } +// Discord is the endpoint for Discord. +var Discord = oauth2.Endpoint{ + AuthURL: "https://discord.com/oauth2/authorize", + TokenURL: "https://discord.com/api/oauth2/token", +} + // Facebook is the endpoint for Facebook. var Facebook = oauth2.Endpoint{ AuthURL: "https://www.facebook.com/v3.2/dialog/oauth", @@ -62,14 +68,15 @@ var GitHub = oauth2.Endpoint{ // GitLab is the endpoint for GitLab. var GitLab = oauth2.Endpoint{ - AuthURL: "https://gitlab.com/oauth/authorize", - TokenURL: "https://gitlab.com/oauth/token", + AuthURL: "https://gitlab.com/oauth/authorize", + TokenURL: "https://gitlab.com/oauth/token", + DeviceAuthURL: "https://gitlab.com/oauth/authorize_device", } // Google is the endpoint for Google. var Google = oauth2.Endpoint{ - AuthURL: "https://accounts.google.com/o/oauth2/auth", - TokenURL: "https://oauth2.googleapis.com/token", + AuthURL: "https://accounts.google.com/o/oauth2/auth", + TokenURL: "https://oauth2.googleapis.com/token", DeviceAuthURL: "https://oauth2.googleapis.com/device/code", } @@ -145,6 +152,12 @@ var Odnoklassniki = oauth2.Endpoint{ TokenURL: "https://api.odnoklassniki.ru/oauth/token.do", } +// Patreon is the endpoint for Patreon. +var Patreon = oauth2.Endpoint{ + AuthURL: "https://www.patreon.com/oauth2/authorize", + TokenURL: "https://www.patreon.com/api/oauth2/token", +} + // PayPal is the endpoint for PayPal. var PayPal = oauth2.Endpoint{ AuthURL: "https://www.paypal.com/webapps/auth/protocol/openidconnect/v1/authorize", @@ -227,8 +240,9 @@ func AzureAD(tenant string) oauth2.Endpoint { tenant = "common" } return oauth2.Endpoint{ - AuthURL: "https://login.microsoftonline.com/" + tenant + "/oauth2/v2.0/authorize", - TokenURL: "https://login.microsoftonline.com/" + tenant + "/oauth2/v2.0/token", + AuthURL: "https://login.microsoftonline.com/" + tenant + "/oauth2/v2.0/authorize", + TokenURL: "https://login.microsoftonline.com/" + tenant + "/oauth2/v2.0/token", + DeviceAuthURL: "https://login.microsoftonline.com/" + tenant + "/oauth2/v2.0/devicecode", } } diff --git a/gitlab/gitlab.go b/gitlab/gitlab.go index 1231d75ac..aea2dfe50 100644 --- a/gitlab/gitlab.go +++ b/gitlab/gitlab.go @@ -6,11 +6,8 @@ package gitlab // import "golang.org/x/oauth2/gitlab" import ( - "golang.org/x/oauth2" + "golang.org/x/oauth2/endpoints" ) // Endpoint is GitLab's OAuth 2.0 endpoint. -var Endpoint = oauth2.Endpoint{ - AuthURL: "https://gitlab.com/oauth/authorize", - TokenURL: "https://gitlab.com/oauth/token", -} +var Endpoint = endpoints.GitLab diff --git a/go.mod b/go.mod index a6e553041..da302fb45 100644 --- a/go.mod +++ b/go.mod @@ -1,16 +1,8 @@ module golang.org/x/oauth2 -go 1.18 +go 1.23.0 require ( - cloud.google.com/go/compute/metadata v0.2.3 + cloud.google.com/go/compute/metadata v0.3.0 github.com/google/go-cmp v0.5.9 - google.golang.org/appengine v1.6.7 -) - -require ( - cloud.google.com/go/compute v1.20.1 // indirect - github.com/golang/protobuf v1.5.3 // indirect - golang.org/x/net v0.22.0 // indirect - google.golang.org/protobuf v1.31.0 // indirect ) diff --git a/go.sum b/go.sum index 1f42ab621..0c9052866 100644 --- a/go.sum +++ b/go.sum @@ -1,26 +1,4 @@ -cloud.google.com/go/compute v1.20.1 h1:6aKEtlUiwEpJzM001l0yFkpXmUVXaN8W+fbkb2AZNbg= -cloud.google.com/go/compute v1.20.1/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM= -cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= -cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= +cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= -golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= -google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= diff --git a/google/appengine.go b/google/appengine.go index feb1157b1..564920bd4 100644 --- a/google/appengine.go +++ b/google/appengine.go @@ -6,16 +6,13 @@ package google import ( "context" - "time" + "log" + "sync" "golang.org/x/oauth2" ) -// Set at init time by appengine_gen1.go. If nil, we're not on App Engine standard first generation (<= Go 1.9) or App Engine flexible. -var appengineTokenFunc func(c context.Context, scopes ...string) (token string, expiry time.Time, err error) - -// Set at init time by appengine_gen1.go. If nil, we're not on App Engine standard first generation (<= Go 1.9) or App Engine flexible. -var appengineAppIDFunc func(c context.Context) string +var logOnce sync.Once // only spam about deprecation once // AppEngineTokenSource returns a token source that fetches tokens from either // the current application's service account or from the metadata server, @@ -23,8 +20,10 @@ var appengineAppIDFunc func(c context.Context) string // details. If you are implementing a 3-legged OAuth 2.0 flow on App Engine that // involves user accounts, see oauth2.Config instead. // -// First generation App Engine runtimes (<= Go 1.9): -// AppEngineTokenSource returns a token source that fetches tokens issued to the +// The current version of this library requires at least Go 1.17 to build, +// so first generation App Engine runtimes (<= Go 1.9) are unsupported. +// Previously, on first generation App Engine runtimes, AppEngineTokenSource +// returned a token source that fetches tokens issued to the // current App Engine application's service account. The provided context must have // come from appengine.NewContext. // @@ -34,5 +33,8 @@ var appengineAppIDFunc func(c context.Context) string // context and scopes are not used. Please use DefaultTokenSource (or ComputeTokenSource, // which DefaultTokenSource will use in this case) instead. func AppEngineTokenSource(ctx context.Context, scope ...string) oauth2.TokenSource { - return appEngineTokenSource(ctx, scope...) + logOnce.Do(func() { + 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.") + }) + return ComputeTokenSource("") } diff --git a/google/appengine_gen1.go b/google/appengine_gen1.go deleted file mode 100644 index e61587945..000000000 --- a/google/appengine_gen1.go +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright 2018 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build appengine - -// This file applies to App Engine first generation runtimes (<= Go 1.9). - -package google - -import ( - "context" - "sort" - "strings" - "sync" - - "golang.org/x/oauth2" - "google.golang.org/appengine" -) - -func init() { - appengineTokenFunc = appengine.AccessToken - appengineAppIDFunc = appengine.AppID -} - -// See comment on AppEngineTokenSource in appengine.go. -func appEngineTokenSource(ctx context.Context, scope ...string) oauth2.TokenSource { - scopes := append([]string{}, scope...) - sort.Strings(scopes) - return &gaeTokenSource{ - ctx: ctx, - scopes: scopes, - key: strings.Join(scopes, " "), - } -} - -// aeTokens helps the fetched tokens to be reused until their expiration. -var ( - aeTokensMu sync.Mutex - aeTokens = make(map[string]*tokenLock) // key is space-separated scopes -) - -type tokenLock struct { - mu sync.Mutex // guards t; held while fetching or updating t - t *oauth2.Token -} - -type gaeTokenSource struct { - ctx context.Context - scopes []string - key string // to aeTokens map; space-separated scopes -} - -func (ts *gaeTokenSource) Token() (*oauth2.Token, error) { - aeTokensMu.Lock() - tok, ok := aeTokens[ts.key] - if !ok { - tok = &tokenLock{} - aeTokens[ts.key] = tok - } - aeTokensMu.Unlock() - - tok.mu.Lock() - defer tok.mu.Unlock() - if tok.t.Valid() { - return tok.t, nil - } - access, exp, err := appengineTokenFunc(ts.ctx, ts.scopes...) - if err != nil { - return nil, err - } - tok.t = &oauth2.Token{ - AccessToken: access, - Expiry: exp, - } - return tok.t, nil -} diff --git a/google/appengine_gen2_flex.go b/google/appengine_gen2_flex.go deleted file mode 100644 index 9c79aa0a0..000000000 --- a/google/appengine_gen2_flex.go +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2018 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build !appengine - -// This file applies to App Engine second generation runtimes (>= Go 1.11) and App Engine flexible. - -package google - -import ( - "context" - "log" - "sync" - - "golang.org/x/oauth2" -) - -var logOnce sync.Once // only spam about deprecation once - -// See comment on AppEngineTokenSource in appengine.go. -func appEngineTokenSource(ctx context.Context, scope ...string) oauth2.TokenSource { - logOnce.Do(func() { - 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.") - }) - return ComputeTokenSource("") -} diff --git a/google/default.go b/google/default.go index 18f369851..0260935ba 100644 --- a/google/default.go +++ b/google/default.go @@ -42,6 +42,17 @@ type Credentials struct { // running on Google Cloud Platform. JSON []byte + // UniverseDomainProvider returns the default service domain for a given + // Cloud universe. Optional. + // + // On GCE, UniverseDomainProvider should return the universe domain value + // from Google Compute Engine (GCE)'s metadata server. See also [The attached service + // account](https://cloud.google.com/docs/authentication/application-default-credentials#attached-sa). + // If the GCE metadata server returns a 404 error, the default universe + // domain value should be returned. If the GCE metadata server returns an + // error other than 404, the error should be returned. + UniverseDomainProvider func() (string, error) + udMu sync.Mutex // guards universeDomain // universeDomain is the default service domain for a given Cloud universe. universeDomain string @@ -64,54 +75,32 @@ func (c *Credentials) UniverseDomain() string { } // GetUniverseDomain returns the default service domain for a given Cloud -// universe. +// universe. If present, UniverseDomainProvider will be invoked and its return +// value will be cached. // // The default value is "googleapis.com". -// -// It obtains the universe domain from the attached service account on GCE when -// authenticating via the GCE metadata server. See also [The attached service -// account](https://cloud.google.com/docs/authentication/application-default-credentials#attached-sa). -// If the GCE metadata server returns a 404 error, the default value is -// returned. If the GCE metadata server returns an error other than 404, the -// error is returned. func (c *Credentials) GetUniverseDomain() (string, error) { c.udMu.Lock() defer c.udMu.Unlock() - if c.universeDomain == "" && metadata.OnGCE() { - // If we're on Google Compute Engine, an App Engine standard second - // generation runtime, or App Engine flexible, use the metadata server. - err := c.computeUniverseDomain() + if c.universeDomain == "" && c.UniverseDomainProvider != nil { + // On Google Compute Engine, an App Engine standard second generation + // runtime, or App Engine flexible, use an externally provided function + // to request the universe domain from the metadata server. + ud, err := c.UniverseDomainProvider() if err != nil { return "", err } + c.universeDomain = ud } - // If not on Google Compute Engine, or in case of any non-error path in - // computeUniverseDomain that did not set universeDomain, set the default - // universe domain. + // If no UniverseDomainProvider (meaning not on Google Compute Engine), or + // in case of any (non-error) empty return value from + // UniverseDomainProvider, set the default universe domain. if c.universeDomain == "" { c.universeDomain = defaultUniverseDomain } return c.universeDomain, nil } -// computeUniverseDomain fetches the default service domain for a given Cloud -// universe from Google Compute Engine (GCE)'s metadata server. It's only valid -// to use this method if your program is running on a GCE instance. -func (c *Credentials) computeUniverseDomain() error { - var err error - c.universeDomain, err = metadata.Get("universe/universe_domain") - if err != nil { - if _, ok := err.(metadata.NotDefinedError); ok { - // http.StatusNotFound (404) - c.universeDomain = defaultUniverseDomain - return nil - } else { - return err - } - } - return nil -} - // DefaultCredentials is the old name of Credentials. // // Deprecated: use Credentials instead. @@ -199,9 +188,7 @@ func DefaultTokenSource(ctx context.Context, scope ...string) (oauth2.TokenSourc // 2. A JSON file in a location known to the gcloud command-line tool. // On Windows, this is %APPDATA%/gcloud/application_default_credentials.json. // On other systems, $HOME/.config/gcloud/application_default_credentials.json. -// 3. On Google App Engine standard first generation runtimes (<= Go 1.9) it uses -// the appengine.AccessToken function. -// 4. On Google Compute Engine, Google App Engine standard second generation runtimes +// 3. On Google Compute Engine, Google App Engine standard second generation runtimes // (>= Go 1.11), and Google App Engine flexible environment, it fetches // credentials from the metadata server. func FindDefaultCredentialsWithParams(ctx context.Context, params CredentialsParams) (*Credentials, error) { @@ -224,24 +211,27 @@ func FindDefaultCredentialsWithParams(ctx context.Context, params CredentialsPar return CredentialsFromJSONWithParams(ctx, b, params) } - // Third, if we're on a Google App Engine standard first generation runtime (<= Go 1.9) - // use those credentials. App Engine standard second generation runtimes (>= Go 1.11) - // and App Engine flexible use ComputeTokenSource and the metadata server. - if appengineTokenFunc != nil { - return &Credentials{ - ProjectID: appengineAppIDFunc(ctx), - TokenSource: AppEngineTokenSource(ctx, params.Scopes...), - }, nil - } - - // Fourth, if we're on Google Compute Engine, an App Engine standard second generation runtime, + // Third, if we're on Google Compute Engine, an App Engine standard second generation runtime, // or App Engine flexible, use the metadata server. if metadata.OnGCE() { id, _ := metadata.ProjectID() + universeDomainProvider := func() (string, error) { + universeDomain, err := metadata.Get("universe/universe_domain") + if err != nil { + if _, ok := err.(metadata.NotDefinedError); ok { + // http.StatusNotFound (404) + return defaultUniverseDomain, nil + } else { + return "", err + } + } + return universeDomain, nil + } return &Credentials{ - ProjectID: id, - TokenSource: computeTokenSource("", params.EarlyTokenRefresh, params.Scopes...), - universeDomain: params.UniverseDomain, + ProjectID: id, + TokenSource: computeTokenSource("", params.EarlyTokenRefresh, params.Scopes...), + UniverseDomainProvider: universeDomainProvider, + universeDomain: params.UniverseDomain, }, nil } @@ -261,6 +251,12 @@ func FindDefaultCredentials(ctx context.Context, scopes ...string) (*Credentials // a Google Developers service account key file, a gcloud user credentials file (a.k.a. refresh // token JSON), or the JSON configuration file for workload identity federation in non-Google cloud // platforms (see https://cloud.google.com/iam/docs/how-to#using-workload-identity-federation). +// +// Important: If you accept a credential configuration (credential JSON/File/Stream) from an +// external source for authentication to Google Cloud Platform, you must validate it before +// providing it to any Google API or library. Providing an unvalidated credential configuration to +// Google APIs can compromise the security of your systems and data. For more information, refer to +// [Validate credential configurations from external sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials). func CredentialsFromJSONWithParams(ctx context.Context, jsonData []byte, params CredentialsParams) (*Credentials, error) { // Make defensive copy of the slices in params. params = params.deepCopy() @@ -304,6 +300,12 @@ func CredentialsFromJSONWithParams(ctx context.Context, jsonData []byte, params } // CredentialsFromJSON invokes CredentialsFromJSONWithParams with the specified scopes. +// +// Important: If you accept a credential configuration (credential JSON/File/Stream) from an +// external source for authentication to Google Cloud Platform, you must validate it before +// providing it to any Google API or library. Providing an unvalidated credential configuration to +// Google APIs can compromise the security of your systems and data. For more information, refer to +// [Validate credential configurations from external sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials). func CredentialsFromJSON(ctx context.Context, jsonData []byte, scopes ...string) (*Credentials, error) { var params CredentialsParams params.Scopes = scopes diff --git a/google/default_test.go b/google/default_test.go index 7352ffcce..c8465e94f 100644 --- a/google/default_test.go +++ b/google/default_test.go @@ -10,6 +10,8 @@ import ( "net/http/httptest" "strings" "testing" + + "cloud.google.com/go/compute/metadata" ) var saJSONJWT = []byte(`{ @@ -255,9 +257,14 @@ func TestCredentialsFromJSONWithParams_User_UniverseDomain_Params_UniverseDomain func TestComputeUniverseDomain(t *testing.T) { universeDomainPath := "/computeMetadata/v1/universe/universe_domain" universeDomainResponseBody := "example.com" + var requests int s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + requests++ if r.URL.Path != universeDomainPath { - t.Errorf("got %s, want %s", r.URL.Path, universeDomainPath) + t.Errorf("bad path, got %s, want %s", r.URL.Path, universeDomainPath) + } + if requests > 1 { + t.Errorf("too many requests, got %d, want 1", requests) } w.Write([]byte(universeDomainResponseBody)) })) @@ -268,11 +275,19 @@ func TestComputeUniverseDomain(t *testing.T) { params := CredentialsParams{ Scopes: []string{scope}, } + universeDomainProvider := func() (string, error) { + universeDomain, err := metadata.Get("universe/universe_domain") + if err != nil { + return "", err + } + return universeDomain, nil + } // Copied from FindDefaultCredentialsWithParams, metadata.OnGCE() = true block creds := &Credentials{ - ProjectID: "fake_project", - TokenSource: computeTokenSource("", params.EarlyTokenRefresh, params.Scopes...), - universeDomain: params.UniverseDomain, // empty + ProjectID: "fake_project", + TokenSource: computeTokenSource("", params.EarlyTokenRefresh, params.Scopes...), + UniverseDomainProvider: universeDomainProvider, + universeDomain: params.UniverseDomain, // empty } c := make(chan bool) go func() { @@ -285,7 +300,7 @@ func TestComputeUniverseDomain(t *testing.T) { } c <- true }() - got, err := creds.GetUniverseDomain() // Second conflicting access. + got, err := creds.GetUniverseDomain() // Second conflicting (and potentially uncached) access. <-c if err != nil { t.Error(err) diff --git a/google/externalaccount/aws.go b/google/externalaccount/aws.go index da61d0c0e..55d59999e 100644 --- a/google/externalaccount/aws.go +++ b/google/externalaccount/aws.go @@ -28,7 +28,7 @@ import ( // AwsSecurityCredentials models AWS security credentials. type AwsSecurityCredentials struct { - // AccessKeyId is the AWS Access Key ID - Required. + // AccessKeyID is the AWS Access Key ID - Required. AccessKeyID string `json:"AccessKeyID"` // SecretAccessKey is the AWS Secret Access Key - Required. SecretAccessKey string `json:"SecretAccessKey"` @@ -520,7 +520,6 @@ func (cs *awsCredentialSource) getMetadataSecurityCredentials(roleName string, h if err != nil { return result, err } - req.Header.Add("Content-Type", "application/json") for name, value := range headers { req.Header.Add(name, value) diff --git a/google/externalaccount/basecredentials.go b/google/externalaccount/basecredentials.go index 400aa0a07..aa0bba2eb 100644 --- a/google/externalaccount/basecredentials.go +++ b/google/externalaccount/basecredentials.go @@ -263,7 +263,7 @@ const ( fileTypeJSON = "json" ) -// Format contains information needed to retireve a subject token for URL or File sourced credentials. +// Format contains information needed to retrieve a subject token for URL or File sourced credentials. type Format struct { // Type should be either "text" or "json". This determines whether the file or URL sourced credentials // expect a simple text subject token or if the subject token will be contained in a JSON object. @@ -278,20 +278,52 @@ type Format struct { type CredentialSource struct { // File is the location for file sourced credentials. // One field amongst File, URL, Executable, or EnvironmentID should be provided, depending on the kind of credential in question. + // + // Important: If you accept a credential configuration (credential + // JSON/File/Stream) from an external source for authentication to Google + // Cloud Platform, you must validate it before providing it to any Google + // API or library. Providing an unvalidated credential configuration to + // Google APIs can compromise the security of your systems and data. For + // more information, refer to [Validate credential configurations from + // external sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials). File string `json:"file"` // Url is the URL to call for URL sourced credentials. // One field amongst File, URL, Executable, or EnvironmentID should be provided, depending on the kind of credential in question. + // + // Important: If you accept a credential configuration (credential + // JSON/File/Stream) from an external source for authentication to Google + // Cloud Platform, you must validate it before providing it to any Google + // API or library. Providing an unvalidated credential configuration to + // Google APIs can compromise the security of your systems and data. For + // more information, refer to [Validate credential configurations from + // external sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials). URL string `json:"url"` // Headers are the headers to attach to the request for URL sourced credentials. Headers map[string]string `json:"headers"` // Executable is the configuration object for executable sourced credentials. // One field amongst File, URL, Executable, or EnvironmentID should be provided, depending on the kind of credential in question. + // + // Important: If you accept a credential configuration (credential + // JSON/File/Stream) from an external source for authentication to Google + // Cloud Platform, you must validate it before providing it to any Google + // API or library. Providing an unvalidated credential configuration to + // Google APIs can compromise the security of your systems and data. For + // more information, refer to [Validate credential configurations from + // external sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials). Executable *ExecutableConfig `json:"executable"` // EnvironmentID is the EnvironmentID used for AWS sourced credentials. This should start with "AWS". // One field amongst File, URL, Executable, or EnvironmentID should be provided, depending on the kind of credential in question. + // + // Important: If you accept a credential configuration (credential + // JSON/File/Stream) from an external source for authentication to Google + // Cloud Platform, you must validate it before providing it to any Google + // API or library. Providing an unvalidated credential configuration to + // Google APIs can compromise the security of your systems and data. For + // more information, refer to [Validate credential configurations from + // external sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials). EnvironmentID string `json:"environment_id"` // RegionURL is the metadata URL to retrieve the region from for EC2 AWS credentials. RegionURL string `json:"region_url"` @@ -329,7 +361,7 @@ type SubjectTokenSupplier interface { type AwsSecurityCredentialsSupplier interface { // AwsRegion should return the AWS region or an error. AwsRegion(ctx context.Context, options SupplierOptions) (string, error) - // GetAwsSecurityCredentials should return a valid set of AwsSecurityCredentials or an error. + // AwsSecurityCredentials should return a valid set of AwsSecurityCredentials or an error. // The external account token source does not cache the returned security credentials, so caching // logic should be implemented in the supplier to prevent multiple requests for the same security credentials. AwsSecurityCredentials(ctx context.Context, options SupplierOptions) (*AwsSecurityCredentials, error) @@ -471,11 +503,12 @@ func (ts tokenSource) Token() (*oauth2.Token, error) { AccessToken: stsResp.AccessToken, TokenType: stsResp.TokenType, } - if stsResp.ExpiresIn < 0 { + + // The RFC8693 doesn't define the explicit 0 of "expires_in" field behavior. + if stsResp.ExpiresIn <= 0 { return nil, fmt.Errorf("oauth2/google/externalaccount: got invalid expiry from security token service") - } else if stsResp.ExpiresIn >= 0 { - accessToken.Expiry = now().Add(time.Duration(stsResp.ExpiresIn) * time.Second) } + accessToken.Expiry = now().Add(time.Duration(stsResp.ExpiresIn) * time.Second) if stsResp.RefreshToken != "" { accessToken.RefreshToken = stsResp.RefreshToken diff --git a/google/externalaccount/basecredentials_test.go b/google/externalaccount/basecredentials_test.go index 33314c3f0..d52f6a789 100644 --- a/google/externalaccount/basecredentials_test.go +++ b/google/externalaccount/basecredentials_test.go @@ -6,6 +6,7 @@ package externalaccount import ( "context" + "encoding/json" "fmt" "io/ioutil" "net/http" @@ -101,15 +102,18 @@ func run(t *testing.T, config *Config, tets *testExchangeTokenServer) (*oauth2.T return ts.Token() } -func validateToken(t *testing.T, tok *oauth2.Token) { - if got, want := tok.AccessToken, correctAT; got != want { +func validateToken(t *testing.T, tok *oauth2.Token, expectToken *oauth2.Token) { + if expectToken == nil { + return + } + if got, want := tok.AccessToken, expectToken.AccessToken; got != want { t.Errorf("Unexpected access token: got %v, but wanted %v", got, want) } - if got, want := tok.TokenType, "Bearer"; got != want { + if got, want := tok.TokenType, expectToken.TokenType; got != want { t.Errorf("Unexpected TokenType: got %v, but wanted %v", got, want) } - if got, want := tok.Expiry, testNow().Add(time.Duration(3600)*time.Second); got != want { + if got, want := tok.Expiry, expectToken.Expiry; got != want { t.Errorf("Unexpected Expiry: got %v, but wanted %v", got, want) } } @@ -173,30 +177,91 @@ func getExpectedMetricsHeader(source string, saImpersonation bool, configLifetim } func TestToken(t *testing.T) { - config := Config{ - Audience: "32555940559.apps.googleusercontent.com", - SubjectTokenType: "urn:ietf:params:oauth:token-type:id_token", - ClientSecret: "notsosecret", - ClientID: "rbrgnognrhongo3bi4gb9ghg9g", - CredentialSource: &testBaseCredSource, - Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"}, + type MockSTSResponse struct { + AccessToken string `json:"access_token"` + IssuedTokenType string `json:"issued_token_type"` + TokenType string `json:"token_type"` + ExpiresIn int32 `json:"expires_in,omitempty"` + Scope string `json:"scopre,omitenpty"` } - server := testExchangeTokenServer{ - url: "/", - authorization: "Basic cmJyZ25vZ25yaG9uZ28zYmk0Z2I5Z2hnOWc6bm90c29zZWNyZXQ=", - contentType: "application/x-www-form-urlencoded", - metricsHeader: getExpectedMetricsHeader("file", false, false), - body: baseCredsRequestBody, - response: baseCredsResponseBody, + testCases := []struct { + name string + responseBody MockSTSResponse + expectToken *oauth2.Token + expectErrorMsg string + }{ + { + name: "happy case", + responseBody: MockSTSResponse{ + AccessToken: correctAT, + IssuedTokenType: "urn:ietf:params:oauth:token-type:access_token", + TokenType: "Bearer", + ExpiresIn: 3600, + Scope: "https://www.googleapis.com/auth/cloud-platform", + }, + expectToken: &oauth2.Token{ + AccessToken: correctAT, + TokenType: "Bearer", + Expiry: testNow().Add(time.Duration(3600) * time.Second), + }, + }, + { + name: "no expiry time on token", + responseBody: MockSTSResponse{ + AccessToken: correctAT, + IssuedTokenType: "urn:ietf:params:oauth:token-type:access_token", + TokenType: "Bearer", + Scope: "https://www.googleapis.com/auth/cloud-platform", + }, + expectToken: nil, + expectErrorMsg: "oauth2/google/externalaccount: got invalid expiry from security token service", + }, + { + name: "negative expiry time", + responseBody: MockSTSResponse{ + AccessToken: correctAT, + IssuedTokenType: "urn:ietf:params:oauth:token-type:access_token", + TokenType: "Bearer", + ExpiresIn: -1, + Scope: "https://www.googleapis.com/auth/cloud-platform", + }, + expectToken: nil, + expectErrorMsg: "oauth2/google/externalaccount: got invalid expiry from security token service", + }, } - tok, err := run(t, &config, &server) + for _, testCase := range testCases { + config := Config{ + Audience: "32555940559.apps.googleusercontent.com", + SubjectTokenType: "urn:ietf:params:oauth:token-type:id_token", + ClientSecret: "notsosecret", + ClientID: "rbrgnognrhongo3bi4gb9ghg9g", + CredentialSource: &testBaseCredSource, + Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"}, + } - if err != nil { - t.Fatalf("Unexpected error: %e", err) + responseBody, err := json.Marshal(testCase.responseBody) + if err != nil { + t.Errorf("Invalid response received.") + } + + server := testExchangeTokenServer{ + url: "/", + authorization: "Basic cmJyZ25vZ25yaG9uZ28zYmk0Z2I5Z2hnOWc6bm90c29zZWNyZXQ=", + contentType: "application/x-www-form-urlencoded", + metricsHeader: getExpectedMetricsHeader("file", false, false), + body: baseCredsRequestBody, + response: string(responseBody), + } + + tok, err := run(t, &config, &server) + + if err != nil && err.Error() != testCase.expectErrorMsg { + t.Errorf("Error not as expected: got = %v, and want = %v", err, testCase.expectErrorMsg) + } + validateToken(t, tok, testCase.expectToken) } - validateToken(t, tok) } func TestWorkforcePoolTokenWithClientID(t *testing.T) { @@ -224,7 +289,12 @@ func TestWorkforcePoolTokenWithClientID(t *testing.T) { if err != nil { t.Fatalf("Unexpected error: %e", err) } - validateToken(t, tok) + expectToken := oauth2.Token{ + AccessToken: correctAT, + TokenType: "Bearer", + Expiry: testNow().Add(time.Duration(3600) * time.Second), + } + validateToken(t, tok, &expectToken) } func TestWorkforcePoolTokenWithoutClientID(t *testing.T) { @@ -251,7 +321,12 @@ func TestWorkforcePoolTokenWithoutClientID(t *testing.T) { if err != nil { t.Fatalf("Unexpected error: %e", err) } - validateToken(t, tok) + expectToken := oauth2.Token{ + AccessToken: correctAT, + TokenType: "Bearer", + Expiry: testNow().Add(time.Duration(3600) * time.Second), + } + validateToken(t, tok, &expectToken) } func TestNonworkforceWithWorkforcePoolUserProject(t *testing.T) { @@ -272,12 +347,12 @@ func TestNonworkforceWithWorkforcePoolUserProject(t *testing.T) { t.Fatalf("Expected error but found none") } if got, want := err.Error(), "oauth2/google/externalaccount: Workforce pool user project should not be set for non-workforce pool credentials"; got != want { - t.Errorf("Incorrect error received.\nExpected: %s\nRecieved: %s", want, got) + t.Errorf("Incorrect error received.\nExpected: %s\nReceived: %s", want, got) } } func TestWorkforcePoolCreation(t *testing.T) { - var audienceValidatyTests = []struct { + var audienceValidityTests = []struct { audience string expectSuccess bool }{ @@ -296,7 +371,7 @@ func TestWorkforcePoolCreation(t *testing.T) { } ctx := context.Background() - for _, tt := range audienceValidatyTests { + for _, tt := range audienceValidityTests { t.Run(" "+tt.audience, func(t *testing.T) { // We prepend a space ahead of the test input when outputting for sake of readability. config := testConfig config.TokenURL = "https://sts.googleapis.com" // Setting the most basic acceptable tokenURL diff --git a/google/externalaccount/executablecredsource_test.go b/google/externalaccount/executablecredsource_test.go index 69ec21ae1..3ecc05f92 100644 --- a/google/externalaccount/executablecredsource_test.go +++ b/google/externalaccount/executablecredsource_test.go @@ -654,7 +654,7 @@ func TestRetrieveOutputFileSubjectTokenNotJSON(t *testing.T) { if _, err = base.subjectToken(); err == nil { t.Fatalf("Expected error but found none") } else if got, want := err.Error(), jsonParsingError(outputFileSource, "tokentokentoken").Error(); got != want { - t.Errorf("Incorrect error received.\nExpected: %s\nRecieved: %s", want, got) + t.Errorf("Incorrect error received.\nExpected: %s\nReceived: %s", want, got) } _, deadlineSet := te.getDeadline() @@ -801,7 +801,7 @@ func TestRetrieveOutputFileSubjectTokenFailureTests(t *testing.T) { if _, err = ecs.subjectToken(); err == nil { t.Errorf("Expected error but found none") } else if got, want := err.Error(), tt.expectedErr.Error(); got != want { - t.Errorf("Incorrect error received.\nExpected: %s\nRecieved: %s", want, got) + t.Errorf("Incorrect error received.\nExpected: %s\nReceived: %s", want, got) } if _, deadlineSet := te.getDeadline(); deadlineSet { @@ -923,7 +923,7 @@ func TestRetrieveOutputFileSubjectTokenInvalidCache(t *testing.T) { } if got, want := out, "tokentokentoken"; got != want { - t.Errorf("Incorrect token received.\nExpected: %s\nRecieved: %s", want, got) + t.Errorf("Incorrect token received.\nExpected: %s\nReceived: %s", want, got) } }) } @@ -1012,7 +1012,7 @@ func TestRetrieveOutputFileSubjectTokenJwt(t *testing.T) { if out, err := ecs.subjectToken(); err != nil { t.Errorf("retrieveSubjectToken() failed: %v", err) } else if got, want := out, "tokentokentoken"; got != want { - t.Errorf("Incorrect token received.\nExpected: %s\nRecieved: %s", want, got) + t.Errorf("Incorrect token received.\nExpected: %s\nReceived: %s", want, got) } if _, deadlineSet := te.getDeadline(); deadlineSet { diff --git a/google/google.go b/google/google.go index ba931c2c3..7b82e7a08 100644 --- a/google/google.go +++ b/google/google.go @@ -252,7 +252,10 @@ func (f *credentialsFile) tokenSource(ctx context.Context, params CredentialsPar // Further information about retrieving access tokens from the GCE metadata // server can be found at https://cloud.google.com/compute/docs/authentication. func ComputeTokenSource(account string, scope ...string) oauth2.TokenSource { - return computeTokenSource(account, 0, scope...) + // refresh 3 minutes and 45 seconds early. The shortest MDS cache is currently 4 minutes, so any + // refreshes earlier are a waste of compute. + earlyExpirySecs := 225 * time.Second + return computeTokenSource(account, earlyExpirySecs, scope...) } func computeTokenSource(account string, earlyExpiry time.Duration, scope ...string) oauth2.TokenSource { diff --git a/google/google_test.go b/google/google_test.go index 7078d429f..5aa5e2845 100644 --- a/google/google_test.go +++ b/google/google_test.go @@ -72,7 +72,7 @@ func TestConfigFromJSON(t *testing.T) { t.Errorf("ClientSecret = %q; want %q", got, want) } if got, want := conf.RedirectURL, "https://www.example.com/oauth2callback"; got != want { - t.Errorf("RedictURL = %q; want %q", got, want) + t.Errorf("RedirectURL = %q; want %q", got, want) } if got, want := strings.Join(conf.Scopes, ","), "scope1,scope2"; got != want { t.Errorf("Scopes = %q; want %q", got, want) diff --git a/google/internal/externalaccountauthorizeduser/externalaccountauthorizeduser_test.go b/google/internal/externalaccountauthorizeduser/externalaccountauthorizeduser_test.go index 94bfee3d6..1bbbbac19 100644 --- a/google/internal/externalaccountauthorizeduser/externalaccountauthorizeduser_test.go +++ b/google/internal/externalaccountauthorizeduser/externalaccountauthorizeduser_test.go @@ -38,7 +38,7 @@ type testRefreshTokenServer struct { server *httptest.Server } -func TestExernalAccountAuthorizedUser_JustToken(t *testing.T) { +func TestExternalAccountAuthorizedUser_JustToken(t *testing.T) { config := &Config{ Token: "AAAAAAA", Expiry: now().Add(time.Hour), @@ -57,7 +57,7 @@ func TestExernalAccountAuthorizedUser_JustToken(t *testing.T) { } } -func TestExernalAccountAuthorizedUser_TokenRefreshWithRefreshTokenInRespondse(t *testing.T) { +func TestExternalAccountAuthorizedUser_TokenRefreshWithRefreshTokenInResponse(t *testing.T) { server := &testRefreshTokenServer{ URL: "/", Authorization: "Basic Q0xJRU5UX0lEOkNMSUVOVF9TRUNSRVQ=", @@ -99,7 +99,7 @@ func TestExernalAccountAuthorizedUser_TokenRefreshWithRefreshTokenInRespondse(t } } -func TestExernalAccountAuthorizedUser_MinimumFieldsRequiredForRefresh(t *testing.T) { +func TestExternalAccountAuthorizedUser_MinimumFieldsRequiredForRefresh(t *testing.T) { server := &testRefreshTokenServer{ URL: "/", Authorization: "Basic Q0xJRU5UX0lEOkNMSUVOVF9TRUNSRVQ=", @@ -187,7 +187,7 @@ func TestExternalAccountAuthorizedUser_MissingRefreshFields(t *testing.T) { }, }, { - name: "missing client secrect", + name: "missing client secret", config: Config{ RefreshToken: "BBBBBBBBB", TokenURL: url, diff --git a/google/internal/stsexchange/sts_exchange_test.go b/google/internal/stsexchange/sts_exchange_test.go index 895b9bcf9..ff9a9ad08 100644 --- a/google/internal/stsexchange/sts_exchange_test.go +++ b/google/internal/stsexchange/sts_exchange_test.go @@ -142,7 +142,7 @@ func TestExchangeToken_Opts(t *testing.T) { } strOpts, ok := data["options"] if !ok { - t.Errorf("Server didn't recieve an \"options\" field.") + t.Errorf("Server didn't receive an \"options\" field.") } else if len(strOpts) < 1 { t.Errorf("\"options\" field has length 0.") } diff --git a/internal/client_appengine.go b/internal/client_appengine.go deleted file mode 100644 index d28140f78..000000000 --- a/internal/client_appengine.go +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2018 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build appengine - -package internal - -import "google.golang.org/appengine/urlfetch" - -func init() { - appengineClientHook = urlfetch.Client -} diff --git a/internal/transport.go b/internal/transport.go index 572074a63..b9db01ddf 100644 --- a/internal/transport.go +++ b/internal/transport.go @@ -18,16 +18,11 @@ var HTTPClient ContextKey // because nobody else can create a ContextKey, being unexported. type ContextKey struct{} -var appengineClientHook func(context.Context) *http.Client - func ContextClient(ctx context.Context) *http.Client { if ctx != nil { if hc, ok := ctx.Value(HTTPClient).(*http.Client); ok { return hc } } - if appengineClientHook != nil { - return appengineClientHook(ctx) - } return http.DefaultClient } diff --git a/jws/jws.go b/jws/jws.go index 95015648b..6f03a49d3 100644 --- a/jws/jws.go +++ b/jws/jws.go @@ -165,11 +165,11 @@ func Encode(header *Header, c *ClaimSet, key *rsa.PrivateKey) (string, error) { // Verify tests whether the provided JWT token's signature was produced by the private key // associated with the supplied public key. func Verify(token string, key *rsa.PublicKey) error { - parts := strings.Split(token, ".") - if len(parts) != 3 { + if strings.Count(token, ".") != 2 { return errors.New("jws: invalid token received, token must have 3 parts") } + parts := strings.SplitN(token, ".", 3) signedContent := parts[0] + "." + parts[1] signatureString, err := base64.RawURLEncoding.DecodeString(parts[2]) if err != nil { diff --git a/jwt/example_test.go b/jwt/example_test.go index 58503d80d..c99b8767f 100644 --- a/jwt/example_test.go +++ b/jwt/example_test.go @@ -10,7 +10,7 @@ import ( "golang.org/x/oauth2/jwt" ) -func ExampleJWTConfig() { +func ExampleConfig() { ctx := context.Background() conf := &jwt.Config{ Email: "xxx@developer.com", diff --git a/microsoft/microsoft.go b/microsoft/microsoft.go index 3ffbc57a6..13091d666 100644 --- a/microsoft/microsoft.go +++ b/microsoft/microsoft.go @@ -25,7 +25,8 @@ func AzureADEndpoint(tenant string) oauth2.Endpoint { tenant = "common" } return oauth2.Endpoint{ - AuthURL: "https://login.microsoftonline.com/" + tenant + "/oauth2/v2.0/authorize", - TokenURL: "https://login.microsoftonline.com/" + tenant + "/oauth2/v2.0/token", + AuthURL: "https://login.microsoftonline.com/" + tenant + "/oauth2/v2.0/authorize", + TokenURL: "https://login.microsoftonline.com/" + tenant + "/oauth2/v2.0/token", + DeviceAuthURL: "https://login.microsoftonline.com/" + tenant + "/oauth2/v2.0/devicecode", } } diff --git a/oauth2.go b/oauth2.go index 90a2c3d6d..eacdd7fd9 100644 --- a/oauth2.go +++ b/oauth2.go @@ -56,7 +56,7 @@ type Config struct { // the OAuth flow, after the resource owner's URLs. RedirectURL string - // Scope specifies optional requested permissions. + // Scopes specifies optional requested permissions. Scopes []string // authStyleCache caches which auth style to use when Endpoint.AuthStyle is @@ -288,7 +288,7 @@ func (tf *tokenRefresher) Token() (*Token, error) { if tf.refreshToken != tk.RefreshToken { tf.refreshToken = tk.RefreshToken } - return tk, err + return tk, nil } // reuseTokenSource is a TokenSource that holds a single token in memory @@ -356,11 +356,15 @@ func NewClient(ctx context.Context, src TokenSource) *http.Client { if src == nil { return internal.ContextClient(ctx) } + cc := internal.ContextClient(ctx) return &http.Client{ Transport: &Transport{ - Base: internal.ContextClient(ctx).Transport, + Base: cc.Transport, Source: ReuseTokenSource(nil, src), }, + CheckRedirect: cc.CheckRedirect, + Jar: cc.Jar, + Timeout: cc.Timeout, } } @@ -393,7 +397,7 @@ func ReuseTokenSource(t *Token, src TokenSource) TokenSource { } } -// ReuseTokenSource returns a TokenSource that acts in the same manner as the +// ReuseTokenSourceWithExpiry returns a TokenSource that acts in the same manner as the // TokenSource returned by ReuseTokenSource, except the expiry buffer is // configurable. The expiration time of a token is calculated as // t.Expiry.Add(-earlyExpiry). diff --git a/pkce.go b/pkce.go index 50593b6df..6a95da975 100644 --- a/pkce.go +++ b/pkce.go @@ -21,7 +21,7 @@ const ( // // A fresh verifier should be generated for each authorization. // S256ChallengeOption(verifier) should then be passed to Config.AuthCodeURL -// (or Config.DeviceAccess) and VerifierOption(verifier) to Config.Exchange +// (or Config.DeviceAuth) and VerifierOption(verifier) to Config.Exchange // (or Config.DeviceAccessToken). func GenerateVerifier() string { // "RECOMMENDED that the output of a suitable random number generator be @@ -51,7 +51,7 @@ func S256ChallengeFromVerifier(verifier string) string { } // S256ChallengeOption derives a PKCE code challenge derived from verifier with -// method S256. It should be passed to Config.AuthCodeURL or Config.DeviceAccess +// method S256. It should be passed to Config.AuthCodeURL or Config.DeviceAuth // only. func S256ChallengeOption(verifier string) AuthCodeOption { return challengeOption{ diff --git a/token.go b/token.go index 5bbb33217..109997d77 100644 --- a/token.go +++ b/token.go @@ -49,6 +49,13 @@ type Token struct { // mechanisms for that TokenSource will not be used. Expiry time.Time `json:"expiry,omitempty"` + // ExpiresIn is the OAuth2 wire format "expires_in" field, + // which specifies how many seconds later the token expires, + // relative to an unknown time base approximately around "now". + // It is the application's responsibility to populate + // `Expiry` from `ExpiresIn` when required. + ExpiresIn int64 `json:"expires_in,omitempty"` + // raw optionally contains extra metadata from the server // when updating a token. raw interface{} diff --git a/transport_test.go b/transport_test.go index faa87d514..a8e6ea236 100644 --- a/transport_test.go +++ b/transport_test.go @@ -9,12 +9,6 @@ import ( "time" ) -type tokenSource struct{ token *Token } - -func (t *tokenSource) Token() (*Token, error) { - return t.token, nil -} - func TestTransportNilTokenSource(t *testing.T) { tr := &Transport{} server := newMockServer(func(w http.ResponseWriter, r *http.Request) {}) @@ -88,13 +82,10 @@ func TestTransportCloseRequestBodySuccess(t *testing.T) { } func TestTransportTokenSource(t *testing.T) { - ts := &tokenSource{ - token: &Token{ - AccessToken: "abc", - }, - } tr := &Transport{ - Source: ts, + Source: StaticTokenSource(&Token{ + AccessToken: "abc", + }), } server := newMockServer(func(w http.ResponseWriter, r *http.Request) { if got, want := r.Header.Get("Authorization"), "Bearer abc"; got != want { @@ -123,14 +114,11 @@ func TestTransportTokenSourceTypes(t *testing.T) { {key: "basic", val: val, want: "Basic abc"}, } for _, tc := range tests { - ts := &tokenSource{ - token: &Token{ + tr := &Transport{ + Source: StaticTokenSource(&Token{ AccessToken: tc.val, TokenType: tc.key, - }, - } - tr := &Transport{ - Source: ts, + }), } server := newMockServer(func(w http.ResponseWriter, r *http.Request) { if got, want := r.Header.Get("Authorization"), tc.want; got != want {