38 Commits

Author SHA1 Message Date
Gopher Robot
3c5dbf08cc go.mod: update golang.org/x dependencies
Update golang.org/x dependencies to their latest tagged versions.

Change-Id: I39a72a7dbb2205a6638a154892c69948ee2deb0d
Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/533241
Reviewed-by: Matt Hickford <matt.hickford@gmail.com>
Reviewed-by: Benny Siegert <bsiegert@gmail.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
Auto-Submit: Gopher Robot <gobot@golang.org>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
2023-10-06 08:33:24 +00:00
Chris Smith
11625ccb95 google: add authorized_user conditional to Credentials.UniverseDomain
Return default universe domain if credentials type is authorized_user.

Change-Id: I20a9b5fafa562fcec84717914a236d081f630591
Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/532196
Run-TryBot: Cody Oss <codyoss@google.com>
Reviewed-by: Cody Oss <codyoss@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
2023-10-02 22:04:54 +00:00
Chris Smith
8d6d45b6cd google: add Credentials.UniverseDomain to support TPC
Read and expose universe_domain from service account JSON files in
CredentialsFromJSONWithParams to support TPC in 1p clients.

Change-Id: I3518a0ec8be5ff7235b946cffd88b26ac8d303cf
Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/531715
Run-TryBot: Cody Oss <codyoss@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Cody Oss <codyoss@google.com>
2023-09-29 14:33:30 +00:00
Jin Qin
43b6a7ba19 google: adding support for external account authorized user
To support a new type of credential: `ExternalAccountAuthorizedUser`

* Refactor the common dependency STS to a separate package.
* Adding the `externalaccountauthorizeduser` package.

Change-Id: I9b9624f912d216b67a0d31945a50f057f747710b
GitHub-Last-Rev: 6e2aaff345
GitHub-Pull-Request: golang/oauth2#671
Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/531095
Reviewed-by: Leo Siracusa <leosiracusa@google.com>
Reviewed-by: Alex Eitzman <eitzman@google.com>
Run-TryBot: Cody Oss <codyoss@google.com>
Reviewed-by: Cody Oss <codyoss@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
2023-09-28 22:24:46 +00:00
M Hickford
14b275c918 oauth2: workaround misspelling of verification_uri
Some servers misspell verification_uri as verification_url, contrary to spec RFC 8628

Example server https://issuetracker.google.com/issues/151238144

Fixes #666

Change-Id: I89e354368bbb0a4e3b979bb547b4cb37bbe1cc02
GitHub-Last-Rev: bbf169b52d
GitHub-Pull-Request: golang/oauth2#667
Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/527835
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Nikolay Turpitko <nick.turpitko@gmail.com>
Auto-Submit: Bryan Mills <bcmills@google.com>
Reviewed-by: Matthew Dempsky <mdempsky@google.com>
Reviewed-by: Bryan Mills <bcmills@google.com>
Run-TryBot: Matt Hickford <matt.hickford@gmail.com>
2023-09-22 21:51:39 +00:00
aeitzman
18352fc433 google/internal/externalaccount: adding BYOID Metrics
Adds framework for sending BYOID metrics via the x-goog-api-client header on outgoing sts requests. Also adds a header file for getting the current version of GoLang

Change-Id: Id5431def96f4cfc03e4ada01d5fb8cac8cfa56a9
GitHub-Last-Rev: c93cd478e5
GitHub-Pull-Request: golang/oauth2#661
Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/523595
Reviewed-by: Leo Siracusa <leosiracusa@google.com>
Run-TryBot: Cody Oss <codyoss@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Cody Oss <codyoss@google.com>
2023-09-22 20:39:34 +00:00
M Hickford
9095a51613 oauth2: clarify error if endpoint missing DeviceAuthURL
Change-Id: I36eb5eb66099161785160f4f39ea1c7f64ad6e74
GitHub-Last-Rev: 31cfe8150f
GitHub-Pull-Request: golang/oauth2#664
Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/526302
Reviewed-by: Matthew Dempsky <mdempsky@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Auto-Submit: Bryan Mills <bcmills@google.com>
Reviewed-by: Bryan Mills <bcmills@google.com>
2023-09-22 16:24:29 +00:00
Jin Qin
2d9e4a2adf oauth2/google: remove meta validations for aws external credentials
Remove the url validations to keep a consistency with other libraries.

Change-Id: Icb1767edc000d9695db3f0c7ca271918fb2083f5
GitHub-Last-Rev: af89ee0c72
GitHub-Pull-Request: golang/oauth2#660
Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/522395
Reviewed-by: Leo Siracusa <leosiracusa@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Cody Oss <codyoss@google.com>
Run-TryBot: Cody Oss <codyoss@google.com>
2023-09-12 16:01:49 +00:00
M Hickford
55cd552a36 oauth2: support PKCE
Fixes #603

Fixes golang/go#59835

Change-Id: Ica0cfef975ba9511e00f097498d33ba27dafca0d
GitHub-Last-Rev: f01f7593a3
GitHub-Pull-Request: golang/oauth2#625
Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/463979
Reviewed-by: Cherry Mui <cherryyz@google.com>
Run-TryBot: Matt Hickford <matt.hickford@gmail.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Roland Shoemaker <roland@golang.org>
2023-09-07 17:49:42 +00:00
M Hickford
e3fb0fb3af oauth2: support device flow
Device Authorization Grant following RFC 8628 https://datatracker.ietf.org/doc/html/rfc8628

Tested with GitHub

Fixes #418

Fixes golang/go#58126

Co-authored-by: cmP <centimitr@gmail.com>

Change-Id: Id588867110c6a5289bf1026da5d7ead88f9c7d14
GitHub-Last-Rev: 9a126d7b53
GitHub-Pull-Request: golang/oauth2#609
Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/450155
Commit-Queue: Bryan Mills <bcmills@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Than McIntosh <thanm@google.com>
Auto-Submit: Bryan Mills <bcmills@google.com>
Run-TryBot: Matt Hickford <matt.hickford@gmail.com>
Reviewed-by: Bryan Mills <bcmills@google.com>
Run-TryBot: Bryan Mills <bcmills@google.com>
2023-09-06 16:35:20 +00:00
Gopher Robot
07085280e4 go.mod: update golang.org/x dependencies
Update golang.org/x dependencies to their latest tagged versions.

Change-Id: I2fb95ca59417e20377bc315094221fa7165128c8
Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/525675
Reviewed-by: Heschi Kreinick <heschi@google.com>
Run-TryBot: Gopher Robot <gobot@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Auto-Submit: Gopher Robot <gobot@golang.org>
2023-09-05 16:42:47 +00:00
Brad Fitzpatrick
a835fc4358 oauth2: move global auth style cache to be per-Config
In 80673b4a4 (https://go.dev/cl/157820) I added a never-shrinking
package-global cache to remember which auto-detected auth style (HTTP
headers vs POST) was supported by a certain OAuth2 server, keyed by
its URL.

Unfortunately, some multi-tenant SaaS OIDC servers behave poorly and
have one global OpenID configuration document for all of their
customers which says ("we support all auth styles! you pick!") but
then give each customer control of which style they specifically
accept. This is bogus behavior on their part, but the oauth2 package's
global caching per URL isn't helping. (It's also bad to have a
package-global cache that can never be GC'ed)

So, this change moves the cache to hang off the oauth *Configs
instead. Unfortunately, it does so with some backwards compatiblity
compromises (an atomic.Value hack), lest people are using old versions
of Go still or copying a Config by value, both of which this package
previously accidentally supported, even though they weren't tested.

This change also means that anybody that's repeatedly making ephemeral
oauth.Configs without an explicit auth style will be losing &
reinitializing their cache on any auth style failures + fallbacks to
the other style. I think that should be pretty rare. People seem to
make an oauth2.Config once earlier and stash it away somewhere (often
deep in a token fetcher or HTTP client/transport).

Change-Id: I91f107368ab3c3d77bc425eeef65372a589feb7b
Signed-off-by: Brad Fitzpatrick <bradfitz@golang.org>
Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/515675
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Roland Shoemaker <roland@golang.org>
Reviewed-by: Adrian Dewhurst <adrian@tailscale.com>
Reviewed-by: Michael Knyszek <mknyszek@google.com>
2023-08-09 17:53:10 +00:00
Gopher Robot
2e4a4e2bfb go.mod: update golang.org/x dependencies
Update golang.org/x dependencies to their latest tagged versions.
Once this CL is submitted, and post-submit testing succeeds on all
first-class ports across all supported Go versions, this repository
will be tagged with its next minor version.

Change-Id: I953aeb97bb9ed634f69dc93cf1f21392261c930c
Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/516037
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
Run-TryBot: Gopher Robot <gobot@golang.org>
Reviewed-by: Carlos Amedee <carlos@golang.org>
Auto-Submit: Gopher Robot <gobot@golang.org>
TryBot-Result: Gopher Robot <gobot@golang.org>
2023-08-04 23:51:27 +00:00
Dmitri Shuralyov
ac6658e9cb all: update go version to 1.18
Go versions 1.16 and 1.17 are long since unsupported per Go release
policy (https://go.dev/doc/devel/release#policy).

Updating go.mod's go statement to 1.18 makes it so that 'go mod tidy'
doesn't include checksums needed for the full module graph loaded by
Go 1.16¹ that were recently added in CL 507840.

It also makes go fix remove the now-obsolete // +build lines².

Done using cmd/go at go1.21rc2:

$ go get go@1.18
go: upgraded go 1.17 => 1.18
$ go mod tidy
$ go fix ./...
google/appengine_gen1.go: fixed buildtag
google/appengine_gen2_flex.go: fixed buildtag
internal/client_appengine.go: fixed buildtag

¹ https://go.dev/ref/mod#graph-pruning
² https://go.dev/doc/go1.18#go-build-lines

Change-Id: I6c6295adef1f5c64a196c2e66005763893efe5e7
Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/507878
Auto-Submit: Dmitri Shuralyov <dmitshur@golang.org>
Reviewed-by: Cody Oss <codyoss@google.com>
Run-TryBot: Dmitri Shuralyov <dmitshur@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
Reviewed-by: Bryan Mills <bcmills@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
2023-07-05 21:55:59 +00:00
Gopher Robot
ec5679f607 go.mod: update golang.org/x dependencies
Update golang.org/x dependencies to their latest tagged versions.
Once this CL is submitted, and post-submit testing succeeds on all
first-class ports across all supported Go versions, this repository
will be tagged with its next minor version.

Change-Id: I6b389549fe4bc53a62cb383c5fb10156ccfcffba
Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/507840
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
Run-TryBot: Gopher Robot <gobot@golang.org>
Auto-Submit: Gopher Robot <gobot@golang.org>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Heschi Kreinick <heschi@google.com>
2023-07-05 21:07:49 +00:00
Bryan C. Mills
989acb1bfe all: update dependencies to their latest versions
This change was prepared by running:
	go1.21rc2 get -u -t ./...
	go1.21rc2 mod tidy -compat=1.17

Change-Id: I533c4361aae073b7a5280aad2c2e5eea752df62a
Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/506296
Reviewed-by: Cody Oss <codyoss@google.com>
Run-TryBot: Bryan Mills <bcmills@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Auto-Submit: Bryan Mills <bcmills@google.com>
2023-06-26 19:20:11 +00:00
Gopher Robot
2323c81c8d go.mod: update golang.org/x dependencies
Update golang.org/x dependencies to their latest tagged versions.
Once this CL is submitted, and post-submit testing succeeds on all
first-class ports across all supported Go versions, this repository
will be tagged with its next minor version.

Change-Id: I7a693f42e110b957194337a0d355dd1f2a5e14ca
Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/502797
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
Auto-Submit: Gopher Robot <gobot@golang.org>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Carlos Amedee <carlos@golang.org>
Run-TryBot: Gopher Robot <gobot@golang.org>
2023-06-13 14:28:47 +00:00
Cody Oss
839de2255f google: don't check for IsNotExist for well-known file
There are cases when reading this file that a ENOTDIR is returned.
Because of this it is safer to just fall-back when any error
happens from reading the gcloud file.

Change-Id: Ie8e45ad508643e900adb5c9787907aaa50cceb5d
Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/493695
Run-TryBot: Cody Oss <codyoss@google.com>
Reviewed-by: Russ Cox <rsc@golang.org>
Auto-Submit: Cody Oss <codyoss@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
2023-05-08 21:24:50 +00:00
Gopher Robot
0690208dba go.mod: update golang.org/x dependencies
Update golang.org/x dependencies to their latest tagged versions.
Once this CL is submitted, and post-submit testing succeeds on all
first-class ports across all supported Go versions, this repository
will be tagged with its next minor version.

Change-Id: I97dfa241b763dfba4fc0c02da2f241255e2f53d1
Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/493576
Reviewed-by: Heschi Kreinick <heschi@google.com>
Auto-Submit: Gopher Robot <gobot@golang.org>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
Run-TryBot: Gopher Robot <gobot@golang.org>
2023-05-08 17:08:26 +00:00
cui fliter
451d5d662f internal: remove repeated definite articles
Change-Id: I0ce35bd2b7b870de9c0ffd898f245b49edbe55f7
Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/489715
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
Run-TryBot: shuang cui <imcusg@gmail.com>
Auto-Submit: Dmitri Shuralyov <dmitshur@golang.org>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Bryan Mills <bcmills@google.com>
2023-05-04 16:27:46 +00:00
M Hickford
cfe200d5bb oauth2: parse RFC 6749 error response
Parse error response described in https://datatracker.ietf.org/doc/html/rfc6749#section-5.2

Handle unorthodox servers responding 200 in error case.

Implements API changes in accepted proposal https://github.com/golang/go/issues/58125

Fixes #441
Fixes #274
Updates #173

Change-Id: If9399c3f952ac0501edbeefeb3a71ed057ca8d37
GitHub-Last-Rev: 0030e27422
GitHub-Pull-Request: golang/oauth2#610
Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/451076
Run-TryBot: Matt Hickford <matt.hickford@gmail.com>
Run-TryBot: Damien Neil <dneil@google.com>
Reviewed-by: Matt Hickford <matt.hickford@gmail.com>
Reviewed-by: Damien Neil <dneil@google.com>
Reviewed-by: Cody Oss <codyoss@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
2023-04-11 16:15:57 +00:00
Gopher Robot
36075149c5 go.mod: update golang.org/x dependencies
Update golang.org/x dependencies to their latest tagged versions.
Once this CL is submitted, and post-submit testing succeeds on all
first-class ports across all supported Go versions, this repository
will be tagged with its next minor version.

Change-Id: If1689e1b37e36e8e8dd1cfc37fe9cb94bd49c807
Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/482856
TryBot-Result: Gopher Robot <gobot@golang.org>
Auto-Submit: Gopher Robot <gobot@golang.org>
Reviewed-by: Carlos Amedee <carlos@golang.org>
Run-TryBot: Gopher Robot <gobot@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
2023-04-06 17:54:20 +00:00
Cody Oss
4abfd87339 google: add CredentialsParams.EarlyTokenRefresh
This option is a followup to to cl/479676 where an option was added
to configure the preemptive token refresh. Currently the option
in this package is only being used by compute credentials. In the
future we can support more/all auth flows but that would require
a lot of new surfaces to be added. Compute credentials are currently
the only case where we are expirencing the need to configure this
setting.

Change-Id: Ib78ca4beec44d0fe030ae81e84c8fcc4924793ba
Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/479956
Run-TryBot: Cody Oss <codyoss@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Roland Shoemaker <roland@golang.org>
2023-03-29 20:00:17 +00:00
Roland Shoemaker
1e7f329364 oauth2: add ReuseTokenSourceWithExpiry
Add a constructor which allows for the configuration of the expiryDelta
buffer. Due to the construction of reuseTokenSource and Token we need
to store the new delta in both places, so the behavior of Valid is
consistent regardless of where it is called from.

Fixes #623

Change-Id: I89f9c206a9cc16bb473b8c619605c8410a82fff0
Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/479676
Run-TryBot: Roland Shoemaker <roland@golang.org>
Reviewed-by: Cody Oss <codyoss@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
2023-03-28 18:36:12 +00:00
thomas-goncalves
86850e0723 oauth2: fix typo
Change-Id: I515f8897cc79c58a8a49df84ccddc5acd9536d87
GitHub-Last-Rev: 5acbebb81b
GitHub-Pull-Request: golang/oauth2#616
Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/459695
Run-TryBot: Cody Oss <codyoss@google.com>
Reviewed-by: Heschi Kreinick <heschi@google.com>
Auto-Submit: Dmitri Shuralyov <dmitshur@golang.org>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Cody Oss <codyoss@google.com>
Reviewed-by: Matt Hickford <matt.hickford@gmail.com>
2023-03-24 18:42:48 +00:00
aeitzman
a6e37e7441 google: Updating 3pi documentation
Fixing dead links in workload docs, adds workforce documentation

Change-Id: Ifad86e1937997f96ef577f5469d1e6fe496197b5
GitHub-Last-Rev: af288081ce
GitHub-Pull-Request: golang/oauth2#638
Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/478555
Auto-Submit: Cody Oss <codyoss@google.com>
Reviewed-by: Leo Siracusa <leosiracusa@google.com>
Reviewed-by: Cody Oss <codyoss@google.com>
Run-TryBot: Cody Oss <codyoss@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
2023-03-22 21:13:33 +00:00
Cody Oss
54b70c833f google: update missing auth help URL
Update the URL to a newer page that better describes how to set
up credentials in different environments.

Change-Id: Ic0726fe298c543265d333cda60d62c235e4e2293
Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/473735
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
Run-TryBot: Cody Oss <codyoss@google.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
Auto-Submit: Cody Oss <codyoss@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
2023-03-17 18:50:21 +00:00
M Hickford
2fc4ef5a6f README: encourage issues and proposals before changes
Text verbatim from https://go.dev/doc/contribute

Change-Id: Iefdcf9e9f771b9e55601bf9c9b59e20593b4573a
GitHub-Last-Rev: ba45caadaf
GitHub-Pull-Request: golang/oauth2#632
Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/471281
Run-TryBot: Matthew Hickford <hickford@google.com>
Reviewed-by: Heschi Kreinick <heschi@google.com>
Reviewed-by: Matthew Hickford <hickford@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
2023-03-10 21:26:16 +00:00
Gopher Robot
62b4eedd72 go.mod: update golang.org/x dependencies
Update golang.org/x dependencies to their latest tagged versions.
Once this CL is submitted, and post-submit testing succeeds on all
first-class ports across all supported Go versions, this repository
will be tagged with its next minor version.

Change-Id: If7957dff90ca75fa8fd84ace90ed15066bfd3e48
Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/473438
Reviewed-by: Heschi Kreinick <heschi@google.com>
Auto-Submit: Gopher Robot <gobot@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
Run-TryBot: Gopher Robot <gobot@golang.org>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
2023-03-05 04:01:11 +00:00
Andy Zhao
885f294722 google: Add support for OAuth2 token exchange over mTLS
With Context Aware Access enabled, users must use the endpoint "https://oauth2.mtls.googleapis.com/token" for token exchange. This PR adds support for runtime configuration of the OAuth2 token endpoint (as determined by the caller). If using the mTLS oauth2 endpoint, the caller will also need to specify an mTLS-enabled HTTPClient via the "context" mechanism for use by the OAuth2 transport.

Change-Id: Ic83342ec1d224d3acdabf00d863249330424fc54
GitHub-Last-Rev: 07e4849e96
GitHub-Pull-Request: golang/oauth2#630
Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/470396
Run-TryBot: Matthew Hickford <hickford@google.com>
Reviewed-by: Shin Fan <shinfan@google.com>
Run-TryBot: Shin Fan <shinfan@google.com>
Reviewed-by: Matthew Hickford <hickford@google.com>
Reviewed-by: Andy Zhao <andyzhao@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
2023-03-03 18:55:16 +00:00
Koichi Shiraishi
6f9c1a18cc google: use Credentials instead of deprecated DefaultCredentials
Change-Id: Ie2d972df025f6b6b7d172015aae7eb34e355cb28
Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/412034
Reviewed-by: Matt Hickford <matt.hickford@gmail.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Run-TryBot: Cody Oss <codyoss@google.com>
Reviewed-by: Matthew Hickford <hickford@google.com>
Run-TryBot: Matt Hickford <matt.hickford@gmail.com>
Auto-Submit: Dmitri Shuralyov <dmitshur@golang.org>
Reviewed-by: Cody Oss <codyoss@google.com>
2023-02-27 20:54:01 +00:00
aeitzman
c82d0e16dc google/internal/externalaccount: Removed URL validation for google URLs in ADC files
Removes URL validation for token_url, service_account_impersonation_url to allow for TPC urls and adds line to the docs to warn users. See https://github.com/googleapis/google-auth-library-nodejs/pull/1517 for same change in node.js library.

Change-Id: I85fa67ee0b99deed2adb75668a1b5501851c499c
GitHub-Last-Rev: 15d7759884
GitHub-Pull-Request: golang/oauth2#627
Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/465696
Reviewed-by: Cody Oss <codyoss@google.com>
Run-TryBot: Cody Oss <codyoss@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Leo Siracusa <leosiracusa@google.com>
2023-02-17 18:48:36 +00:00
Gopher Robot
adbaf66a0b go.mod: update golang.org/x dependencies
Update golang.org/x dependencies to their latest tagged versions.
Once this CL is submitted, and post-submit testing succeeds on all
first-class ports across all supported Go versions, this repository
will be tagged with its next minor version.

Change-Id: I011be77aad8a91714b6af22e05408254e4110f3f
Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/466737
Auto-Submit: Gopher Robot <gobot@golang.org>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Heschi Kreinick <heschi@google.com>
Run-TryBot: Gopher Robot <gobot@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
2023-02-08 21:58:22 +00:00
Eric Chiang
e07593a4c4 oauth2: remove direct dependency on golang.org/x/net
Change-Id: If8fd952f4bfd3bac3e85592a49616adf4b43ce51
Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/460764
Reviewed-by: Cody Oss <codyoss@google.com>
Run-TryBot: Matt Hickford <matt.hickford@gmail.com>
Reviewed-by: Matt Hickford <matt.hickford@gmail.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
2023-01-11 15:44:51 +00:00
Gopher Robot
34ffb07a99 go.mod: update golang.org/x dependencies
Update golang.org/x dependencies to their latest tagged versions.
Once this CL is submitted, and post-submit testing succeeds on all
first-class ports across all supported Go versions, this repository
will be tagged with its next minor version.

Change-Id: Ic017d7ac9986ab17999bbf9a9528e5671946e57a
Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/460501
Reviewed-by: Heschi Kreinick <heschi@google.com>
Run-TryBot: Gopher Robot <gobot@golang.org>
Auto-Submit: Gopher Robot <gobot@golang.org>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
2023-01-04 16:08:32 +00:00
Gopher Robot
b177c21ac9 go.mod: update golang.org/x dependencies
Update golang.org/x dependencies to their latest tagged versions.
Once this CL is submitted, and post-submit testing succeeds on all
first-class ports across all supported Go versions, this repository
will be tagged with its next minor version.

Change-Id: Iddd5948b1dc74c208372e82df1971f9cd2b5f296
Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/455435
Run-TryBot: Gopher Robot <gobot@golang.org>
Auto-Submit: Gopher Robot <gobot@golang.org>
Reviewed-by: Heschi Kreinick <heschi@google.com>
Reviewed-by: Carlos Amedee <carlos@golang.org>
TryBot-Result: Gopher Robot <gobot@golang.org>
2022-12-06 15:30:02 +00:00
aeitzman
510acbce1f google/internal/externalaccount: Added check for aws region and security credential environment variables before aws metadata call
Adds check for aws values in environment variables before the metadata server is called to prevent unnecessary off box calls. See https://github.com/googleapis/google-auth-library-java/pull/1100 for same change in java library.

Change-Id: Ie86a899be88c38d3fcbbe377f9bf30a7a66530c0
GitHub-Last-Rev: bcab69572c
GitHub-Pull-Request: golang/oauth2#612
Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/453715
Reviewed-by: Leo Siracusa <leosiracusa@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Run-TryBot: Cody Oss <codyoss@google.com>
Auto-Submit: Cody Oss <codyoss@google.com>
Reviewed-by: Cody Oss <codyoss@google.com>
2022-11-30 16:58:33 +00:00
Ryan Kohler
ec4a9b2ff2 google/internal/externalaccount: Adding metadata verification
Change-Id: I4d664862b7b287131c1481b238ebd0875f7c233b
GitHub-Last-Rev: 74bcc33f5e
GitHub-Pull-Request: golang/oauth2#608
Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/449975
Run-TryBot: Cody Oss <codyoss@google.com>
Auto-Submit: Cody Oss <codyoss@google.com>
Reviewed-by: Leo Siracusa <leosiracusa@google.com>
Reviewed-by: Cody Oss <codyoss@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
2022-11-17 22:06:29 +00:00
44 changed files with 1935 additions and 471 deletions

View File

@@ -19,7 +19,7 @@ See pkg.go.dev for further documentation and examples.
* [pkg.go.dev/golang.org/x/oauth2](https://pkg.go.dev/golang.org/x/oauth2)
* [pkg.go.dev/golang.org/x/oauth2/google](https://pkg.go.dev/golang.org/x/oauth2/google)
## Policy for new packages
## Policy for new endpoints
We no longer accept new provider-specific packages in this repo if all
they do is add a single endpoint variable. If you just want to add a
@@ -29,8 +29,12 @@ package.
## Report Issues / Send Patches
This repository uses Gerrit for code changes. To learn how to submit changes to
this repository, see https://golang.org/doc/contribute.html.
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:
* 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.
* The code owners are listed at [dev.golang.org/owners](https://dev.golang.org/owners#:~:text=x/oauth2).

View File

@@ -47,6 +47,10 @@ type Config struct {
// client ID & client secret sent. The zero value means to
// auto-detect.
AuthStyle oauth2.AuthStyle
// authStyleCache caches which auth style to use when Endpoint.AuthStyle is
// the zero value (AuthStyleAutoDetect).
authStyleCache internal.LazyAuthStyleCache
}
// Token uses client credentials to retrieve a token.
@@ -103,7 +107,7 @@ func (c *tokenSource) Token() (*oauth2.Token, error) {
v[k] = p
}
tk, err := internal.RetrieveToken(c.ctx, c.conf.ClientID, c.conf.ClientSecret, c.conf.TokenURL, v, internal.AuthStyle(c.conf.AuthStyle))
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())
if err != nil {
if rErr, ok := err.(*internal.RetrieveError); ok {
return nil, (*oauth2.RetrieveError)(rErr)

View File

@@ -12,8 +12,6 @@ import (
"net/http/httptest"
"net/url"
"testing"
"golang.org/x/oauth2/internal"
)
func newConf(serverURL string) *Config {
@@ -114,7 +112,6 @@ func TestTokenRequest(t *testing.T) {
}
func TestTokenRefreshRequest(t *testing.T) {
internal.ResetAuthCache()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.String() == "/somethingelse" {
return

198
deviceauth.go Normal file
View File

@@ -0,0 +1,198 @@
package oauth2
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
"golang.org/x/oauth2/internal"
)
// https://datatracker.ietf.org/doc/html/rfc8628#section-3.5
const (
errAuthorizationPending = "authorization_pending"
errSlowDown = "slow_down"
errAccessDenied = "access_denied"
errExpiredToken = "expired_token"
)
// DeviceAuthResponse describes a successful RFC 8628 Device Authorization Response
// https://datatracker.ietf.org/doc/html/rfc8628#section-3.2
type DeviceAuthResponse struct {
// DeviceCode
DeviceCode string `json:"device_code"`
// UserCode is the code the user should enter at the verification uri
UserCode string `json:"user_code"`
// VerificationURI is where user should enter the user code
VerificationURI string `json:"verification_uri"`
// 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.
VerificationURIComplete string `json:"verification_uri_complete,omitempty"`
// Expiry is when the device code and user code expire
Expiry time.Time `json:"expires_in,omitempty"`
// Interval is the duration in seconds that Poll should wait between requests
Interval int64 `json:"interval,omitempty"`
}
func (d DeviceAuthResponse) MarshalJSON() ([]byte, error) {
type Alias DeviceAuthResponse
var expiresIn int64
if !d.Expiry.IsZero() {
expiresIn = int64(time.Until(d.Expiry).Seconds())
}
return json.Marshal(&struct {
ExpiresIn int64 `json:"expires_in,omitempty"`
*Alias
}{
ExpiresIn: expiresIn,
Alias: (*Alias)(&d),
})
}
func (c *DeviceAuthResponse) UnmarshalJSON(data []byte) error {
type Alias DeviceAuthResponse
aux := &struct {
ExpiresIn int64 `json:"expires_in"`
// workaround misspelling of verification_uri
VerificationURL string `json:"verification_url"`
*Alias
}{
Alias: (*Alias)(c),
}
if err := json.Unmarshal(data, &aux); err != nil {
return err
}
if aux.ExpiresIn != 0 {
c.Expiry = time.Now().UTC().Add(time.Second * time.Duration(aux.ExpiresIn))
}
if c.VerificationURI == "" {
c.VerificationURI = aux.VerificationURL
}
return nil
}
// DeviceAuth returns a device auth struct which contains a device code
// and authorization information provided for users to enter on another device.
func (c *Config) DeviceAuth(ctx context.Context, opts ...AuthCodeOption) (*DeviceAuthResponse, error) {
// https://datatracker.ietf.org/doc/html/rfc8628#section-3.1
v := url.Values{
"client_id": {c.ClientID},
}
if len(c.Scopes) > 0 {
v.Set("scope", strings.Join(c.Scopes, " "))
}
for _, opt := range opts {
opt.setValue(v)
}
return retrieveDeviceAuth(ctx, c, v)
}
func retrieveDeviceAuth(ctx context.Context, c *Config, v url.Values) (*DeviceAuthResponse, error) {
if c.Endpoint.DeviceAuthURL == "" {
return nil, errors.New("endpoint missing DeviceAuthURL")
}
req, err := http.NewRequest("POST", c.Endpoint.DeviceAuthURL, strings.NewReader(v.Encode()))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Accept", "application/json")
t := time.Now()
r, err := internal.ContextClient(ctx).Do(req)
if err != nil {
return nil, err
}
body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20))
if err != nil {
return nil, fmt.Errorf("oauth2: cannot auth device: %v", err)
}
if code := r.StatusCode; code < 200 || code > 299 {
return nil, &RetrieveError{
Response: r,
Body: body,
}
}
da := &DeviceAuthResponse{}
err = json.Unmarshal(body, &da)
if err != nil {
return nil, fmt.Errorf("unmarshal %s", err)
}
if !da.Expiry.IsZero() {
// Make a small adjustment to account for time taken by the request
da.Expiry = da.Expiry.Add(-time.Since(t))
}
return da, nil
}
// DeviceAccessToken polls the server to exchange a device code for a token.
func (c *Config) DeviceAccessToken(ctx context.Context, da *DeviceAuthResponse, opts ...AuthCodeOption) (*Token, error) {
if !da.Expiry.IsZero() {
var cancel context.CancelFunc
ctx, cancel = context.WithDeadline(ctx, da.Expiry)
defer cancel()
}
// https://datatracker.ietf.org/doc/html/rfc8628#section-3.4
v := url.Values{
"client_id": {c.ClientID},
"grant_type": {"urn:ietf:params:oauth:grant-type:device_code"},
"device_code": {da.DeviceCode},
}
if len(c.Scopes) > 0 {
v.Set("scope", strings.Join(c.Scopes, " "))
}
for _, opt := range opts {
opt.setValue(v)
}
// "If no value is provided, clients MUST use 5 as the default."
// https://datatracker.ietf.org/doc/html/rfc8628#section-3.2
interval := da.Interval
if interval == 0 {
interval = 5
}
ticker := time.NewTicker(time.Duration(interval) * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-ticker.C:
tok, err := retrieveToken(ctx, c, v)
if err == nil {
return tok, nil
}
e, ok := err.(*RetrieveError)
if !ok {
return nil, err
}
switch e.ErrorCode {
case errSlowDown:
// https://datatracker.ietf.org/doc/html/rfc8628#section-3.5
// "the interval MUST be increased by 5 seconds for this and all subsequent requests"
interval += 5
ticker.Reset(time.Duration(interval) * time.Second)
case errAuthorizationPending:
// Do nothing.
case errAccessDenied, errExpiredToken:
fallthrough
default:
return tok, err
}
}
}
}

97
deviceauth_test.go Normal file
View File

@@ -0,0 +1,97 @@
package oauth2
import (
"context"
"encoding/json"
"fmt"
"strings"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
)
func TestDeviceAuthResponseMarshalJson(t *testing.T) {
tests := []struct {
name string
response DeviceAuthResponse
want string
}{
{
name: "empty",
response: DeviceAuthResponse{},
want: `{"device_code":"","user_code":"","verification_uri":""}`,
},
{
name: "soon",
response: DeviceAuthResponse{
Expiry: time.Now().Add(100*time.Second + 999*time.Millisecond),
},
want: `{"expires_in":100,"device_code":"","user_code":"","verification_uri":""}`,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
begin := time.Now()
gotBytes, err := json.Marshal(tc.response)
if err != nil {
t.Fatal(err)
}
if strings.Contains(tc.want, "expires_in") && time.Since(begin) > 999*time.Millisecond {
t.Skip("test ran too slowly to compare `expires_in`")
}
got := string(gotBytes)
if got != tc.want {
t.Errorf("want=%s, got=%s", tc.want, got)
}
})
}
}
func TestDeviceAuthResponseUnmarshalJson(t *testing.T) {
tests := []struct {
name string
data string
want DeviceAuthResponse
}{
{
name: "empty",
data: `{}`,
want: DeviceAuthResponse{},
},
{
name: "soon",
data: `{"expires_in":100}`,
want: DeviceAuthResponse{Expiry: time.Now().UTC().Add(100 * time.Second)},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
begin := time.Now()
got := DeviceAuthResponse{}
err := json.Unmarshal([]byte(tc.data), &got)
if err != nil {
t.Fatal(err)
}
if !cmp.Equal(got, tc.want, cmpopts.IgnoreUnexported(DeviceAuthResponse{}), cmpopts.EquateApproxTime(time.Second+time.Since(begin))) {
t.Errorf("want=%#v, got=%#v", tc.want, got)
}
})
}
}
func ExampleConfig_DeviceAuth() {
var config Config
ctx := context.Background()
response, err := config.DeviceAuth(ctx)
if err != nil {
panic(err)
}
fmt.Printf("please enter code %s at %s\n", response.UserCode, response.VerificationURI)
token, err := config.DeviceAccessToken(ctx, response)
if err != nil {
panic(err)
}
fmt.Println(token)
}

View File

@@ -55,8 +55,9 @@ var Fitbit = oauth2.Endpoint{
// GitHub is the endpoint for Github.
var GitHub = oauth2.Endpoint{
AuthURL: "https://github.com/login/oauth/authorize",
TokenURL: "https://github.com/login/oauth/access_token",
AuthURL: "https://github.com/login/oauth/authorize",
TokenURL: "https://github.com/login/oauth/access_token",
DeviceAuthURL: "https://github.com/login/device/code",
}
// GitLab is the endpoint for GitLab.
@@ -69,6 +70,7 @@ var GitLab = oauth2.Endpoint{
var Google = oauth2.Endpoint{
AuthURL: "https://accounts.google.com/o/oauth2/auth",
TokenURL: "https://oauth2.googleapis.com/token",
DeviceAuthURL: "https://oauth2.googleapis.com/device/code",
}
// Heroku is the endpoint for Heroku.

View File

@@ -26,9 +26,13 @@ func ExampleConfig() {
},
}
// use PKCE to protect against CSRF attacks
// https://www.ietf.org/archive/id/draft-ietf-oauth-security-topics-22.html#name-countermeasures-6
verifier := oauth2.GenerateVerifier()
// Redirect user to consent page to ask for permission
// for the scopes specified above.
url := conf.AuthCodeURL("state", oauth2.AccessTypeOffline)
url := conf.AuthCodeURL("state", oauth2.AccessTypeOffline, oauth2.S256ChallengeOption(verifier))
fmt.Printf("Visit the URL for the auth dialog: %v", url)
// Use the authorization code that is pushed to the redirect
@@ -39,7 +43,7 @@ func ExampleConfig() {
if _, err := fmt.Scan(&code); err != nil {
log.Fatal(err)
}
tok, err := conf.Exchange(ctx, code)
tok, err := conf.Exchange(ctx, code, oauth2.VerifierOption(verifier))
if err != nil {
log.Fatal(err)
}

View File

@@ -6,11 +6,8 @@
package github // import "golang.org/x/oauth2/github"
import (
"golang.org/x/oauth2"
"golang.org/x/oauth2/endpoints"
)
// Endpoint is Github's OAuth 2.0 endpoint.
var Endpoint = oauth2.Endpoint{
AuthURL: "https://github.com/login/oauth/authorize",
TokenURL: "https://github.com/login/oauth/access_token",
}
var Endpoint = endpoints.GitHub

13
go.mod
View File

@@ -1,15 +1,16 @@
module golang.org/x/oauth2
go 1.17
go 1.18
require (
cloud.google.com/go/compute/metadata v0.2.0
github.com/google/go-cmp v0.5.8
golang.org/x/net v0.2.0
cloud.google.com/go/compute/metadata v0.2.3
github.com/google/go-cmp v0.5.9
google.golang.org/appengine v1.6.7
)
require (
github.com/golang/protobuf v1.5.2 // indirect
google.golang.org/protobuf v1.28.0 // indirect
cloud.google.com/go/compute v1.20.1 // indirect
github.com/golang/protobuf v1.5.3 // indirect
golang.org/x/net v0.16.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
)

44
go.sum
View File

@@ -1,46 +1,26 @@
cloud.google.com/go/compute/metadata v0.2.0 h1:nBbNSZyDpkNlo3DepaaLKVuO7ClyifSAmNloSCZrHnQ=
cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
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.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
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=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
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/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/net v0.16.0 h1:7eBu7KsSvFDtSXUIDbh3aqlK4DPsZ1rByC8PFfBThos=
golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
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/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=

View File

@@ -3,7 +3,6 @@
// license that can be found in the LICENSE file.
//go:build appengine
// +build appengine
// This file applies to App Engine first generation runtimes (<= Go 1.9).

View File

@@ -3,7 +3,6 @@
// license that can be found in the LICENSE file.
//go:build !appengine
// +build !appengine
// This file applies to App Engine second generation runtimes (>= Go 1.11) and App Engine flexible.

View File

@@ -8,17 +8,22 @@ import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"runtime"
"time"
"cloud.google.com/go/compute/metadata"
"golang.org/x/oauth2"
"golang.org/x/oauth2/authhandler"
)
const (
adcSetupURL = "https://cloud.google.com/docs/authentication/external/set-up-adc"
universeDomainDefault = "googleapis.com"
)
// Credentials holds Google credentials, including "Application Default Credentials".
// For more details, see:
// https://developers.google.com/accounts/docs/application-default-credentials
@@ -35,6 +40,18 @@ type Credentials struct {
// environment and not with a credentials file, e.g. when code is
// running on Google Cloud Platform.
JSON []byte
// universeDomain is the default service domain for a given Cloud universe.
universeDomain string
}
// UniverseDomain returns the default service domain for a given Cloud universe.
// The default value is "googleapis.com".
func (c *Credentials) UniverseDomain() string {
if c.universeDomain == "" {
return universeDomainDefault
}
return c.universeDomain
}
// DefaultCredentials is the old name of Credentials.
@@ -62,6 +79,18 @@ type CredentialsParams struct {
// PKCE is used to support PKCE flow. Optional for 3LO flow.
PKCE *authhandler.PKCEParams
// The OAuth2 TokenURL default override. This value overrides the default TokenURL,
// unless explicitly specified by the credentials config file. Optional.
TokenURL string
// EarlyTokenRefresh is the amount of time before a token expires that a new
// token will be preemptively fetched. If unset the default value is 10
// seconds.
//
// Note: This option is currently only respected when using credentials
// fetched from the GCE metadata server.
EarlyTokenRefresh time.Duration
}
func (params CredentialsParams) deepCopy() CredentialsParams {
@@ -127,17 +156,15 @@ func FindDefaultCredentialsWithParams(ctx context.Context, params CredentialsPar
// Second, try a well-known file.
filename := wellKnownFile()
if creds, err := readCredentialsFile(ctx, filename, params); err == nil {
return creds, nil
} else if !os.IsNotExist(err) {
return nil, fmt.Errorf("google: error getting credentials using well-known file (%v): %v", filename, err)
if b, err := os.ReadFile(filename); err == nil {
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 &DefaultCredentials{
return &Credentials{
ProjectID: appengineAppIDFunc(ctx),
TokenSource: AppEngineTokenSource(ctx, params.Scopes...),
}, nil
@@ -147,15 +174,14 @@ func FindDefaultCredentialsWithParams(ctx context.Context, params CredentialsPar
// or App Engine flexible, use the metadata server.
if metadata.OnGCE() {
id, _ := metadata.ProjectID()
return &DefaultCredentials{
return &Credentials{
ProjectID: id,
TokenSource: ComputeTokenSource("", params.Scopes...),
TokenSource: computeTokenSource("", params.EarlyTokenRefresh, params.Scopes...),
}, nil
}
// None are found; return helpful error.
const url = "https://developers.google.com/accounts/docs/application-default-credentials"
return nil, fmt.Errorf("google: could not find default credentials. See %v for more information.", url)
return nil, fmt.Errorf("google: could not find default credentials. See %v for more information", adcSetupURL)
}
// FindDefaultCredentials invokes FindDefaultCredentialsWithParams with the specified scopes.
@@ -189,15 +215,23 @@ func CredentialsFromJSONWithParams(ctx context.Context, jsonData []byte, params
if err := json.Unmarshal(jsonData, &f); err != nil {
return nil, err
}
universeDomain := f.UniverseDomain
// Authorized user credentials are only supported in the googleapis.com universe.
if f.Type == userCredentialsKey {
universeDomain = universeDomainDefault
}
ts, err := f.tokenSource(ctx, params)
if err != nil {
return nil, err
}
ts = newErrWrappingTokenSource(ts)
return &DefaultCredentials{
ProjectID: f.ProjectID,
TokenSource: ts,
JSON: jsonData,
return &Credentials{
ProjectID: f.ProjectID,
TokenSource: ts,
JSON: jsonData,
universeDomain: universeDomain,
}, nil
}
@@ -216,8 +250,8 @@ func wellKnownFile() string {
return filepath.Join(guessUnixHomeDir(), ".config", "gcloud", f)
}
func readCredentialsFile(ctx context.Context, filename string, params CredentialsParams) (*DefaultCredentials, error) {
b, err := ioutil.ReadFile(filename)
func readCredentialsFile(ctx context.Context, filename string, params CredentialsParams) (*Credentials, error) {
b, err := os.ReadFile(filename)
if err != nil {
return nil, err
}

124
google/default_test.go Normal file
View File

@@ -0,0 +1,124 @@
// Copyright 2023 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.
package google
import (
"context"
"testing"
)
var saJSONJWT = []byte(`{
"type": "service_account",
"project_id": "fake_project",
"private_key_id": "268f54e43a1af97cfc71731688434f45aca15c8b",
"private_key": "super secret key",
"client_email": "gopher@developer.gserviceaccount.com",
"client_id": "gopher.apps.googleusercontent.com",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/gopher%40fake_project.iam.gserviceaccount.com"
}`)
var saJSONJWTUniverseDomain = []byte(`{
"type": "service_account",
"project_id": "fake_project",
"universe_domain": "example.com",
"private_key_id": "268f54e43a1af97cfc71731688434f45aca15c8b",
"private_key": "super secret key",
"client_email": "gopher@developer.gserviceaccount.com",
"client_id": "gopher.apps.googleusercontent.com",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/gopher%40fake_project.iam.gserviceaccount.com"
}`)
var userJSON = []byte(`{
"client_id": "abc123.apps.googleusercontent.com",
"client_secret": "shh",
"refresh_token": "refreshing",
"type": "authorized_user",
"quota_project_id": "fake_project2"
}`)
var userJSONUniverseDomain = []byte(`{
"client_id": "abc123.apps.googleusercontent.com",
"client_secret": "shh",
"refresh_token": "refreshing",
"type": "authorized_user",
"quota_project_id": "fake_project2",
"universe_domain": "example.com"
}`)
func TestCredentialsFromJSONWithParams_SA(t *testing.T) {
ctx := context.Background()
scope := "https://www.googleapis.com/auth/cloud-platform"
params := CredentialsParams{
Scopes: []string{scope},
}
creds, err := CredentialsFromJSONWithParams(ctx, saJSONJWT, params)
if err != nil {
t.Fatal(err)
}
if want := "fake_project"; creds.ProjectID != want {
t.Fatalf("got %q, want %q", creds.ProjectID, want)
}
if want := "googleapis.com"; creds.UniverseDomain() != want {
t.Fatalf("got %q, want %q", creds.UniverseDomain(), want)
}
}
func TestCredentialsFromJSONWithParams_SA_UniverseDomain(t *testing.T) {
ctx := context.Background()
scope := "https://www.googleapis.com/auth/cloud-platform"
params := CredentialsParams{
Scopes: []string{scope},
}
creds, err := CredentialsFromJSONWithParams(ctx, saJSONJWTUniverseDomain, params)
if err != nil {
t.Fatal(err)
}
if want := "fake_project"; creds.ProjectID != want {
t.Fatalf("got %q, want %q", creds.ProjectID, want)
}
if want := "example.com"; creds.UniverseDomain() != want {
t.Fatalf("got %q, want %q", creds.UniverseDomain(), want)
}
}
func TestCredentialsFromJSONWithParams_User(t *testing.T) {
ctx := context.Background()
scope := "https://www.googleapis.com/auth/cloud-platform"
params := CredentialsParams{
Scopes: []string{scope},
}
creds, err := CredentialsFromJSONWithParams(ctx, userJSON, params)
if err != nil {
t.Fatal(err)
}
if want := "googleapis.com"; creds.UniverseDomain() != want {
t.Fatalf("got %q, want %q", creds.UniverseDomain(), want)
}
}
func TestCredentialsFromJSONWithParams_User_UniverseDomain(t *testing.T) {
ctx := context.Background()
scope := "https://www.googleapis.com/auth/cloud-platform"
params := CredentialsParams{
Scopes: []string{scope},
}
creds, err := CredentialsFromJSONWithParams(ctx, userJSONUniverseDomain, params)
if err != nil {
t.Fatal(err)
}
if want := "googleapis.com"; creds.UniverseDomain() != want {
t.Fatalf("got %q, want %q", creds.UniverseDomain(), want)
}
}

View File

@@ -26,7 +26,7 @@
//
// Using workload identity federation, your application can access Google Cloud
// resources from Amazon Web Services (AWS), Microsoft Azure or any identity
// provider that supports OpenID Connect (OIDC).
// provider that supports OpenID Connect (OIDC) or SAML 2.0.
// Traditionally, applications running outside Google Cloud have used service
// account keys to access Google Cloud resources. Using identity federation,
// you can allow your workload to impersonate a service account.
@@ -36,26 +36,75 @@
// Follow the detailed instructions on how to configure Workload Identity Federation
// in various platforms:
//
// Amazon Web Services (AWS): https://cloud.google.com/iam/docs/access-resources-aws
// Microsoft Azure: https://cloud.google.com/iam/docs/access-resources-azure
// OIDC identity provider: https://cloud.google.com/iam/docs/access-resources-oidc
// Amazon Web Services (AWS): https://cloud.google.com/iam/docs/workload-identity-federation-with-other-clouds#aws
// Microsoft Azure: https://cloud.google.com/iam/docs/workload-identity-federation-with-other-clouds#azure
// OIDC identity provider: https://cloud.google.com/iam/docs/workload-identity-federation-with-other-providers#oidc
// SAML 2.0 identity provider: https://cloud.google.com/iam/docs/workload-identity-federation-with-other-providers#saml
//
// For OIDC and SAML providers, the library can retrieve tokens in three ways:
// from a local file location (file-sourced credentials), from a server
// (URL-sourced credentials), or from a local executable (executable-sourced
// credentials).
// For file-sourced credentials, a background process needs to be continuously
// refreshing the file location with a new OIDC token prior to expiration.
// refreshing the file location with a new OIDC/SAML token prior to expiration.
// For tokens with one hour lifetimes, the token needs to be updated in the file
// every hour. The token can be stored directly as plain text or in JSON format.
// For URL-sourced credentials, a local server needs to host a GET endpoint to
// return the OIDC token. The response can be in plain text or JSON.
// return the OIDC/SAML token. The response can be in plain text or JSON.
// Additional required request headers can also be specified.
// For executable-sourced credentials, an application needs to be available to
// output the OIDC token and other information in a JSON format.
// output the OIDC/SAML token and other information in a JSON format.
// For more information on how these work (and how to implement
// executable-sourced credentials), please check out:
// https://cloud.google.com/iam/docs/using-workload-identity-federation#oidc
// https://cloud.google.com/iam/docs/workload-identity-federation-with-other-providers#create_a_credential_configuration
//
// Note that this library does not perform any validation on the token_url, token_info_url,
// or service_account_impersonation_url fields of the credential configuration.
// It is not recommended to use a credential configuration that you did not generate with
// the gcloud CLI unless you verify that the URL fields point to a googleapis.com domain.
//
// # Workforce Identity Federation
//
// Workforce identity federation lets you use an external identity provider (IdP) to
// authenticate and authorize a workforce—a group of users, such as employees, partners,
// and contractors—using IAM, so that the users can access Google Cloud services.
// Workforce identity federation extends Google Cloud's identity capabilities to support
// syncless, attribute-based single sign on.
//
// With workforce identity federation, your workforce can access Google Cloud resources
// using an external identity provider (IdP) that supports OpenID Connect (OIDC) or
// SAML 2.0 such as Azure Active Directory (Azure AD), Active Directory Federation
// Services (AD FS), Okta, and others.
//
// Follow the detailed instructions on how to configure Workload Identity Federation
// in various platforms:
//
// Azure AD: https://cloud.google.com/iam/docs/workforce-sign-in-azure-ad
// Okta: https://cloud.google.com/iam/docs/workforce-sign-in-okta
// OIDC identity provider: https://cloud.google.com/iam/docs/configuring-workforce-identity-federation#oidc
// SAML 2.0 identity provider: https://cloud.google.com/iam/docs/configuring-workforce-identity-federation#saml
//
// For workforce identity federation, the library can retrieve tokens in three ways:
// from a local file location (file-sourced credentials), from a server
// (URL-sourced credentials), or from a local executable (executable-sourced
// credentials).
// For file-sourced credentials, a background process needs to be continuously
// refreshing the file location with a new OIDC/SAML token prior to expiration.
// For tokens with one hour lifetimes, the token needs to be updated in the file
// every hour. The token can be stored directly as plain text or in JSON format.
// For URL-sourced credentials, a local server needs to host a GET endpoint to
// return the OIDC/SAML token. The response can be in plain text or JSON.
// Additional required request headers can also be specified.
// For executable-sourced credentials, an application needs to be available to
// output the OIDC/SAML token and other information in a JSON format.
// For more information on how these work (and how to implement
// executable-sourced credentials), please check out:
// https://cloud.google.com/iam/docs/workforce-obtaining-short-lived-credentials#generate_a_configuration_file_for_non-interactive_sign-in
//
// Note that this library does not perform any validation on the token_url, token_info_url,
// or service_account_impersonation_url fields of the credential configuration.
// It is not recommended to use a credential configuration that you did not generate with
// the gcloud CLI unless you verify that the URL fields point to a googleapis.com domain.
//
// # Credentials
//

View File

@@ -16,16 +16,21 @@ import (
"cloud.google.com/go/compute/metadata"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google/internal/externalaccount"
"golang.org/x/oauth2/google/internal/externalaccountauthorizeduser"
"golang.org/x/oauth2/jwt"
)
// Endpoint is Google's OAuth 2.0 default endpoint.
var Endpoint = oauth2.Endpoint{
AuthURL: "https://accounts.google.com/o/oauth2/auth",
TokenURL: "https://oauth2.googleapis.com/token",
AuthStyle: oauth2.AuthStyleInParams,
AuthURL: "https://accounts.google.com/o/oauth2/auth",
TokenURL: "https://oauth2.googleapis.com/token",
DeviceAuthURL: "https://oauth2.googleapis.com/device/code",
AuthStyle: oauth2.AuthStyleInParams,
}
// MTLSTokenURL is Google's OAuth 2.0 default mTLS endpoint.
const MTLSTokenURL = "https://oauth2.mtls.googleapis.com/token"
// JWTTokenURL is Google's OAuth 2.0 token URL to use with the JWT flow.
const JWTTokenURL = "https://oauth2.googleapis.com/token"
@@ -92,10 +97,11 @@ func JWTConfigFromJSON(jsonKey []byte, scope ...string) (*jwt.Config, error) {
// JSON key file types.
const (
serviceAccountKey = "service_account"
userCredentialsKey = "authorized_user"
externalAccountKey = "external_account"
impersonatedServiceAccount = "impersonated_service_account"
serviceAccountKey = "service_account"
userCredentialsKey = "authorized_user"
externalAccountKey = "external_account"
externalAccountAuthorizedUserKey = "external_account_authorized_user"
impersonatedServiceAccount = "impersonated_service_account"
)
// credentialsFile is the unmarshalled representation of a credentials file.
@@ -103,12 +109,13 @@ type credentialsFile struct {
Type string `json:"type"`
// Service Account fields
ClientEmail string `json:"client_email"`
PrivateKeyID string `json:"private_key_id"`
PrivateKey string `json:"private_key"`
AuthURL string `json:"auth_uri"`
TokenURL string `json:"token_uri"`
ProjectID string `json:"project_id"`
ClientEmail string `json:"client_email"`
PrivateKeyID string `json:"private_key_id"`
PrivateKey string `json:"private_key"`
AuthURL string `json:"auth_uri"`
TokenURL string `json:"token_uri"`
ProjectID string `json:"project_id"`
UniverseDomain string `json:"universe_domain"`
// User Credential fields
// (These typically come from gcloud auth.)
@@ -128,6 +135,9 @@ type credentialsFile struct {
QuotaProjectID string `json:"quota_project_id"`
WorkforcePoolUserProject string `json:"workforce_pool_user_project"`
// External Account Authorized User fields
RevokeURL string `json:"revoke_url"`
// Service account impersonation
SourceCredentials *credentialsFile `json:"source_credentials"`
}
@@ -172,7 +182,11 @@ func (f *credentialsFile) tokenSource(ctx context.Context, params CredentialsPar
cfg.Endpoint.AuthURL = Endpoint.AuthURL
}
if cfg.Endpoint.TokenURL == "" {
cfg.Endpoint.TokenURL = Endpoint.TokenURL
if params.TokenURL != "" {
cfg.Endpoint.TokenURL = params.TokenURL
} else {
cfg.Endpoint.TokenURL = Endpoint.TokenURL
}
}
tok := &oauth2.Token{RefreshToken: f.RefreshToken}
return cfg.TokenSource(ctx, tok), nil
@@ -192,6 +206,19 @@ func (f *credentialsFile) tokenSource(ctx context.Context, params CredentialsPar
WorkforcePoolUserProject: f.WorkforcePoolUserProject,
}
return cfg.TokenSource(ctx)
case externalAccountAuthorizedUserKey:
cfg := &externalaccountauthorizeduser.Config{
Audience: f.Audience,
RefreshToken: f.RefreshToken,
TokenURL: f.TokenURLExternal,
TokenInfoURL: f.TokenInfoURL,
ClientID: f.ClientID,
ClientSecret: f.ClientSecret,
RevokeURL: f.RevokeURL,
QuotaProjectID: f.QuotaProjectID,
Scopes: params.Scopes,
}
return cfg.TokenSource(ctx)
case impersonatedServiceAccount:
if f.ServiceAccountImpersonationURL == "" || f.SourceCredentials == nil {
return nil, errors.New("missing 'source_credentials' field or 'service_account_impersonation_url' in credentials")
@@ -224,7 +251,11 @@ 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 oauth2.ReuseTokenSource(nil, computeSource{account: account, scopes: scope})
return computeTokenSource(account, 0, scope...)
}
func computeTokenSource(account string, earlyExpiry time.Duration, scope ...string) oauth2.TokenSource {
return oauth2.ReuseTokenSourceWithExpiry(nil, computeSource{account: account, scopes: scope}, earlyExpiry)
}
type computeSource struct {

View File

@@ -62,6 +62,13 @@ const (
// The AWS authorization header name for the auto-generated date.
awsDateHeader = "x-amz-date"
// Supported AWS configuration environment variables.
awsAccessKeyId = "AWS_ACCESS_KEY_ID"
awsDefaultRegion = "AWS_DEFAULT_REGION"
awsRegion = "AWS_REGION"
awsSecretAccessKey = "AWS_SECRET_ACCESS_KEY"
awsSessionToken = "AWS_SESSION_TOKEN"
awsTimeFormatLong = "20060102T150405Z"
awsTimeFormatShort = "20060102"
)
@@ -274,16 +281,37 @@ func (cs awsCredentialSource) doRequest(req *http.Request) (*http.Response, erro
return cs.client.Do(req.WithContext(cs.ctx))
}
func canRetrieveRegionFromEnvironment() bool {
// The AWS region can be provided through AWS_REGION or AWS_DEFAULT_REGION. Only one is
// required.
return getenv(awsRegion) != "" || getenv(awsDefaultRegion) != ""
}
func canRetrieveSecurityCredentialFromEnvironment() bool {
// Check if both AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are available.
return getenv(awsAccessKeyId) != "" && getenv(awsSecretAccessKey) != ""
}
func shouldUseMetadataServer() bool {
return !canRetrieveRegionFromEnvironment() || !canRetrieveSecurityCredentialFromEnvironment()
}
func (cs awsCredentialSource) credentialSourceType() string {
return "aws"
}
func (cs awsCredentialSource) subjectToken() (string, error) {
if cs.requestSigner == nil {
awsSessionToken, err := cs.getAWSSessionToken()
if err != nil {
return "", err
}
headers := make(map[string]string)
if awsSessionToken != "" {
headers[awsIMDSv2SessionTokenHeader] = awsSessionToken
if shouldUseMetadataServer() {
awsSessionToken, err := cs.getAWSSessionToken()
if err != nil {
return "", err
}
if awsSessionToken != "" {
headers[awsIMDSv2SessionTokenHeader] = awsSessionToken
}
}
awsSecurityCredentials, err := cs.getSecurityCredentials(headers)
@@ -389,11 +417,11 @@ func (cs *awsCredentialSource) getAWSSessionToken() (string, error) {
}
func (cs *awsCredentialSource) getRegion(headers map[string]string) (string, error) {
if envAwsRegion := getenv("AWS_REGION"); envAwsRegion != "" {
return envAwsRegion, nil
}
if envAwsRegion := getenv("AWS_DEFAULT_REGION"); envAwsRegion != "" {
return envAwsRegion, nil
if canRetrieveRegionFromEnvironment() {
if envAwsRegion := getenv(awsRegion); envAwsRegion != "" {
return envAwsRegion, nil
}
return getenv("AWS_DEFAULT_REGION"), nil
}
if cs.RegionURL == "" {
@@ -434,14 +462,12 @@ func (cs *awsCredentialSource) getRegion(headers map[string]string) (string, err
}
func (cs *awsCredentialSource) getSecurityCredentials(headers map[string]string) (result awsSecurityCredentials, err error) {
if accessKeyID := getenv("AWS_ACCESS_KEY_ID"); accessKeyID != "" {
if secretAccessKey := getenv("AWS_SECRET_ACCESS_KEY"); secretAccessKey != "" {
return awsSecurityCredentials{
AccessKeyID: accessKeyID,
SecretAccessKey: secretAccessKey,
SecurityToken: getenv("AWS_SESSION_TOKEN"),
}, nil
}
if canRetrieveSecurityCredentialFromEnvironment() {
return awsSecurityCredentials{
AccessKeyID: getenv(awsAccessKeyId),
SecretAccessKey: getenv(awsSecretAccessKey),
SecurityToken: getenv(awsSessionToken),
}, nil
}
roleName, err := cs.getMetadataRoleName(headers)

View File

@@ -474,6 +474,38 @@ func createDefaultAwsTestServer() *testAwsServer {
)
}
func createDefaultAwsTestServerWithImdsv2(t *testing.T) *testAwsServer {
validateSessionTokenHeaders := func(r *http.Request) {
if r.URL.Path == "/latest/api/token" {
headerValue := r.Header.Get(awsIMDSv2SessionTtlHeader)
if headerValue != awsIMDSv2SessionTtl {
t.Errorf("%q = \n%q\n want \n%q", awsIMDSv2SessionTtlHeader, headerValue, awsIMDSv2SessionTtl)
}
} else {
headerValue := r.Header.Get(awsIMDSv2SessionTokenHeader)
if headerValue != "sessiontoken" {
t.Errorf("%q = \n%q\n want \n%q", awsIMDSv2SessionTokenHeader, headerValue, "sessiontoken")
}
}
}
return createAwsTestServer(
"/latest/meta-data/iam/security-credentials",
"/latest/meta-data/placement/availability-zone",
"https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15",
"/latest/api/token",
"gcp-aws-role",
"us-east-2b",
map[string]string{
"SecretAccessKey": secretAccessKey,
"AccessKeyId": accessKeyID,
"Token": securityToken,
},
"sessiontoken",
validateSessionTokenHeaders,
)
}
func (server *testAwsServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch p := r.URL.Path; p {
case server.url:
@@ -558,10 +590,12 @@ func TestAWSCredential_BasicRequest(t *testing.T) {
tfc.CredentialSource = server.getCredentialSource(ts.URL)
oldGetenv := getenv
defer func() { getenv = oldGetenv }()
getenv = setEnvironment(map[string]string{})
oldNow := now
defer func() { now = oldNow }()
defer func() {
getenv = oldGetenv
now = oldNow
}()
getenv = setEnvironment(map[string]string{})
now = setTime(defaultTime)
base, err := tfc.parse(context.Background())
@@ -588,45 +622,19 @@ func TestAWSCredential_BasicRequest(t *testing.T) {
}
func TestAWSCredential_IMDSv2(t *testing.T) {
validateSessionTokenHeaders := func(r *http.Request) {
if r.URL.Path == "/latest/api/token" {
headerValue := r.Header.Get(awsIMDSv2SessionTtlHeader)
if headerValue != awsIMDSv2SessionTtl {
t.Errorf("%q = \n%q\n want \n%q", awsIMDSv2SessionTtlHeader, headerValue, awsIMDSv2SessionTtl)
}
} else {
headerValue := r.Header.Get(awsIMDSv2SessionTokenHeader)
if headerValue != "sessiontoken" {
t.Errorf("%q = \n%q\n want \n%q", awsIMDSv2SessionTokenHeader, headerValue, "sessiontoken")
}
}
}
server := createAwsTestServer(
"/latest/meta-data/iam/security-credentials",
"/latest/meta-data/placement/availability-zone",
"https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15",
"/latest/api/token",
"gcp-aws-role",
"us-east-2b",
map[string]string{
"SecretAccessKey": secretAccessKey,
"AccessKeyId": accessKeyID,
"Token": securityToken,
},
"sessiontoken",
validateSessionTokenHeaders,
)
server := createDefaultAwsTestServerWithImdsv2(t)
ts := httptest.NewServer(server)
tfc := testFileConfig
tfc.CredentialSource = server.getCredentialSource(ts.URL)
oldGetenv := getenv
defer func() { getenv = oldGetenv }()
getenv = setEnvironment(map[string]string{})
oldNow := now
defer func() { now = oldNow }()
defer func() {
getenv = oldGetenv
now = oldNow
}()
getenv = setEnvironment(map[string]string{})
now = setTime(defaultTime)
base, err := tfc.parse(context.Background())
@@ -661,10 +669,12 @@ func TestAWSCredential_BasicRequestWithoutSecurityToken(t *testing.T) {
tfc.CredentialSource = server.getCredentialSource(ts.URL)
oldGetenv := getenv
defer func() { getenv = oldGetenv }()
getenv = setEnvironment(map[string]string{})
oldNow := now
defer func() { now = oldNow }()
defer func() {
getenv = oldGetenv
now = oldNow
}()
getenv = setEnvironment(map[string]string{})
now = setTime(defaultTime)
base, err := tfc.parse(context.Background())
@@ -698,14 +708,16 @@ func TestAWSCredential_BasicRequestWithEnv(t *testing.T) {
tfc.CredentialSource = server.getCredentialSource(ts.URL)
oldGetenv := getenv
defer func() { getenv = oldGetenv }()
oldNow := now
defer func() {
getenv = oldGetenv
now = oldNow
}()
getenv = setEnvironment(map[string]string{
"AWS_ACCESS_KEY_ID": "AKIDEXAMPLE",
"AWS_SECRET_ACCESS_KEY": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
"AWS_REGION": "us-west-1",
})
oldNow := now
defer func() { now = oldNow }()
now = setTime(defaultTime)
base, err := tfc.parse(context.Background())
@@ -739,14 +751,16 @@ func TestAWSCredential_BasicRequestWithDefaultEnv(t *testing.T) {
tfc.CredentialSource = server.getCredentialSource(ts.URL)
oldGetenv := getenv
defer func() { getenv = oldGetenv }()
oldNow := now
defer func() {
getenv = oldGetenv
now = oldNow
}()
getenv = setEnvironment(map[string]string{
"AWS_ACCESS_KEY_ID": "AKIDEXAMPLE",
"AWS_SECRET_ACCESS_KEY": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
"AWS_DEFAULT_REGION": "us-west-1",
"AWS_REGION": "us-west-1",
})
oldNow := now
defer func() { now = oldNow }()
now = setTime(defaultTime)
base, err := tfc.parse(context.Background())
@@ -779,15 +793,17 @@ func TestAWSCredential_BasicRequestWithTwoRegions(t *testing.T) {
tfc.CredentialSource = server.getCredentialSource(ts.URL)
oldGetenv := getenv
defer func() { getenv = oldGetenv }()
oldNow := now
defer func() {
getenv = oldGetenv
now = oldNow
}()
getenv = setEnvironment(map[string]string{
"AWS_ACCESS_KEY_ID": "AKIDEXAMPLE",
"AWS_SECRET_ACCESS_KEY": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
"AWS_REGION": "us-west-1",
"AWS_DEFAULT_REGION": "us-east-1",
})
oldNow := now
defer func() { now = oldNow }()
now = setTime(defaultTime)
base, err := tfc.parse(context.Background())
@@ -821,7 +837,9 @@ func TestAWSCredential_RequestWithBadVersion(t *testing.T) {
tfc.CredentialSource.EnvironmentID = "aws3"
oldGetenv := getenv
defer func() { getenv = oldGetenv }()
defer func() {
getenv = oldGetenv
}()
getenv = setEnvironment(map[string]string{})
_, err := tfc.parse(context.Background())
@@ -842,7 +860,9 @@ func TestAWSCredential_RequestWithNoRegionURL(t *testing.T) {
tfc.CredentialSource.RegionURL = ""
oldGetenv := getenv
defer func() { getenv = oldGetenv }()
defer func() {
getenv = oldGetenv
}()
getenv = setEnvironment(map[string]string{})
base, err := tfc.parse(context.Background())
@@ -863,13 +883,16 @@ func TestAWSCredential_RequestWithNoRegionURL(t *testing.T) {
func TestAWSCredential_RequestWithBadRegionURL(t *testing.T) {
server := createDefaultAwsTestServer()
ts := httptest.NewServer(server)
server.WriteRegion = notFound
tfc := testFileConfig
tfc.CredentialSource = server.getCredentialSource(ts.URL)
oldGetenv := getenv
defer func() { getenv = oldGetenv }()
defer func() {
getenv = oldGetenv
}()
getenv = setEnvironment(map[string]string{})
base, err := tfc.parse(context.Background())
@@ -890,6 +913,7 @@ func TestAWSCredential_RequestWithBadRegionURL(t *testing.T) {
func TestAWSCredential_RequestWithMissingCredential(t *testing.T) {
server := createDefaultAwsTestServer()
ts := httptest.NewServer(server)
server.WriteSecurityCredentials = func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("{}"))
}
@@ -898,7 +922,9 @@ func TestAWSCredential_RequestWithMissingCredential(t *testing.T) {
tfc.CredentialSource = server.getCredentialSource(ts.URL)
oldGetenv := getenv
defer func() { getenv = oldGetenv }()
defer func() {
getenv = oldGetenv
}()
getenv = setEnvironment(map[string]string{})
base, err := tfc.parse(context.Background())
@@ -919,6 +945,7 @@ func TestAWSCredential_RequestWithMissingCredential(t *testing.T) {
func TestAWSCredential_RequestWithIncompleteCredential(t *testing.T) {
server := createDefaultAwsTestServer()
ts := httptest.NewServer(server)
server.WriteSecurityCredentials = func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`{"AccessKeyId":"FOOBARBAS"}`))
}
@@ -927,7 +954,9 @@ func TestAWSCredential_RequestWithIncompleteCredential(t *testing.T) {
tfc.CredentialSource = server.getCredentialSource(ts.URL)
oldGetenv := getenv
defer func() { getenv = oldGetenv }()
defer func() {
getenv = oldGetenv
}()
getenv = setEnvironment(map[string]string{})
base, err := tfc.parse(context.Background())
@@ -954,7 +983,9 @@ func TestAWSCredential_RequestWithNoCredentialURL(t *testing.T) {
tfc.CredentialSource.URL = ""
oldGetenv := getenv
defer func() { getenv = oldGetenv }()
defer func() {
getenv = oldGetenv
}()
getenv = setEnvironment(map[string]string{})
base, err := tfc.parse(context.Background())
@@ -981,7 +1012,9 @@ func TestAWSCredential_RequestWithBadCredentialURL(t *testing.T) {
tfc.CredentialSource = server.getCredentialSource(ts.URL)
oldGetenv := getenv
defer func() { getenv = oldGetenv }()
defer func() {
getenv = oldGetenv
}()
getenv = setEnvironment(map[string]string{})
base, err := tfc.parse(context.Background())
@@ -1008,7 +1041,9 @@ func TestAWSCredential_RequestWithBadFinalCredentialURL(t *testing.T) {
tfc.CredentialSource = server.getCredentialSource(ts.URL)
oldGetenv := getenv
defer func() { getenv = oldGetenv }()
defer func() {
getenv = oldGetenv
}()
getenv = setEnvironment(map[string]string{})
base, err := tfc.parse(context.Background())
@@ -1025,3 +1060,194 @@ func TestAWSCredential_RequestWithBadFinalCredentialURL(t *testing.T) {
t.Errorf("subjectToken = %q, want %q", got, want)
}
}
func TestAWSCredential_ShouldNotCallMetadataEndpointWhenCredsAreInEnv(t *testing.T) {
server := createDefaultAwsTestServer()
ts := httptest.NewServer(server)
metadataTs := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Error("Metadata server should not have been called.")
}))
tfc := testFileConfig
tfc.CredentialSource = server.getCredentialSource(ts.URL)
tfc.CredentialSource.IMDSv2SessionTokenURL = metadataTs.URL
oldGetenv := getenv
oldNow := now
defer func() {
getenv = oldGetenv
now = oldNow
}()
getenv = setEnvironment(map[string]string{
"AWS_ACCESS_KEY_ID": "AKIDEXAMPLE",
"AWS_SECRET_ACCESS_KEY": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
"AWS_REGION": "us-west-1",
})
now = setTime(defaultTime)
base, err := tfc.parse(context.Background())
if err != nil {
t.Fatalf("parse() failed %v", err)
}
out, err := base.subjectToken()
if err != nil {
t.Fatalf("retrieveSubjectToken() failed: %v", err)
}
expected := getExpectedSubjectToken(
"https://sts.us-west-1.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15",
"us-west-1",
"AKIDEXAMPLE",
"wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
"",
)
if got, want := out, expected; !reflect.DeepEqual(got, want) {
t.Errorf("subjectToken = \n%q\n want \n%q", got, want)
}
}
func TestAWSCredential_ShouldCallMetadataEndpointWhenNoRegion(t *testing.T) {
server := createDefaultAwsTestServerWithImdsv2(t)
ts := httptest.NewServer(server)
tfc := testFileConfig
tfc.CredentialSource = server.getCredentialSource(ts.URL)
oldGetenv := getenv
oldNow := now
defer func() {
getenv = oldGetenv
now = oldNow
}()
getenv = setEnvironment(map[string]string{
"AWS_ACCESS_KEY_ID": accessKeyID,
"AWS_SECRET_ACCESS_KEY": secretAccessKey,
})
now = setTime(defaultTime)
base, err := tfc.parse(context.Background())
if err != nil {
t.Fatalf("parse() failed %v", err)
}
out, err := base.subjectToken()
if err != nil {
t.Fatalf("retrieveSubjectToken() failed: %v", err)
}
expected := getExpectedSubjectToken(
"https://sts.us-east-2.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15",
"us-east-2",
accessKeyID,
secretAccessKey,
"",
)
if got, want := out, expected; !reflect.DeepEqual(got, want) {
t.Errorf("subjectToken = \n%q\n want \n%q", got, want)
}
}
func TestAWSCredential_ShouldCallMetadataEndpointWhenNoAccessKey(t *testing.T) {
server := createDefaultAwsTestServerWithImdsv2(t)
ts := httptest.NewServer(server)
tfc := testFileConfig
tfc.CredentialSource = server.getCredentialSource(ts.URL)
oldGetenv := getenv
oldNow := now
defer func() {
getenv = oldGetenv
now = oldNow
}()
getenv = setEnvironment(map[string]string{
"AWS_SECRET_ACCESS_KEY": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
"AWS_REGION": "us-west-1",
})
now = setTime(defaultTime)
base, err := tfc.parse(context.Background())
if err != nil {
t.Fatalf("parse() failed %v", err)
}
out, err := base.subjectToken()
if err != nil {
t.Fatalf("retrieveSubjectToken() failed: %v", err)
}
expected := getExpectedSubjectToken(
"https://sts.us-west-1.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15",
"us-west-1",
accessKeyID,
secretAccessKey,
securityToken,
)
if got, want := out, expected; !reflect.DeepEqual(got, want) {
t.Errorf("subjectToken = \n%q\n want \n%q", got, want)
}
}
func TestAWSCredential_ShouldCallMetadataEndpointWhenNoSecretAccessKey(t *testing.T) {
server := createDefaultAwsTestServerWithImdsv2(t)
ts := httptest.NewServer(server)
tfc := testFileConfig
tfc.CredentialSource = server.getCredentialSource(ts.URL)
oldGetenv := getenv
oldNow := now
defer func() {
getenv = oldGetenv
now = oldNow
}()
getenv = setEnvironment(map[string]string{
"AWS_ACCESS_KEY_ID": "AKIDEXAMPLE",
"AWS_REGION": "us-west-1",
})
now = setTime(defaultTime)
base, err := tfc.parse(context.Background())
if err != nil {
t.Fatalf("parse() failed %v", err)
}
out, err := base.subjectToken()
if err != nil {
t.Fatalf("retrieveSubjectToken() failed: %v", err)
}
expected := getExpectedSubjectToken(
"https://sts.us-west-1.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15",
"us-west-1",
accessKeyID,
secretAccessKey,
securityToken,
)
if got, want := out, expected; !reflect.DeepEqual(got, want) {
t.Errorf("subjectToken = \n%q\n want \n%q", got, want)
}
}
func TestAwsCredential_CredentialSourceType(t *testing.T) {
server := createDefaultAwsTestServer()
ts := httptest.NewServer(server)
tfc := testFileConfig
tfc.CredentialSource = server.getCredentialSource(ts.URL)
base, err := tfc.parse(context.Background())
if err != nil {
t.Fatalf("parse() failed %v", err)
}
if got, want := base.credentialSourceType(), "aws"; got != want {
t.Errorf("got %v but want %v", got, want)
}
}

View File

@@ -8,13 +8,12 @@ import (
"context"
"fmt"
"net/http"
"net/url"
"regexp"
"strconv"
"strings"
"time"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google/internal/stsexchange"
)
// now aliases time.Now for testing
@@ -63,72 +62,23 @@ type Config struct {
WorkforcePoolUserProject string
}
// Each element consists of a list of patterns. validateURLs checks for matches
// that include all elements in a given list, in that order.
var (
validTokenURLPatterns = []*regexp.Regexp{
// The complicated part in the middle matches any number of characters that
// aren't period, spaces, or slashes.
regexp.MustCompile(`(?i)^[^\.\s\/\\]+\.sts\.googleapis\.com$`),
regexp.MustCompile(`(?i)^sts\.googleapis\.com$`),
regexp.MustCompile(`(?i)^sts\.[^\.\s\/\\]+\.googleapis\.com$`),
regexp.MustCompile(`(?i)^[^\.\s\/\\]+-sts\.googleapis\.com$`),
regexp.MustCompile(`(?i)^sts-[^\.\s\/\\]+\.p\.googleapis\.com$`),
}
validImpersonateURLPatterns = []*regexp.Regexp{
regexp.MustCompile(`^[^\.\s\/\\]+\.iamcredentials\.googleapis\.com$`),
regexp.MustCompile(`^iamcredentials\.googleapis\.com$`),
regexp.MustCompile(`^iamcredentials\.[^\.\s\/\\]+\.googleapis\.com$`),
regexp.MustCompile(`^[^\.\s\/\\]+-iamcredentials\.googleapis\.com$`),
regexp.MustCompile(`^iamcredentials-[^\.\s\/\\]+\.p\.googleapis\.com$`),
}
validWorkforceAudiencePattern *regexp.Regexp = regexp.MustCompile(`//iam\.googleapis\.com/locations/[^/]+/workforcePools/`)
)
func validateURL(input string, patterns []*regexp.Regexp, scheme string) bool {
parsed, err := url.Parse(input)
if err != nil {
return false
}
if !strings.EqualFold(parsed.Scheme, scheme) {
return false
}
toTest := parsed.Host
for _, pattern := range patterns {
if pattern.MatchString(toTest) {
return true
}
}
return false
}
func validateWorkforceAudience(input string) bool {
return validWorkforceAudiencePattern.MatchString(input)
}
// TokenSource Returns an external account TokenSource struct. This is to be called by package google to construct a google.Credentials.
func (c *Config) TokenSource(ctx context.Context) (oauth2.TokenSource, error) {
return c.tokenSource(ctx, validTokenURLPatterns, validImpersonateURLPatterns, "https")
return c.tokenSource(ctx, "https")
}
// tokenSource is a private function that's directly called by some of the tests,
// because the unit test URLs are mocked, and would otherwise fail the
// validity check.
func (c *Config) tokenSource(ctx context.Context, tokenURLValidPats []*regexp.Regexp, impersonateURLValidPats []*regexp.Regexp, scheme string) (oauth2.TokenSource, error) {
valid := validateURL(c.TokenURL, tokenURLValidPats, scheme)
if !valid {
return nil, fmt.Errorf("oauth2/google: invalid TokenURL provided while constructing tokenSource")
}
if c.ServiceAccountImpersonationURL != "" {
valid := validateURL(c.ServiceAccountImpersonationURL, impersonateURLValidPats, scheme)
if !valid {
return nil, fmt.Errorf("oauth2/google: invalid ServiceAccountImpersonationURL provided while constructing tokenSource")
}
}
func (c *Config) tokenSource(ctx context.Context, scheme string) (oauth2.TokenSource, error) {
if c.WorkforcePoolUserProject != "" {
valid := validateWorkforceAudience(c.Audience)
if !valid {
@@ -226,6 +176,7 @@ func (c *Config) parse(ctx context.Context) (baseCredentialSource, error) {
}
type baseCredentialSource interface {
credentialSourceType() string
subjectToken() (string, error)
}
@@ -235,6 +186,15 @@ type tokenSource struct {
conf *Config
}
func getMetricsHeaderValue(conf *Config, credSource baseCredentialSource) string {
return fmt.Sprintf("gl-go/%s auth/%s google-byoid-sdk source/%s sa-impersonation/%t config-lifetime/%t",
goVersion(),
"unknown",
credSource.credentialSourceType(),
conf.ServiceAccountImpersonationURL != "",
conf.ServiceAccountImpersonationLifetimeSeconds != 0)
}
// Token allows tokenSource to conform to the oauth2.TokenSource interface.
func (ts tokenSource) Token() (*oauth2.Token, error) {
conf := ts.conf
@@ -248,7 +208,7 @@ func (ts tokenSource) Token() (*oauth2.Token, error) {
if err != nil {
return nil, err
}
stsRequest := stsTokenExchangeRequest{
stsRequest := stsexchange.TokenExchangeRequest{
GrantType: "urn:ietf:params:oauth:grant-type:token-exchange",
Audience: conf.Audience,
Scope: conf.Scopes,
@@ -258,7 +218,8 @@ func (ts tokenSource) Token() (*oauth2.Token, error) {
}
header := make(http.Header)
header.Add("Content-Type", "application/x-www-form-urlencoded")
clientAuth := clientAuthentication{
header.Add("x-goog-api-client", getMetricsHeaderValue(conf, credSource))
clientAuth := stsexchange.ClientAuthentication{
AuthStyle: oauth2.AuthStyleInHeader,
ClientID: conf.ClientID,
ClientSecret: conf.ClientSecret,
@@ -271,7 +232,7 @@ func (ts tokenSource) Token() (*oauth2.Token, error) {
"userProject": conf.WorkforcePoolUserProject,
}
}
stsResp, err := exchangeToken(ts.ctx, conf.TokenURL, &stsRequest, clientAuth, header, options)
stsResp, err := stsexchange.ExchangeToken(ts.ctx, conf.TokenURL, &stsRequest, clientAuth, header, options)
if err != nil {
return nil, err
}

View File

@@ -6,10 +6,10 @@ package externalaccount
import (
"context"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
@@ -52,6 +52,7 @@ type testExchangeTokenServer struct {
url string
authorization string
contentType string
metricsHeader string
body string
response string
}
@@ -69,6 +70,10 @@ func run(t *testing.T, config *Config, tets *testExchangeTokenServer) (*oauth2.T
if got, want := headerContentType, tets.contentType; got != want {
t.Errorf("got %v but want %v", got, want)
}
headerMetrics := r.Header.Get("x-goog-api-client")
if got, want := headerMetrics, tets.metricsHeader; got != want {
t.Errorf("got %v but want %v", got, want)
}
body, err := ioutil.ReadAll(r.Body)
if err != nil {
t.Fatalf("Failed reading request body: %s.", err)
@@ -107,6 +112,10 @@ func validateToken(t *testing.T, tok *oauth2.Token) {
}
}
func getExpectedMetricsHeader(source string, saImpersonation bool, configLifetime bool) string {
return fmt.Sprintf("gl-go/%s auth/unknown google-byoid-sdk source/%s sa-impersonation/%t config-lifetime/%t", goVersion(), source, saImpersonation, configLifetime)
}
func TestToken(t *testing.T) {
config := Config{
Audience: "32555940559.apps.googleusercontent.com",
@@ -121,6 +130,7 @@ func TestToken(t *testing.T) {
url: "/",
authorization: "Basic cmJyZ25vZ25yaG9uZ28zYmk0Z2I5Z2hnOWc6bm90c29zZWNyZXQ=",
contentType: "application/x-www-form-urlencoded",
metricsHeader: getExpectedMetricsHeader("file", false, false),
body: baseCredsRequestBody,
response: baseCredsResponseBody,
}
@@ -148,6 +158,7 @@ func TestWorkforcePoolTokenWithClientID(t *testing.T) {
url: "/",
authorization: "Basic cmJyZ25vZ25yaG9uZ28zYmk0Z2I5Z2hnOWc6bm90c29zZWNyZXQ=",
contentType: "application/x-www-form-urlencoded",
metricsHeader: getExpectedMetricsHeader("file", false, false),
body: workforcePoolRequestBodyWithClientId,
response: baseCredsResponseBody,
}
@@ -174,6 +185,7 @@ func TestWorkforcePoolTokenWithoutClientID(t *testing.T) {
url: "/",
authorization: "",
contentType: "application/x-www-form-urlencoded",
metricsHeader: getExpectedMetricsHeader("file", false, false),
body: workforcePoolRequestBodyWithoutClientId,
response: baseCredsResponseBody,
}
@@ -208,140 +220,6 @@ func TestNonworkforceWithWorkforcePoolUserProject(t *testing.T) {
}
}
func TestValidateURLTokenURL(t *testing.T) {
var urlValidityTests = []struct {
tokURL string
expectSuccess bool
}{
{"https://east.sts.googleapis.com", true},
{"https://sts.googleapis.com", true},
{"https://sts.asfeasfesef.googleapis.com", true},
{"https://us-east-1-sts.googleapis.com", true},
{"https://sts.googleapis.com/your/path/here", true},
{"https://.sts.googleapis.com", false},
{"https://badsts.googleapis.com", false},
{"https://sts.asfe.asfesef.googleapis.com", false},
{"https://sts..googleapis.com", false},
{"https://-sts.googleapis.com", false},
{"https://us-ea.st-1-sts.googleapis.com", false},
{"https://sts.googleapis.com.evil.com/whatever/path", false},
{"https://us-eas\\t-1.sts.googleapis.com", false},
{"https:/us-ea/st-1.sts.googleapis.com", false},
{"https:/us-east 1.sts.googleapis.com", false},
{"https://", false},
{"http://us-east-1.sts.googleapis.com", false},
{"https://us-east-1.sts.googleapis.comevil.com", false},
{"https://sts-xyz.p.googleapis.com", true},
{"https://sts.pgoogleapis.com", false},
{"https://p.googleapis.com", false},
{"https://sts.p.com", false},
{"http://sts.p.googleapis.com", false},
{"https://xyz-sts.p.googleapis.com", false},
{"https://sts-xyz.123.p.googleapis.com", false},
{"https://sts-xyz.p1.googleapis.com", false},
{"https://sts-xyz.p.foo.com", false},
{"https://sts-xyz.p.foo.googleapis.com", false},
}
ctx := context.Background()
for _, tt := range urlValidityTests {
t.Run(" "+tt.tokURL, func(t *testing.T) { // We prepend a space ahead of the test input when outputting for sake of readability.
config := testConfig
config.TokenURL = tt.tokURL
_, err := config.TokenSource(ctx)
if tt.expectSuccess && err != nil {
t.Errorf("got %v but want nil", err)
} else if !tt.expectSuccess && err == nil {
t.Errorf("got nil but expected an error")
}
})
}
for _, el := range urlValidityTests {
el.tokURL = strings.ToUpper(el.tokURL)
}
for _, tt := range urlValidityTests {
t.Run(" "+tt.tokURL, func(t *testing.T) { // We prepend a space ahead of the test input when outputting for sake of readability.
config := testConfig
config.TokenURL = tt.tokURL
_, err := config.TokenSource(ctx)
if tt.expectSuccess && err != nil {
t.Errorf("got %v but want nil", err)
} else if !tt.expectSuccess && err == nil {
t.Errorf("got nil but expected an error")
}
})
}
}
func TestValidateURLImpersonateURL(t *testing.T) {
var urlValidityTests = []struct {
impURL string
expectSuccess bool
}{
{"https://east.iamcredentials.googleapis.com", true},
{"https://iamcredentials.googleapis.com", true},
{"https://iamcredentials.asfeasfesef.googleapis.com", true},
{"https://us-east-1-iamcredentials.googleapis.com", true},
{"https://iamcredentials.googleapis.com/your/path/here", true},
{"https://.iamcredentials.googleapis.com", false},
{"https://badiamcredentials.googleapis.com", false},
{"https://iamcredentials.asfe.asfesef.googleapis.com", false},
{"https://iamcredentials..googleapis.com", false},
{"https://-iamcredentials.googleapis.com", false},
{"https://us-ea.st-1-iamcredentials.googleapis.com", false},
{"https://iamcredentials.googleapis.com.evil.com/whatever/path", false},
{"https://us-eas\\t-1.iamcredentials.googleapis.com", false},
{"https:/us-ea/st-1.iamcredentials.googleapis.com", false},
{"https:/us-east 1.iamcredentials.googleapis.com", false},
{"https://", false},
{"http://us-east-1.iamcredentials.googleapis.com", false},
{"https://us-east-1.iamcredentials.googleapis.comevil.com", false},
{"https://iamcredentials-xyz.p.googleapis.com", true},
{"https://iamcredentials.pgoogleapis.com", false},
{"https://p.googleapis.com", false},
{"https://iamcredentials.p.com", false},
{"http://iamcredentials.p.googleapis.com", false},
{"https://xyz-iamcredentials.p.googleapis.com", false},
{"https://iamcredentials-xyz.123.p.googleapis.com", false},
{"https://iamcredentials-xyz.p1.googleapis.com", false},
{"https://iamcredentials-xyz.p.foo.com", false},
{"https://iamcredentials-xyz.p.foo.googleapis.com", false},
}
ctx := context.Background()
for _, tt := range urlValidityTests {
t.Run(" "+tt.impURL, 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
config.ServiceAccountImpersonationURL = tt.impURL
_, err := config.TokenSource(ctx)
if tt.expectSuccess && err != nil {
t.Errorf("got %v but want nil", err)
} else if !tt.expectSuccess && err == nil {
t.Errorf("got nil but expected an error")
}
})
}
for _, el := range urlValidityTests {
el.impURL = strings.ToUpper(el.impURL)
}
for _, tt := range urlValidityTests {
t.Run(" "+tt.impURL, 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
config.ServiceAccountImpersonationURL = tt.impURL
_, err := config.TokenSource(ctx)
if tt.expectSuccess && err != nil {
t.Errorf("got %v but want nil", err)
} else if !tt.expectSuccess && err == nil {
t.Errorf("got nil but expected an error")
}
})
}
}
func TestWorkforcePoolCreation(t *testing.T) {
var audienceValidatyTests = []struct {
audience string

View File

@@ -233,6 +233,10 @@ func (cs executableCredentialSource) parseSubjectTokenFromSource(response []byte
return "", tokenTypeError(source)
}
func (cs executableCredentialSource) credentialSourceType() string {
return "executable"
}
func (cs executableCredentialSource) subjectToken() (string, error) {
if token, err := cs.getTokenFromOutputFile(); token != "" || err != nil {
return token, err

View File

@@ -150,6 +150,9 @@ func TestCreateExecutableCredential(t *testing.T) {
if ecs.Timeout != tt.expectedTimeout {
t.Errorf("ecs.Timeout got %v but want %v", ecs.Timeout, tt.expectedTimeout)
}
if ecs.credentialSourceType() != "executable" {
t.Errorf("ecs.CredentialSourceType() got %s but want executable", ecs.credentialSourceType())
}
}
})
}

View File

@@ -19,6 +19,10 @@ type fileCredentialSource struct {
Format format
}
func (cs fileCredentialSource) credentialSourceType() string {
return "file"
}
func (cs fileCredentialSource) subjectToken() (string, error) {
tokenFile, err := os.Open(cs.File)
if err != nil {

View File

@@ -68,6 +68,9 @@ func TestRetrieveFileSubjectToken(t *testing.T) {
t.Errorf("got %v but want %v", out, test.want)
}
if got, want := base.credentialSourceType(), "file"; got != want {
t.Errorf("got %v but want %v", got, want)
}
})
}
}

View File

@@ -0,0 +1,64 @@
// Copyright 2023 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.
package externalaccount
import (
"runtime"
"strings"
"unicode"
)
var (
// version is a package internal global variable for testing purposes.
version = runtime.Version
)
// versionUnknown is only used when the runtime version cannot be determined.
const versionUnknown = "UNKNOWN"
// goVersion returns a Go runtime version derived from the runtime environment
// that is modified to be suitable for reporting in a header, meaning it has no
// whitespace. If it is unable to determine the Go runtime version, it returns
// versionUnknown.
func goVersion() string {
const develPrefix = "devel +"
s := version()
if strings.HasPrefix(s, develPrefix) {
s = s[len(develPrefix):]
if p := strings.IndexFunc(s, unicode.IsSpace); p >= 0 {
s = s[:p]
}
return s
} else if p := strings.IndexFunc(s, unicode.IsSpace); p >= 0 {
s = s[:p]
}
notSemverRune := func(r rune) bool {
return !strings.ContainsRune("0123456789.", r)
}
if strings.HasPrefix(s, "go1") {
s = s[2:]
var prerelease string
if p := strings.IndexFunc(s, notSemverRune); p >= 0 {
s, prerelease = s[:p], s[p:]
}
if strings.HasSuffix(s, ".") {
s += "0"
} else if strings.Count(s, ".") < 2 {
s += ".0"
}
if prerelease != "" {
// Some release candidates already have a dash in them.
if !strings.HasPrefix(prerelease, "-") {
prerelease = "-" + prerelease
}
s += prerelease
}
return s
}
return "UNKNOWN"
}

View File

@@ -0,0 +1,48 @@
// Copyright 2023 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.
package externalaccount
import (
"runtime"
"testing"
"github.com/google/go-cmp/cmp"
)
func TestGoVersion(t *testing.T) {
testVersion := func(v string) func() string {
return func() string {
return v
}
}
for _, tst := range []struct {
v func() string
want string
}{
{
testVersion("go1.19"),
"1.19.0",
},
{
testVersion("go1.21-20230317-RC01"),
"1.21.0-20230317-RC01",
},
{
testVersion("devel +abc1234"),
"abc1234",
},
{
testVersion("this should be unknown"),
versionUnknown,
},
} {
version = tst.v
got := goVersion()
if diff := cmp.Diff(got, tst.want); diff != "" {
t.Errorf("got(-),want(+):\n%s", diff)
}
}
version = runtime.Version
}

View File

@@ -9,7 +9,6 @@ import (
"io/ioutil"
"net/http"
"net/http/httptest"
"regexp"
"testing"
)
@@ -43,7 +42,7 @@ func createImpersonationServer(urlWanted, authWanted, bodyWanted, response strin
}))
}
func createTargetServer(t *testing.T) *httptest.Server {
func createTargetServer(metricsHeaderWanted string, t *testing.T) *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if got, want := r.URL.String(), "/"; got != want {
t.Errorf("URL.String(): got %v but want %v", got, want)
@@ -56,6 +55,10 @@ func createTargetServer(t *testing.T) *httptest.Server {
if got, want := headerContentType, "application/x-www-form-urlencoded"; got != want {
t.Errorf("got %v but want %v", got, want)
}
headerMetrics := r.Header.Get("x-goog-api-client")
if got, want := headerMetrics, metricsHeaderWanted; got != want {
t.Errorf("got %v but want %v", got, want)
}
body, err := ioutil.ReadAll(r.Body)
if err != nil {
t.Fatalf("Failed reading request body: %v.", err)
@@ -72,6 +75,7 @@ var impersonationTests = []struct {
name string
config Config
expectedImpersonationBody string
expectedMetricsHeader string
}{
{
name: "Base Impersonation",
@@ -85,6 +89,7 @@ var impersonationTests = []struct {
Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"},
},
expectedImpersonationBody: "{\"lifetime\":\"3600s\",\"scope\":[\"https://www.googleapis.com/auth/devstorage.full_control\"]}",
expectedMetricsHeader: getExpectedMetricsHeader("file", true, false),
},
{
name: "With TokenLifetime Set",
@@ -99,6 +104,7 @@ var impersonationTests = []struct {
ServiceAccountImpersonationLifetimeSeconds: 10000,
},
expectedImpersonationBody: "{\"lifetime\":\"10000s\",\"scope\":[\"https://www.googleapis.com/auth/devstorage.full_control\"]}",
expectedMetricsHeader: getExpectedMetricsHeader("file", true, true),
},
}
@@ -110,12 +116,11 @@ func TestImpersonation(t *testing.T) {
defer impersonateServer.Close()
testImpersonateConfig.ServiceAccountImpersonationURL = impersonateServer.URL
targetServer := createTargetServer(t)
targetServer := createTargetServer(tt.expectedMetricsHeader, t)
defer targetServer.Close()
testImpersonateConfig.TokenURL = targetServer.URL
allURLs := regexp.MustCompile(".+")
ourTS, err := testImpersonateConfig.tokenSource(context.Background(), []*regexp.Regexp{allURLs}, []*regexp.Regexp{allURLs}, "http")
ourTS, err := testImpersonateConfig.tokenSource(context.Background(), "http")
if err != nil {
t.Fatalf("Failed to create TokenSource: %v", err)
}

View File

@@ -23,6 +23,10 @@ type urlCredentialSource struct {
ctx context.Context
}
func (cs urlCredentialSource) credentialSourceType() string {
return "url"
}
func (cs urlCredentialSource) subjectToken() (string, error) {
client := oauth2.NewClient(cs.ctx, nil)
req, err := http.NewRequest("GET", cs.URL, nil)

View File

@@ -111,3 +111,21 @@ func TestRetrieveURLSubjectToken_JSON(t *testing.T) {
t.Errorf("got %v but want %v", out, myURLToken)
}
}
func TestURLCredential_CredentialSourceType(t *testing.T) {
cs := CredentialSource{
URL: "http://example.com",
Format: format{Type: fileTypeText},
}
tfc := testFileConfig
tfc.CredentialSource = cs
base, err := tfc.parse(context.Background())
if err != nil {
t.Fatalf("parse() failed %v", err)
}
if got, want := base.credentialSourceType(), "url"; got != want {
t.Errorf("got %v but want %v", got, want)
}
}

View File

@@ -0,0 +1,114 @@
// Copyright 2023 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.
package externalaccountauthorizeduser
import (
"context"
"errors"
"time"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google/internal/stsexchange"
)
// now aliases time.Now for testing.
var now = func() time.Time {
return time.Now().UTC()
}
var tokenValid = func(token oauth2.Token) bool {
return token.Valid()
}
type Config struct {
// Audience is the Secure Token Service (STS) audience which contains the resource name for the workforce pool and
// the provider identifier in that pool.
Audience string
// RefreshToken is the optional OAuth 2.0 refresh token. If specified, credentials can be refreshed.
RefreshToken string
// TokenURL is the optional STS token exchange endpoint for refresh. Must be specified for refresh, can be left as
// None if the token can not be refreshed.
TokenURL string
// TokenInfoURL is the optional STS endpoint URL for token introspection.
TokenInfoURL string
// ClientID is only required in conjunction with ClientSecret, as described above.
ClientID string
// ClientSecret is currently only required if token_info endpoint also needs to be called with the generated GCP
// access token. When provided, STS will be called with additional basic authentication using client_id as username
// and client_secret as password.
ClientSecret string
// Token is the OAuth2.0 access token. Can be nil if refresh information is provided.
Token string
// Expiry is the optional expiration datetime of the OAuth 2.0 access token.
Expiry time.Time
// RevokeURL is the optional STS endpoint URL for revoking tokens.
RevokeURL string
// QuotaProjectID is the optional project ID used for quota and billing. This project may be different from the
// project used to create the credentials.
QuotaProjectID string
Scopes []string
}
func (c *Config) canRefresh() bool {
return c.ClientID != "" && c.ClientSecret != "" && c.RefreshToken != "" && c.TokenURL != ""
}
func (c *Config) TokenSource(ctx context.Context) (oauth2.TokenSource, error) {
var token oauth2.Token
if c.Token != "" && !c.Expiry.IsZero() {
token = oauth2.Token{
AccessToken: c.Token,
Expiry: c.Expiry,
TokenType: "Bearer",
}
}
if !tokenValid(token) && !c.canRefresh() {
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`).")
}
ts := tokenSource{
ctx: ctx,
conf: c,
}
return oauth2.ReuseTokenSource(&token, ts), nil
}
type tokenSource struct {
ctx context.Context
conf *Config
}
func (ts tokenSource) Token() (*oauth2.Token, error) {
conf := ts.conf
if !conf.canRefresh() {
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.")
}
clientAuth := stsexchange.ClientAuthentication{
AuthStyle: oauth2.AuthStyleInHeader,
ClientID: conf.ClientID,
ClientSecret: conf.ClientSecret,
}
stsResponse, err := stsexchange.RefreshAccessToken(ts.ctx, conf.TokenURL, conf.RefreshToken, clientAuth, nil)
if err != nil {
return nil, err
}
if stsResponse.ExpiresIn < 0 {
return nil, errors.New("oauth2/google: got invalid expiry from security token service")
}
if stsResponse.RefreshToken != "" {
conf.RefreshToken = stsResponse.RefreshToken
}
token := &oauth2.Token{
AccessToken: stsResponse.AccessToken,
Expiry: now().Add(time.Duration(stsResponse.ExpiresIn) * time.Second),
TokenType: "Bearer",
}
return token, nil
}

View File

@@ -0,0 +1,259 @@
// Copyright 2023 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.
package externalaccountauthorizeduser
import (
"context"
"encoding/json"
"errors"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
"time"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google/internal/stsexchange"
)
const expiryDelta = 10 * time.Second
var (
expiry = time.Unix(234852, 0)
testNow = func() time.Time { return expiry }
testValid = func(t oauth2.Token) bool {
return t.AccessToken != "" && !t.Expiry.Round(0).Add(-expiryDelta).Before(testNow())
}
)
type testRefreshTokenServer struct {
URL string
Authorization string
ContentType string
Body string
ResponsePayload *stsexchange.Response
Response string
server *httptest.Server
}
func TestExernalAccountAuthorizedUser_JustToken(t *testing.T) {
config := &Config{
Token: "AAAAAAA",
Expiry: now().Add(time.Hour),
}
ts, err := config.TokenSource(context.Background())
if err != nil {
t.Fatalf("Error getting token source: %v", err)
}
token, err := ts.Token()
if err != nil {
t.Fatalf("Error retrieving Token: %v", err)
}
if got, want := token.AccessToken, "AAAAAAA"; got != want {
t.Fatalf("Unexpected access token, got %v, want %v", got, want)
}
}
func TestExernalAccountAuthorizedUser_TokenRefreshWithRefreshTokenInRespondse(t *testing.T) {
server := &testRefreshTokenServer{
URL: "/",
Authorization: "Basic Q0xJRU5UX0lEOkNMSUVOVF9TRUNSRVQ=",
ContentType: "application/x-www-form-urlencoded",
Body: "grant_type=refresh_token&refresh_token=BBBBBBBBB",
ResponsePayload: &stsexchange.Response{
ExpiresIn: 3600,
AccessToken: "AAAAAAA",
RefreshToken: "CCCCCCC",
},
}
url, err := server.run(t)
if err != nil {
t.Fatalf("Error starting server")
}
defer server.close(t)
config := &Config{
RefreshToken: "BBBBBBBBB",
TokenURL: url,
ClientID: "CLIENT_ID",
ClientSecret: "CLIENT_SECRET",
}
ts, err := config.TokenSource(context.Background())
if err != nil {
t.Fatalf("Error getting token source: %v", err)
}
token, err := ts.Token()
if err != nil {
t.Fatalf("Error retrieving Token: %v", err)
}
if got, want := token.AccessToken, "AAAAAAA"; got != want {
t.Fatalf("Unexpected access token, got %v, want %v", got, want)
}
if config.RefreshToken != "CCCCCCC" {
t.Fatalf("Refresh token not updated")
}
}
func TestExernalAccountAuthorizedUser_MinimumFieldsRequiredForRefresh(t *testing.T) {
server := &testRefreshTokenServer{
URL: "/",
Authorization: "Basic Q0xJRU5UX0lEOkNMSUVOVF9TRUNSRVQ=",
ContentType: "application/x-www-form-urlencoded",
Body: "grant_type=refresh_token&refresh_token=BBBBBBBBB",
ResponsePayload: &stsexchange.Response{
ExpiresIn: 3600,
AccessToken: "AAAAAAA",
},
}
url, err := server.run(t)
if err != nil {
t.Fatalf("Error starting server")
}
defer server.close(t)
config := &Config{
RefreshToken: "BBBBBBBBB",
TokenURL: url,
ClientID: "CLIENT_ID",
ClientSecret: "CLIENT_SECRET",
}
ts, err := config.TokenSource(context.Background())
if err != nil {
t.Fatalf("Error getting token source: %v", err)
}
token, err := ts.Token()
if err != nil {
t.Fatalf("Error retrieving Token: %v", err)
}
if got, want := token.AccessToken, "AAAAAAA"; got != want {
t.Fatalf("Unexpected access token, got %v, want %v", got, want)
}
}
func TestExternalAccountAuthorizedUser_MissingRefreshFields(t *testing.T) {
server := &testRefreshTokenServer{
URL: "/",
Authorization: "Basic Q0xJRU5UX0lEOkNMSUVOVF9TRUNSRVQ=",
ContentType: "application/x-www-form-urlencoded",
Body: "grant_type=refresh_token&refresh_token=BBBBBBBBB",
ResponsePayload: &stsexchange.Response{
ExpiresIn: 3600,
AccessToken: "AAAAAAA",
},
}
url, err := server.run(t)
if err != nil {
t.Fatalf("Error starting server")
}
defer server.close(t)
testCases := []struct {
name string
config Config
}{
{
name: "empty config",
config: Config{},
},
{
name: "missing refresh token",
config: Config{
TokenURL: url,
ClientID: "CLIENT_ID",
ClientSecret: "CLIENT_SECRET",
},
},
{
name: "missing token url",
config: Config{
RefreshToken: "BBBBBBBBB",
ClientID: "CLIENT_ID",
ClientSecret: "CLIENT_SECRET",
},
},
{
name: "missing client id",
config: Config{
RefreshToken: "BBBBBBBBB",
TokenURL: url,
ClientSecret: "CLIENT_SECRET",
},
},
{
name: "missing client secrect",
config: Config{
RefreshToken: "BBBBBBBBB",
TokenURL: url,
ClientID: "CLIENT_ID",
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
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`)."
_, err := tc.config.TokenSource((context.Background()))
if err == nil {
t.Fatalf("Expected error, but received none")
}
if got := err.Error(); got != expectErrMsg {
t.Fatalf("Unexpected error, got %v, want %v", got, expectErrMsg)
}
})
}
}
func (trts *testRefreshTokenServer) run(t *testing.T) (string, error) {
t.Helper()
if trts.server != nil {
return "", errors.New("Server is already running")
}
trts.server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if got, want := r.URL.String(), trts.URL; got != want {
t.Errorf("URL.String(): got %v but want %v", got, want)
}
headerAuth := r.Header.Get("Authorization")
if got, want := headerAuth, trts.Authorization; got != want {
t.Errorf("got %v but want %v", got, want)
}
headerContentType := r.Header.Get("Content-Type")
if got, want := headerContentType, trts.ContentType; got != want {
t.Errorf("got %v but want %v", got, want)
}
body, err := ioutil.ReadAll(r.Body)
if err != nil {
t.Fatalf("Failed reading request body: %s.", err)
}
if got, want := string(body), trts.Body; got != want {
t.Errorf("Unexpected exchange payload: got %v but want %v", got, want)
}
w.Header().Set("Content-Type", "application/json")
if trts.ResponsePayload != nil {
content, err := json.Marshal(trts.ResponsePayload)
if err != nil {
t.Fatalf("unable to marshall response JSON")
}
w.Write(content)
} else {
w.Write([]byte(trts.Response))
}
}))
return trts.server.URL, nil
}
func (trts *testRefreshTokenServer) close(t *testing.T) error {
t.Helper()
if trts.server == nil {
return errors.New("No server is running")
}
trts.server.Close()
trts.server = nil
return nil
}

View File

@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package externalaccount
package stsexchange
import (
"encoding/base64"
@@ -12,8 +12,8 @@ import (
"golang.org/x/oauth2"
)
// clientAuthentication represents an OAuth client ID and secret and the mechanism for passing these credentials as stated in rfc6749#2.3.1.
type clientAuthentication struct {
// ClientAuthentication represents an OAuth client ID and secret and the mechanism for passing these credentials as stated in rfc6749#2.3.1.
type ClientAuthentication struct {
// AuthStyle can be either basic or request-body
AuthStyle oauth2.AuthStyle
ClientID string
@@ -23,7 +23,7 @@ type clientAuthentication struct {
// InjectAuthentication is used to add authentication to a Secure Token Service exchange
// request. It modifies either the passed url.Values or http.Header depending on the desired
// authentication format.
func (c *clientAuthentication) InjectAuthentication(values url.Values, headers http.Header) {
func (c *ClientAuthentication) InjectAuthentication(values url.Values, headers http.Header) {
if c.ClientID == "" || c.ClientSecret == "" || values == nil || headers == nil {
return
}

View File

@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package externalaccount
package stsexchange
import (
"net/http"
@@ -38,7 +38,7 @@ func TestClientAuthentication_InjectHeaderAuthentication(t *testing.T) {
"Content-Type": ContentType,
}
headerAuthentication := clientAuthentication{
headerAuthentication := ClientAuthentication{
AuthStyle: oauth2.AuthStyleInHeader,
ClientID: clientID,
ClientSecret: clientSecret,
@@ -80,7 +80,7 @@ func TestClientAuthentication_ParamsAuthentication(t *testing.T) {
headerP := http.Header{
"Content-Type": ContentType,
}
paramsAuthentication := clientAuthentication{
paramsAuthentication := ClientAuthentication{
AuthStyle: oauth2.AuthStyleInParams,
ClientID: clientID,
ClientSecret: clientSecret,

View File

@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package externalaccount
package stsexchange
import (
"context"
@@ -18,14 +18,17 @@ import (
"golang.org/x/oauth2"
)
// exchangeToken performs an oauth2 token exchange with the provided endpoint.
func defaultHeader() http.Header {
header := make(http.Header)
header.Add("Content-Type", "application/x-www-form-urlencoded")
return header
}
// ExchangeToken performs an oauth2 token exchange with the provided endpoint.
// The first 4 fields are all mandatory. headers can be used to pass additional
// headers beyond the bare minimum required by the token exchange. options can
// be used to pass additional JSON-structured options to the remote server.
func exchangeToken(ctx context.Context, endpoint string, request *stsTokenExchangeRequest, authentication clientAuthentication, headers http.Header, options map[string]interface{}) (*stsTokenExchangeResponse, error) {
client := oauth2.NewClient(ctx, nil)
func ExchangeToken(ctx context.Context, endpoint string, request *TokenExchangeRequest, authentication ClientAuthentication, headers http.Header, options map[string]interface{}) (*Response, error) {
data := url.Values{}
data.Set("audience", request.Audience)
data.Set("grant_type", "urn:ietf:params:oauth:grant-type:token-exchange")
@@ -41,13 +44,28 @@ func exchangeToken(ctx context.Context, endpoint string, request *stsTokenExchan
data.Set("options", string(opts))
}
return makeRequest(ctx, endpoint, data, authentication, headers)
}
func RefreshAccessToken(ctx context.Context, endpoint string, refreshToken string, authentication ClientAuthentication, headers http.Header) (*Response, error) {
data := url.Values{}
data.Set("grant_type", "refresh_token")
data.Set("refresh_token", refreshToken)
return makeRequest(ctx, endpoint, data, authentication, headers)
}
func makeRequest(ctx context.Context, endpoint string, data url.Values, authentication ClientAuthentication, headers http.Header) (*Response, error) {
if headers == nil {
headers = defaultHeader()
}
client := oauth2.NewClient(ctx, nil)
authentication.InjectAuthentication(data, headers)
encodedData := data.Encode()
req, err := http.NewRequest("POST", endpoint, strings.NewReader(encodedData))
if err != nil {
return nil, fmt.Errorf("oauth2/google: failed to properly build http request: %v", err)
}
req = req.WithContext(ctx)
for key, list := range headers {
@@ -71,7 +89,7 @@ func exchangeToken(ctx context.Context, endpoint string, request *stsTokenExchan
if c := resp.StatusCode; c < 200 || c > 299 {
return nil, fmt.Errorf("oauth2/google: status code %d: %s", c, body)
}
var stsResp stsTokenExchangeResponse
var stsResp Response
err = json.Unmarshal(body, &stsResp)
if err != nil {
return nil, fmt.Errorf("oauth2/google: failed to unmarshal response body from Secure Token Server: %v", err)
@@ -81,8 +99,8 @@ func exchangeToken(ctx context.Context, endpoint string, request *stsTokenExchan
return &stsResp, nil
}
// stsTokenExchangeRequest contains fields necessary to make an oauth2 token exchange.
type stsTokenExchangeRequest struct {
// TokenExchangeRequest contains fields necessary to make an oauth2 token exchange.
type TokenExchangeRequest struct {
ActingParty struct {
ActorToken string
ActorTokenType string
@@ -96,8 +114,8 @@ type stsTokenExchangeRequest struct {
SubjectTokenType string
}
// stsTokenExchangeResponse is used to decode the remote server response during an oauth2 token exchange.
type stsTokenExchangeResponse struct {
// Response is used to decode the remote server response during an oauth2 token exchange.
type Response struct {
AccessToken string `json:"access_token"`
IssuedTokenType string `json:"issued_token_type"`
TokenType string `json:"token_type"`

View File

@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package externalaccount
package stsexchange
import (
"context"
@@ -16,13 +16,13 @@ import (
"golang.org/x/oauth2"
)
var auth = clientAuthentication{
var auth = ClientAuthentication{
AuthStyle: oauth2.AuthStyleInHeader,
ClientID: clientID,
ClientSecret: clientSecret,
}
var tokenRequest = stsTokenExchangeRequest{
var exchangeTokenRequest = TokenExchangeRequest{
ActingParty: struct {
ActorToken string
ActorTokenType string
@@ -36,9 +36,9 @@ var tokenRequest = stsTokenExchangeRequest{
SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt",
}
var requestbody = "audience=32555940559.apps.googleusercontent.com&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&requested_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fdevstorage.full_control&subject_token=Sample.Subject.Token&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Ajwt"
var responseBody = `{"access_token":"Sample.Access.Token","issued_token_type":"urn:ietf:params:oauth:token-type:access_token","token_type":"Bearer","expires_in":3600,"scope":"https://www.googleapis.com/auth/cloud-platform"}`
var expectedToken = stsTokenExchangeResponse{
var exchangeRequestBody = "audience=32555940559.apps.googleusercontent.com&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&requested_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fdevstorage.full_control&subject_token=Sample.Subject.Token&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Ajwt"
var exchangeResponseBody = `{"access_token":"Sample.Access.Token","issued_token_type":"urn:ietf:params:oauth:token-type:access_token","token_type":"Bearer","expires_in":3600,"scope":"https://www.googleapis.com/auth/cloud-platform"}`
var expectedExchangeToken = Response{
AccessToken: "Sample.Access.Token",
IssuedTokenType: "urn:ietf:params:oauth:token-type:access_token",
TokenType: "Bearer",
@@ -47,6 +47,18 @@ var expectedToken = stsTokenExchangeResponse{
RefreshToken: "",
}
var refreshToken = "ReFrEsHtOkEn"
var refreshRequestBody = "grant_type=refresh_token&refresh_token=" + refreshToken
var refreshResponseBody = `{"access_token":"Sample.Access.Token","issued_token_type":"urn:ietf:params:oauth:token-type:access_token","token_type":"Bearer","expires_in":3600,"scope":"https://www.googleapis.com/auth/cloud-platform","refresh_token":"REFRESHED_REFRESH"}`
var expectedRefreshResponse = Response{
AccessToken: "Sample.Access.Token",
IssuedTokenType: "urn:ietf:params:oauth:token-type:access_token",
TokenType: "Bearer",
ExpiresIn: 3600,
Scope: "https://www.googleapis.com/auth/cloud-platform",
RefreshToken: "REFRESHED_REFRESH",
}
func TestExchangeToken(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
@@ -65,26 +77,34 @@ func TestExchangeToken(t *testing.T) {
if err != nil {
t.Errorf("Failed reading request body: %v.", err)
}
if got, want := string(body), requestbody; got != want {
if got, want := string(body), exchangeRequestBody; got != want {
t.Errorf("Unexpected exchange payload, got %v but want %v", got, want)
}
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(responseBody))
w.Write([]byte(exchangeResponseBody))
}))
defer ts.Close()
headers := http.Header{}
headers.Add("Content-Type", "application/x-www-form-urlencoded")
resp, err := exchangeToken(context.Background(), ts.URL, &tokenRequest, auth, headers, nil)
resp, err := ExchangeToken(context.Background(), ts.URL, &exchangeTokenRequest, auth, headers, nil)
if err != nil {
t.Fatalf("exchangeToken failed with error: %v", err)
}
if expectedToken != *resp {
t.Errorf("mismatched messages received by mock server. \nWant: \n%v\n\nGot:\n%v", expectedToken, *resp)
if expectedExchangeToken != *resp {
t.Errorf("mismatched messages received by mock server. \nWant: \n%v\n\nGot:\n%v", expectedExchangeToken, *resp)
}
resp, err = ExchangeToken(context.Background(), ts.URL, &exchangeTokenRequest, auth, nil, nil)
if err != nil {
t.Fatalf("exchangeToken failed with error: %v", err)
}
if expectedExchangeToken != *resp {
t.Errorf("mismatched messages received by mock server. \nWant: \n%v\n\nGot:\n%v", expectedExchangeToken, *resp)
}
}
func TestExchangeToken_Err(t *testing.T) {
@@ -96,7 +116,7 @@ func TestExchangeToken_Err(t *testing.T) {
headers := http.Header{}
headers.Add("Content-Type", "application/x-www-form-urlencoded")
_, err := exchangeToken(context.Background(), ts.URL, &tokenRequest, auth, headers, nil)
_, err := ExchangeToken(context.Background(), ts.URL, &exchangeTokenRequest, auth, headers, nil)
if err == nil {
t.Errorf("Expected handled error; instead got nil.")
}
@@ -171,7 +191,7 @@ func TestExchangeToken_Opts(t *testing.T) {
// Send a proper reply so that no other errors crop up.
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(responseBody))
w.Write([]byte(exchangeResponseBody))
}))
defer ts.Close()
@@ -183,5 +203,69 @@ func TestExchangeToken_Opts(t *testing.T) {
inputOpts := make(map[string]interface{})
inputOpts["one"] = firstOption
inputOpts["two"] = secondOption
exchangeToken(context.Background(), ts.URL, &tokenRequest, auth, headers, inputOpts)
ExchangeToken(context.Background(), ts.URL, &exchangeTokenRequest, auth, headers, inputOpts)
}
func TestRefreshToken(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
t.Errorf("Unexpected request method, %v is found", r.Method)
}
if r.URL.String() != "/" {
t.Errorf("Unexpected request URL, %v is found", r.URL)
}
if got, want := r.Header.Get("Authorization"), "Basic cmJyZ25vZ25yaG9uZ28zYmk0Z2I5Z2hnOWc6bm90c29zZWNyZXQ="; got != want {
t.Errorf("Unexpected authorization header, got %v, want %v", got, want)
}
if got, want := r.Header.Get("Content-Type"), "application/x-www-form-urlencoded"; got != want {
t.Errorf("Unexpected Content-Type header, got %v, want %v", got, want)
}
body, err := ioutil.ReadAll(r.Body)
if err != nil {
t.Errorf("Failed reading request body: %v.", err)
}
if got, want := string(body), refreshRequestBody; got != want {
t.Errorf("Unexpected exchange payload, got %v but want %v", got, want)
}
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(refreshResponseBody))
}))
defer ts.Close()
headers := http.Header{}
headers.Add("Content-Type", "application/x-www-form-urlencoded")
resp, err := RefreshAccessToken(context.Background(), ts.URL, refreshToken, auth, headers)
if err != nil {
t.Fatalf("exchangeToken failed with error: %v", err)
}
if expectedRefreshResponse != *resp {
t.Errorf("mismatched messages received by mock server. \nWant: \n%v\n\nGot:\n%v", expectedRefreshResponse, *resp)
}
resp, err = RefreshAccessToken(context.Background(), ts.URL, refreshToken, auth, nil)
if err != nil {
t.Fatalf("exchangeToken failed with error: %v", err)
}
if expectedRefreshResponse != *resp {
t.Errorf("mismatched messages received by mock server. \nWant: \n%v\n\nGot:\n%v", expectedRefreshResponse, *resp)
}
}
func TestRefreshToken_Err(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte("what's wrong with this response?"))
}))
defer ts.Close()
headers := http.Header{}
headers.Add("Content-Type", "application/x-www-form-urlencoded")
_, err := RefreshAccessToken(context.Background(), ts.URL, refreshToken, auth, headers)
if err == nil {
t.Errorf("Expected handled error; instead got nil.")
}
}

View File

@@ -3,7 +3,6 @@
// license that can be found in the LICENSE file.
//go:build appengine
// +build appengine
package internal

View File

@@ -14,7 +14,7 @@ import (
// ParseKey converts the binary contents of a private key file
// to an *rsa.PrivateKey. It detects whether the private key is in a
// PEM container or not. If so, it extracts the the private key
// PEM container or not. If so, it extracts the private key
// from PEM container before conversion. It only supports PEM
// containers with no passphrase.
func ParseKey(key []byte) (*rsa.PrivateKey, error) {

View File

@@ -18,9 +18,8 @@ import (
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"golang.org/x/net/context/ctxhttp"
)
// Token represents the credentials used to authorize
@@ -57,12 +56,18 @@ type Token struct {
}
// tokenJSON is the struct representing the HTTP response from OAuth2
// providers returning a token in JSON form.
// providers returning a token or error in JSON form.
// https://datatracker.ietf.org/doc/html/rfc6749#section-5.1
type tokenJSON struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
RefreshToken string `json:"refresh_token"`
ExpiresIn expirationTime `json:"expires_in"` // at least PayPal returns string, while most return number
// error fields
// https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
ErrorCode string `json:"error"`
ErrorDescription string `json:"error_description"`
ErrorURI string `json:"error_uri"`
}
func (e *tokenJSON) expiry() (t time.Time) {
@@ -111,41 +116,60 @@ const (
AuthStyleInHeader AuthStyle = 2
)
// authStyleCache is the set of tokenURLs we've successfully used via
// LazyAuthStyleCache is a backwards compatibility compromise to let Configs
// have a lazily-initialized AuthStyleCache.
//
// The two users of this, oauth2.Config and oauth2/clientcredentials.Config,
// both would ideally just embed an unexported AuthStyleCache but because both
// were historically allowed to be copied by value we can't retroactively add an
// uncopyable Mutex to them.
//
// We could use an atomic.Pointer, but that was added recently enough (in Go
// 1.18) that we'd break Go 1.17 users where the tests as of 2023-08-03
// still pass. By using an atomic.Value, it supports both Go 1.17 and
// copying by value, even if that's not ideal.
type LazyAuthStyleCache struct {
v atomic.Value // of *AuthStyleCache
}
func (lc *LazyAuthStyleCache) Get() *AuthStyleCache {
if c, ok := lc.v.Load().(*AuthStyleCache); ok {
return c
}
c := new(AuthStyleCache)
if !lc.v.CompareAndSwap(nil, c) {
c = lc.v.Load().(*AuthStyleCache)
}
return c
}
// AuthStyleCache is the set of tokenURLs we've successfully used via
// RetrieveToken and which style auth we ended up using.
// It's called a cache, but it doesn't (yet?) shrink. It's expected that
// the set of OAuth2 servers a program contacts over time is fixed and
// small.
var authStyleCache struct {
sync.Mutex
m map[string]AuthStyle // keyed by tokenURL
}
// ResetAuthCache resets the global authentication style cache used
// for AuthStyleUnknown token requests.
func ResetAuthCache() {
authStyleCache.Lock()
defer authStyleCache.Unlock()
authStyleCache.m = nil
type AuthStyleCache struct {
mu sync.Mutex
m map[string]AuthStyle // keyed by tokenURL
}
// lookupAuthStyle reports which auth style we last used with tokenURL
// when calling RetrieveToken and whether we have ever done so.
func lookupAuthStyle(tokenURL string) (style AuthStyle, ok bool) {
authStyleCache.Lock()
defer authStyleCache.Unlock()
style, ok = authStyleCache.m[tokenURL]
func (c *AuthStyleCache) lookupAuthStyle(tokenURL string) (style AuthStyle, ok bool) {
c.mu.Lock()
defer c.mu.Unlock()
style, ok = c.m[tokenURL]
return
}
// setAuthStyle adds an entry to authStyleCache, documented above.
func setAuthStyle(tokenURL string, v AuthStyle) {
authStyleCache.Lock()
defer authStyleCache.Unlock()
if authStyleCache.m == nil {
authStyleCache.m = make(map[string]AuthStyle)
func (c *AuthStyleCache) setAuthStyle(tokenURL string, v AuthStyle) {
c.mu.Lock()
defer c.mu.Unlock()
if c.m == nil {
c.m = make(map[string]AuthStyle)
}
authStyleCache.m[tokenURL] = v
c.m[tokenURL] = v
}
// newTokenRequest returns a new *http.Request to retrieve a new token
@@ -185,10 +209,10 @@ func cloneURLValues(v url.Values) url.Values {
return v2
}
func RetrieveToken(ctx context.Context, clientID, clientSecret, tokenURL string, v url.Values, authStyle AuthStyle) (*Token, error) {
func RetrieveToken(ctx context.Context, clientID, clientSecret, tokenURL string, v url.Values, authStyle AuthStyle, styleCache *AuthStyleCache) (*Token, error) {
needsAuthStyleProbe := authStyle == 0
if needsAuthStyleProbe {
if style, ok := lookupAuthStyle(tokenURL); ok {
if style, ok := styleCache.lookupAuthStyle(tokenURL); ok {
authStyle = style
needsAuthStyleProbe = false
} else {
@@ -218,7 +242,7 @@ func RetrieveToken(ctx context.Context, clientID, clientSecret, tokenURL string,
token, err = doTokenRoundTrip(ctx, req)
}
if needsAuthStyleProbe && err == nil {
setAuthStyle(tokenURL, authStyle)
styleCache.setAuthStyle(tokenURL, authStyle)
}
// Don't overwrite `RefreshToken` with an empty value
// if this was a token refreshing request.
@@ -229,7 +253,7 @@ func RetrieveToken(ctx context.Context, clientID, clientSecret, tokenURL string,
}
func doTokenRoundTrip(ctx context.Context, req *http.Request) (*Token, error) {
r, err := ctxhttp.Do(ctx, ContextClient(ctx), req)
r, err := ContextClient(ctx).Do(req.WithContext(ctx))
if err != nil {
return nil, err
}
@@ -238,21 +262,29 @@ func doTokenRoundTrip(ctx context.Context, req *http.Request) (*Token, error) {
if err != nil {
return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err)
}
if code := r.StatusCode; code < 200 || code > 299 {
return nil, &RetrieveError{
Response: r,
Body: body,
}
failureStatus := r.StatusCode < 200 || r.StatusCode > 299
retrieveError := &RetrieveError{
Response: r,
Body: body,
// attempt to populate error detail below
}
var token *Token
content, _, _ := mime.ParseMediaType(r.Header.Get("Content-Type"))
switch content {
case "application/x-www-form-urlencoded", "text/plain":
// some endpoints return a query string
vals, err := url.ParseQuery(string(body))
if err != nil {
return nil, err
if failureStatus {
return nil, retrieveError
}
return nil, fmt.Errorf("oauth2: cannot parse response: %v", err)
}
retrieveError.ErrorCode = vals.Get("error")
retrieveError.ErrorDescription = vals.Get("error_description")
retrieveError.ErrorURI = vals.Get("error_uri")
token = &Token{
AccessToken: vals.Get("access_token"),
TokenType: vals.Get("token_type"),
@@ -267,8 +299,14 @@ func doTokenRoundTrip(ctx context.Context, req *http.Request) (*Token, error) {
default:
var tj tokenJSON
if err = json.Unmarshal(body, &tj); err != nil {
return nil, err
if failureStatus {
return nil, retrieveError
}
return nil, fmt.Errorf("oauth2: cannot parse json: %v", err)
}
retrieveError.ErrorCode = tj.ErrorCode
retrieveError.ErrorDescription = tj.ErrorDescription
retrieveError.ErrorURI = tj.ErrorURI
token = &Token{
AccessToken: tj.AccessToken,
TokenType: tj.TokenType,
@@ -278,17 +316,37 @@ func doTokenRoundTrip(ctx context.Context, req *http.Request) (*Token, error) {
}
json.Unmarshal(body, &token.Raw) // no error checks for optional fields
}
// according to spec, servers should respond status 400 in error case
// https://www.rfc-editor.org/rfc/rfc6749#section-5.2
// but some unorthodox servers respond 200 in error case
if failureStatus || retrieveError.ErrorCode != "" {
return nil, retrieveError
}
if token.AccessToken == "" {
return nil, errors.New("oauth2: server response missing access_token")
}
return token, nil
}
// mirrors oauth2.RetrieveError
type RetrieveError struct {
Response *http.Response
Body []byte
Response *http.Response
Body []byte
ErrorCode string
ErrorDescription string
ErrorURI string
}
func (r *RetrieveError) Error() string {
if r.ErrorCode != "" {
s := fmt.Sprintf("oauth2: %q", r.ErrorCode)
if r.ErrorDescription != "" {
s += fmt.Sprintf(" %q", r.ErrorDescription)
}
if r.ErrorURI != "" {
s += fmt.Sprintf(" %q", r.ErrorURI)
}
return s
}
return fmt.Sprintf("oauth2: cannot fetch token: %v\nResponse: %s", r.Response.Status, r.Body)
}

View File

@@ -16,7 +16,7 @@ import (
)
func TestRetrieveToken_InParams(t *testing.T) {
ResetAuthCache()
styleCache := new(AuthStyleCache)
const clientID = "client-id"
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if got, want := r.FormValue("client_id"), clientID; got != want {
@@ -29,14 +29,14 @@ func TestRetrieveToken_InParams(t *testing.T) {
io.WriteString(w, `{"access_token": "ACCESS_TOKEN", "token_type": "bearer"}`)
}))
defer ts.Close()
_, err := RetrieveToken(context.Background(), clientID, "", ts.URL, url.Values{}, AuthStyleInParams)
_, err := RetrieveToken(context.Background(), clientID, "", ts.URL, url.Values{}, AuthStyleInParams, styleCache)
if err != nil {
t.Errorf("RetrieveToken = %v; want no error", err)
}
}
func TestRetrieveTokenWithContexts(t *testing.T) {
ResetAuthCache()
styleCache := new(AuthStyleCache)
const clientID = "client-id"
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -45,7 +45,7 @@ func TestRetrieveTokenWithContexts(t *testing.T) {
}))
defer ts.Close()
_, err := RetrieveToken(context.Background(), clientID, "", ts.URL, url.Values{}, AuthStyleUnknown)
_, err := RetrieveToken(context.Background(), clientID, "", ts.URL, url.Values{}, AuthStyleUnknown, styleCache)
if err != nil {
t.Errorf("RetrieveToken (with background context) = %v; want no error", err)
}
@@ -58,7 +58,7 @@ func TestRetrieveTokenWithContexts(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
_, err = RetrieveToken(ctx, clientID, "", cancellingts.URL, url.Values{}, AuthStyleUnknown)
_, err = RetrieveToken(ctx, clientID, "", cancellingts.URL, url.Values{}, AuthStyleUnknown, styleCache)
close(retrieved)
if err == nil {
t.Errorf("RetrieveToken (with cancelled context) = nil; want error")

View File

@@ -16,6 +16,7 @@ import (
"net/url"
"strings"
"sync"
"time"
"golang.org/x/oauth2/internal"
)
@@ -57,6 +58,10 @@ type Config struct {
// Scope specifies optional requested permissions.
Scopes []string
// authStyleCache caches which auth style to use when Endpoint.AuthStyle is
// the zero value (AuthStyleAutoDetect).
authStyleCache internal.LazyAuthStyleCache
}
// A TokenSource is anything that can return a token.
@@ -70,8 +75,9 @@ type TokenSource interface {
// Endpoint represents an OAuth 2.0 provider's authorization and token
// endpoint URLs.
type Endpoint struct {
AuthURL string
TokenURL string
AuthURL string
DeviceAuthURL string
TokenURL string
// AuthStyle optionally specifies how the endpoint wants the
// client ID & client secret sent. The zero value means to
@@ -138,15 +144,19 @@ func SetAuthURLParam(key, value string) AuthCodeOption {
// AuthCodeURL returns a URL to OAuth 2.0 provider's consent page
// that asks for permissions for the required scopes explicitly.
//
// State is a token to protect the user from CSRF attacks. You must
// always provide a non-empty string and validate that it matches the
// the state query parameter on your redirect callback.
// See http://tools.ietf.org/html/rfc6749#section-10.12 for more info.
// State is an opaque value used by the client to maintain state between the
// request and callback. The authorization server includes this value when
// redirecting the user agent back to the client.
//
// Opts may include AccessTypeOnline or AccessTypeOffline, as well
// as ApprovalForce.
// It can also be used to pass the PKCE challenge.
// See https://www.oauth.com/oauth2-servers/pkce/ for more info.
//
// To protect against CSRF attacks, opts should include a PKCE challenge
// (S256ChallengeOption). Not all servers support PKCE. An alternative is to
// generate a random state parameter and verify it after exchange.
// See https://datatracker.ietf.org/doc/html/rfc6749#section-10.12 (predating
// PKCE), https://www.oauth.com/oauth2-servers/pkce/ and
// https://www.ietf.org/archive/id/draft-ietf-oauth-v2-1-09.html#name-cross-site-request-forgery (describing both approaches)
func (c *Config) AuthCodeURL(state string, opts ...AuthCodeOption) string {
var buf bytes.Buffer
buf.WriteString(c.Endpoint.AuthURL)
@@ -161,7 +171,6 @@ func (c *Config) AuthCodeURL(state string, opts ...AuthCodeOption) string {
v.Set("scope", strings.Join(c.Scopes, " "))
}
if state != "" {
// TODO(light): Docs say never to omit state; don't allow empty.
v.Set("state", state)
}
for _, opt := range opts {
@@ -206,10 +215,11 @@ func (c *Config) PasswordCredentialsToken(ctx context.Context, username, passwor
// The provided context optionally controls which HTTP client is used. See the HTTPClient variable.
//
// The code will be in the *http.Request.FormValue("code"). Before
// calling Exchange, be sure to validate FormValue("state").
// calling Exchange, be sure to validate FormValue("state") if you are
// using it to protect against CSRF attacks.
//
// Opts may include the PKCE verifier code if previously used in AuthCodeURL.
// See https://www.oauth.com/oauth2-servers/pkce/ for more info.
// If using PKCE to protect against CSRF attacks, opts should include a
// VerifierOption.
func (c *Config) Exchange(ctx context.Context, code string, opts ...AuthCodeOption) (*Token, error) {
v := url.Values{
"grant_type": {"authorization_code"},
@@ -290,6 +300,8 @@ type reuseTokenSource struct {
mu sync.Mutex // guards t
t *Token
expiryDelta time.Duration
}
// Token returns the current token if it's still valid, else will
@@ -305,6 +317,7 @@ func (s *reuseTokenSource) Token() (*Token, error) {
if err != nil {
return nil, err
}
t.expiryDelta = s.expiryDelta
s.t = t
return t, nil
}
@@ -379,3 +392,30 @@ func ReuseTokenSource(t *Token, src TokenSource) TokenSource {
new: src,
}
}
// ReuseTokenSource 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).
func ReuseTokenSourceWithExpiry(t *Token, src TokenSource, earlyExpiry time.Duration) TokenSource {
// Don't wrap a reuseTokenSource in itself. That would work,
// but cause an unnecessary number of mutex operations.
// Just build the equivalent one.
if rt, ok := src.(*reuseTokenSource); ok {
if t == nil {
// Just use it directly, but set the expiryDelta to earlyExpiry,
// so the behavior matches what the user expects.
rt.expiryDelta = earlyExpiry
return rt
}
src = rt.new
}
if t != nil {
t.expiryDelta = earlyExpiry
}
return &reuseTokenSource{
t: t,
new: src,
expiryDelta: earlyExpiry,
}
}

View File

@@ -15,8 +15,6 @@ import (
"net/url"
"testing"
"time"
"golang.org/x/oauth2/internal"
)
type mockTransport struct {
@@ -355,7 +353,6 @@ func TestExchangeRequest_BadResponseType(t *testing.T) {
}
func TestExchangeRequest_NonBasicAuth(t *testing.T) {
internal.ResetAuthCache()
tr := &mockTransport{
rt: func(r *http.Request) (w *http.Response, err error) {
headerAuth := r.Header.Get("Authorization")
@@ -427,7 +424,6 @@ func TestPasswordCredentialsTokenRequest(t *testing.T) {
}
func TestTokenRefreshRequest(t *testing.T) {
internal.ResetAuthCache()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.String() == "/somethingelse" {
return
@@ -484,6 +480,7 @@ func TestTokenRetrieveError(t *testing.T) {
t.Errorf("Unexpected token refresh request URL, %v is found.", r.URL)
}
w.Header().Set("Content-type", "application/json")
// "The authorization server responds with an HTTP 400 (Bad Request)" https://www.rfc-editor.org/rfc/rfc6749#section-5.2
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(`{"error": "invalid_grant"}`))
}))
@@ -493,15 +490,47 @@ func TestTokenRetrieveError(t *testing.T) {
if err == nil {
t.Fatalf("got no error, expected one")
}
_, ok := err.(*RetrieveError)
re, ok := err.(*RetrieveError)
if !ok {
t.Fatalf("got %T error, expected *RetrieveError; error was: %v", err, err)
}
// Test error string for backwards compatibility
expected := fmt.Sprintf("oauth2: cannot fetch token: %v\nResponse: %s", "400 Bad Request", `{"error": "invalid_grant"}`)
expected := `oauth2: "invalid_grant"`
if errStr := err.Error(); errStr != expected {
t.Fatalf("got %#v, expected %#v", errStr, expected)
}
expected = "invalid_grant"
if re.ErrorCode != expected {
t.Fatalf("got %#v, expected %#v", re.ErrorCode, expected)
}
}
// TestTokenRetrieveError200 tests handling of unorthodox server that returns 200 in error case
func TestTokenRetrieveError200(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.String() != "/token" {
t.Errorf("Unexpected token refresh request URL, %v is found.", r.URL)
}
w.Header().Set("Content-type", "application/json")
w.Write([]byte(`{"error": "invalid_grant"}`))
}))
defer ts.Close()
conf := newConf(ts.URL)
_, err := conf.Exchange(context.Background(), "exchange-code")
if err == nil {
t.Fatalf("got no error, expected one")
}
re, ok := err.(*RetrieveError)
if !ok {
t.Fatalf("got %T error, expected *RetrieveError; error was: %v", err, err)
}
expected := `oauth2: "invalid_grant"`
if errStr := err.Error(); errStr != expected {
t.Fatalf("got %#v, expected %#v", errStr, expected)
}
expected = "invalid_grant"
if re.ErrorCode != expected {
t.Fatalf("got %#v, expected %#v", re.ErrorCode, expected)
}
}
func TestRefreshToken_RefreshTokenReplacement(t *testing.T) {

68
pkce.go Normal file
View File

@@ -0,0 +1,68 @@
// Copyright 2023 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.
package oauth2
import (
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"net/url"
)
const (
codeChallengeKey = "code_challenge"
codeChallengeMethodKey = "code_challenge_method"
codeVerifierKey = "code_verifier"
)
// GenerateVerifier generates a PKCE code verifier with 32 octets of randomness.
// This follows recommendations in RFC 7636.
//
// 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.DeviceAccessToken).
func GenerateVerifier() string {
// "RECOMMENDED that the output of a suitable random number generator be
// used to create a 32-octet sequence. The octet sequence is then
// base64url-encoded to produce a 43-octet URL-safe string to use as the
// code verifier."
// https://datatracker.ietf.org/doc/html/rfc7636#section-4.1
data := make([]byte, 32)
if _, err := rand.Read(data); err != nil {
panic(err)
}
return base64.RawURLEncoding.EncodeToString(data)
}
// VerifierOption returns a PKCE code verifier AuthCodeOption. It should be
// passed to Config.Exchange or Config.DeviceAccessToken only.
func VerifierOption(verifier string) AuthCodeOption {
return setParam{k: codeVerifierKey, v: verifier}
}
// S256ChallengeFromVerifier returns a PKCE code challenge derived from verifier with method S256.
//
// Prefer to use S256ChallengeOption where possible.
func S256ChallengeFromVerifier(verifier string) string {
sha := sha256.Sum256([]byte(verifier))
return base64.RawURLEncoding.EncodeToString(sha[:])
}
// S256ChallengeOption derives a PKCE code challenge derived from verifier with
// method S256. It should be passed to Config.AuthCodeURL or Config.DeviceAccess
// only.
func S256ChallengeOption(verifier string) AuthCodeOption {
return challengeOption{
challenge_method: "S256",
challenge: S256ChallengeFromVerifier(verifier),
}
}
type challengeOption struct{ challenge_method, challenge string }
func (p challengeOption) setValue(m url.Values) {
m.Set(codeChallengeMethodKey, p.challenge_method)
m.Set(codeChallengeKey, p.challenge)
}

View File

@@ -16,10 +16,10 @@ import (
"golang.org/x/oauth2/internal"
)
// expiryDelta determines how earlier a token should be considered
// defaultExpiryDelta determines how earlier a token should be considered
// expired than its actual expiration time. It is used to avoid late
// expirations due to client-server time mismatches.
const expiryDelta = 10 * time.Second
const defaultExpiryDelta = 10 * time.Second
// Token represents the credentials used to authorize
// the requests to access protected resources on the OAuth 2.0
@@ -52,6 +52,11 @@ type Token struct {
// raw optionally contains extra metadata from the server
// when updating a token.
raw interface{}
// expiryDelta is used to calculate when a token is considered
// expired, by subtracting from Expiry. If zero, defaultExpiryDelta
// is used.
expiryDelta time.Duration
}
// Type returns t.TokenType if non-empty, else "Bearer".
@@ -127,6 +132,11 @@ func (t *Token) expired() bool {
if t.Expiry.IsZero() {
return false
}
expiryDelta := defaultExpiryDelta
if t.expiryDelta != 0 {
expiryDelta = t.expiryDelta
}
return t.Expiry.Round(0).Add(-expiryDelta).Before(timeNow())
}
@@ -154,7 +164,7 @@ func tokenFromInternal(t *internal.Token) *Token {
// This token is then mapped from *internal.Token into an *oauth2.Token which is returned along
// with an error..
func retrieveToken(ctx context.Context, c *Config, v url.Values) (*Token, error) {
tk, err := internal.RetrieveToken(ctx, c.ClientID, c.ClientSecret, c.Endpoint.TokenURL, v, internal.AuthStyle(c.Endpoint.AuthStyle))
tk, err := internal.RetrieveToken(ctx, c.ClientID, c.ClientSecret, c.Endpoint.TokenURL, v, internal.AuthStyle(c.Endpoint.AuthStyle), c.authStyleCache.Get())
if err != nil {
if rErr, ok := err.(*internal.RetrieveError); ok {
return nil, (*RetrieveError)(rErr)
@@ -165,14 +175,31 @@ func retrieveToken(ctx context.Context, c *Config, v url.Values) (*Token, error)
}
// RetrieveError is the error returned when the token endpoint returns a
// non-2XX HTTP status code.
// non-2XX HTTP status code or populates RFC 6749's 'error' parameter.
// https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
type RetrieveError struct {
Response *http.Response
// Body is the body that was consumed by reading Response.Body.
// It may be truncated.
Body []byte
// ErrorCode is RFC 6749's 'error' parameter.
ErrorCode string
// ErrorDescription is RFC 6749's 'error_description' parameter.
ErrorDescription string
// ErrorURI is RFC 6749's 'error_uri' parameter.
ErrorURI string
}
func (r *RetrieveError) Error() string {
if r.ErrorCode != "" {
s := fmt.Sprintf("oauth2: %q", r.ErrorCode)
if r.ErrorDescription != "" {
s += fmt.Sprintf(" %q", r.ErrorDescription)
}
if r.ErrorURI != "" {
s += fmt.Sprintf(" %q", r.ErrorURI)
}
return s
}
return fmt.Sprintf("oauth2: cannot fetch token: %v\nResponse: %s", r.Response.Status, r.Body)
}

View File

@@ -43,9 +43,13 @@ func TestTokenExpiry(t *testing.T) {
want bool
}{
{name: "12 seconds", tok: &Token{Expiry: now.Add(12 * time.Second)}, want: false},
{name: "10 seconds", tok: &Token{Expiry: now.Add(expiryDelta)}, want: false},
{name: "10 seconds-1ns", tok: &Token{Expiry: now.Add(expiryDelta - 1*time.Nanosecond)}, want: true},
{name: "10 seconds", tok: &Token{Expiry: now.Add(defaultExpiryDelta)}, want: false},
{name: "10 seconds-1ns", tok: &Token{Expiry: now.Add(defaultExpiryDelta - 1*time.Nanosecond)}, want: true},
{name: "-1 hour", tok: &Token{Expiry: now.Add(-1 * time.Hour)}, want: true},
{name: "12 seconds, custom expiryDelta", tok: &Token{Expiry: now.Add(12 * time.Second), expiryDelta: time.Second * 5}, want: false},
{name: "5 seconds, custom expiryDelta", tok: &Token{Expiry: now.Add(time.Second * 5), expiryDelta: time.Second * 5}, want: false},
{name: "5 seconds-1ns, custom expiryDelta", tok: &Token{Expiry: now.Add(time.Second*5 - 1*time.Nanosecond), expiryDelta: time.Second * 5}, want: true},
{name: "-1 hour, custom expiryDelta", tok: &Token{Expiry: now.Add(-1 * time.Hour), expiryDelta: time.Second * 5}, want: true},
}
for _, tc := range cases {
if got, want := tc.tok.expired(), tc.want; got != want {