mirror of
https://github.com/golang/oauth2.git
synced 2025-07-21 00:00:09 +08:00
Compare commits
44 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
cf14319341 | ||
|
32d34ef364 | ||
|
2d34e3091b | ||
|
696f7b3128 | ||
|
471209bbe2 | ||
|
6968da209b | ||
|
d2c4e0a625 | ||
|
883dc3c9d8 | ||
|
1c06e8705e | ||
|
65c15a3514 | ||
|
ce56909505 | ||
|
0042180b24 | ||
|
ce350bff61 | ||
|
44967abe90 | ||
|
9c82a8cf7a | ||
|
681b4d8edc | ||
|
3f78298bee | ||
|
109dabf901 | ||
|
ac571fa341 | ||
|
314ee5b92b | ||
|
b9c813be7d | ||
|
49a531d12a | ||
|
22134a4103 | ||
|
3e6480915d | ||
|
16a9973a41 | ||
|
b52af7d5b4 | ||
|
6d8340f1c5 | ||
|
5fd42413ed | ||
|
84cb9f7f5c | ||
|
4b7f0bdbc7 | ||
|
e11eea88a8 | ||
|
d0e617c58c | ||
|
3c9c1f6d00 | ||
|
5a05c654f9 | ||
|
3a6776ada7 | ||
|
85231f99d6 | ||
|
34a7afaa85 | ||
|
95bec95381 | ||
|
ebe81ad837 | ||
|
adffd94437 | ||
|
deefa7e836 | ||
|
39adbb7807 | ||
|
4ce7bbb2ff | ||
|
1e6999b1be |
4
LICENSE
4
LICENSE
@ -1,4 +1,4 @@
|
||||
Copyright (c) 2009 The Go Authors. All rights reserved.
|
||||
Copyright 2009 The Go Authors.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
@ -10,7 +10,7 @@ notice, this list of conditions and the following disclaimer.
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
* Neither the name of Google LLC nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
|
15
README.md
15
README.md
@ -5,15 +5,6 @@
|
||||
|
||||
oauth2 package contains a client implementation for OAuth 2.0 spec.
|
||||
|
||||
## Installation
|
||||
|
||||
~~~~
|
||||
go get golang.org/x/oauth2
|
||||
~~~~
|
||||
|
||||
Or you can manually git clone the repository to
|
||||
`$(go env GOPATH)/src/golang.org/x/oauth2`.
|
||||
|
||||
See pkg.go.dev for further documentation and examples.
|
||||
|
||||
* [pkg.go.dev/golang.org/x/oauth2](https://pkg.go.dev/golang.org/x/oauth2)
|
||||
@ -33,7 +24,11 @@ The main issue tracker for the oauth2 repository is located at
|
||||
https://github.com/golang/oauth2/issues.
|
||||
|
||||
This repository uses Gerrit for code changes. To learn how to submit changes to
|
||||
this repository, see https://golang.org/doc/contribute.html. In particular:
|
||||
this repository, see https://go.dev/doc/contribute.
|
||||
|
||||
The git repository is https://go.googlesource.com/oauth2.
|
||||
|
||||
Note:
|
||||
|
||||
* Excluding trivial changes, all contributions should be connected to an existing issue.
|
||||
* API changes must go through the [change proposal process](https://go.dev/s/proposal-process) before they can be accepted.
|
||||
|
@ -34,7 +34,7 @@ type PKCEParams struct {
|
||||
// and returns an auth code and state upon approval.
|
||||
type AuthorizationHandler func(authCodeURL string) (code string, state string, err error)
|
||||
|
||||
// TokenSourceWithPKCE is an enhanced version of TokenSource with PKCE support.
|
||||
// TokenSourceWithPKCE is an enhanced version of [oauth2.TokenSource] with PKCE support.
|
||||
//
|
||||
// The pkce parameter supports PKCE flow, which uses code challenge and code verifier
|
||||
// to prevent CSRF attacks. A unique code challenge and code verifier should be generated
|
||||
@ -43,12 +43,12 @@ func TokenSourceWithPKCE(ctx context.Context, config *oauth2.Config, state strin
|
||||
return oauth2.ReuseTokenSource(nil, authHandlerSource{config: config, ctx: ctx, authHandler: authHandler, state: state, pkce: pkce})
|
||||
}
|
||||
|
||||
// TokenSource returns an oauth2.TokenSource that fetches access tokens
|
||||
// TokenSource returns an [oauth2.TokenSource] that fetches access tokens
|
||||
// using 3-legged-OAuth flow.
|
||||
//
|
||||
// The provided context.Context is used for oauth2 Exchange operation.
|
||||
// The provided [context.Context] is used for oauth2 Exchange operation.
|
||||
//
|
||||
// The provided oauth2.Config should be a full configuration containing AuthURL,
|
||||
// The provided [oauth2.Config] should be a full configuration containing AuthURL,
|
||||
// TokenURL, and Scope.
|
||||
//
|
||||
// An environment-specific AuthorizationHandler is used to obtain user consent.
|
||||
|
@ -37,7 +37,7 @@ type Config struct {
|
||||
// URL. This is a constant specific to each server.
|
||||
TokenURL string
|
||||
|
||||
// Scope specifies optional requested permissions.
|
||||
// Scopes specifies optional requested permissions.
|
||||
Scopes []string
|
||||
|
||||
// EndpointParams specifies additional parameters for requests to the token endpoint.
|
||||
@ -55,7 +55,7 @@ type Config struct {
|
||||
|
||||
// Token uses client credentials to retrieve a token.
|
||||
//
|
||||
// The provided context optionally controls which HTTP client is used. See the oauth2.HTTPClient variable.
|
||||
// The provided context optionally controls which HTTP client is used. See the [oauth2.HTTPClient] variable.
|
||||
func (c *Config) Token(ctx context.Context) (*oauth2.Token, error) {
|
||||
return c.TokenSource(ctx).Token()
|
||||
}
|
||||
@ -64,18 +64,18 @@ func (c *Config) Token(ctx context.Context) (*oauth2.Token, error) {
|
||||
// The token will auto-refresh as necessary.
|
||||
//
|
||||
// The provided context optionally controls which HTTP client
|
||||
// is returned. See the oauth2.HTTPClient variable.
|
||||
// is returned. See the [oauth2.HTTPClient] variable.
|
||||
//
|
||||
// The returned Client and its Transport should not be modified.
|
||||
// The returned [http.Client] and its Transport should not be modified.
|
||||
func (c *Config) Client(ctx context.Context) *http.Client {
|
||||
return oauth2.NewClient(ctx, c.TokenSource(ctx))
|
||||
}
|
||||
|
||||
// TokenSource returns a TokenSource that returns t until t expires,
|
||||
// TokenSource returns a [oauth2.TokenSource] that returns t until t expires,
|
||||
// automatically refreshing it as necessary using the provided context and the
|
||||
// client ID and client secret.
|
||||
//
|
||||
// Most users will use Config.Client instead.
|
||||
// Most users will use [Config.Client] instead.
|
||||
func (c *Config) TokenSource(ctx context.Context) oauth2.TokenSource {
|
||||
source := &tokenSource{
|
||||
ctx: ctx,
|
||||
|
@ -7,7 +7,6 @@ package clientcredentials
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
@ -36,9 +35,9 @@ func TestTokenSourceGrantTypeOverride(t *testing.T) {
|
||||
wantGrantType := "password"
|
||||
var gotGrantType string
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
t.Errorf("ioutil.ReadAll(r.Body) == %v, %v, want _, <nil>", body, err)
|
||||
t.Errorf("io.ReadAll(r.Body) == %v, %v, want _, <nil>", body, err)
|
||||
}
|
||||
if err := r.Body.Close(); err != nil {
|
||||
t.Errorf("r.Body.Close() == %v, want <nil>", err)
|
||||
@ -81,7 +80,7 @@ func TestTokenRequest(t *testing.T) {
|
||||
if got, want := r.Header.Get("Content-Type"), "application/x-www-form-urlencoded"; got != want {
|
||||
t.Errorf("Content-Type header = %q; want %q", got, want)
|
||||
}
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
r.Body.Close()
|
||||
}
|
||||
@ -123,7 +122,7 @@ func TestTokenRefreshRequest(t *testing.T) {
|
||||
if got, want := headerContentType, "application/x-www-form-urlencoded"; got != want {
|
||||
t.Errorf("Content-Type = %q; want %q", got, want)
|
||||
}
|
||||
body, _ := ioutil.ReadAll(r.Body)
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
const want = "audience=audience1&grant_type=client_credentials&scope=scope1+scope2"
|
||||
if string(body) != want {
|
||||
t.Errorf("Unexpected refresh token payload.\n got: %s\nwant: %s\n", body, want)
|
||||
|
@ -7,9 +7,6 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
)
|
||||
|
||||
func TestDeviceAuthResponseMarshalJson(t *testing.T) {
|
||||
@ -74,7 +71,16 @@ func TestDeviceAuthResponseUnmarshalJson(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !cmp.Equal(got, tc.want, cmpopts.IgnoreUnexported(DeviceAuthResponse{}), cmpopts.EquateApproxTime(time.Second+time.Since(begin))) {
|
||||
margin := time.Second + time.Since(begin)
|
||||
timeDiff := got.Expiry.Sub(tc.want.Expiry)
|
||||
if timeDiff < 0 {
|
||||
timeDiff *= -1
|
||||
}
|
||||
if timeDiff > margin {
|
||||
t.Errorf("expiry time difference too large, got=%v, want=%v margin=%v", got.Expiry, tc.want.Expiry, margin)
|
||||
}
|
||||
got.Expiry, tc.want.Expiry = time.Time{}, time.Time{}
|
||||
if got != tc.want {
|
||||
t.Errorf("want=%#v, got=%#v", tc.want, got)
|
||||
}
|
||||
})
|
||||
|
@ -6,7 +6,7 @@
|
||||
package endpoints
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"net/url"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
@ -17,6 +17,30 @@ var Amazon = oauth2.Endpoint{
|
||||
TokenURL: "https://api.amazon.com/auth/o2/token",
|
||||
}
|
||||
|
||||
// Apple is the endpoint for "Sign in with Apple".
|
||||
//
|
||||
// Documentation: https://developer.apple.com/documentation/signinwithapplerestapi
|
||||
var Apple = oauth2.Endpoint{
|
||||
AuthURL: "https://appleid.apple.com/auth/authorize",
|
||||
TokenURL: "https://appleid.apple.com/auth/token",
|
||||
}
|
||||
|
||||
// Asana is the endpoint for Asana.
|
||||
//
|
||||
// Documentation: https://developers.asana.com/docs/oauth
|
||||
var Asana = oauth2.Endpoint{
|
||||
AuthURL: "https://app.asana.com/-/oauth_authorize",
|
||||
TokenURL: "https://app.asana.com/-/oauth_token",
|
||||
}
|
||||
|
||||
// Badgr is the endpoint for Canvas Badges.
|
||||
//
|
||||
// Documentation: https://community.canvaslms.com/t5/Canvas-Badges-Credentials/Developers-Build-an-app-that-integrates-with-the-Canvas-Badges/ta-p/528727
|
||||
var Badgr = oauth2.Endpoint{
|
||||
AuthURL: "https://badgr.com/auth/oauth2/authorize",
|
||||
TokenURL: "https://api.badgr.io/o/token",
|
||||
}
|
||||
|
||||
// Battlenet is the endpoint for Battlenet.
|
||||
var Battlenet = oauth2.Endpoint{
|
||||
AuthURL: "https://battle.net/oauth/authorize",
|
||||
@ -35,10 +59,44 @@ var Cern = oauth2.Endpoint{
|
||||
TokenURL: "https://oauth.web.cern.ch/OAuth/Token",
|
||||
}
|
||||
|
||||
// Coinbase is the endpoint for Coinbase.
|
||||
//
|
||||
// Documentation: https://docs.cdp.coinbase.com/coinbase-app/docs/coinbase-app-reference
|
||||
var Coinbase = oauth2.Endpoint{
|
||||
AuthURL: "https://login.coinbase.com/oauth2/auth",
|
||||
TokenURL: "https://login.coinbase.com/oauth2/token",
|
||||
}
|
||||
|
||||
// Discord is the endpoint for Discord.
|
||||
//
|
||||
// Documentation: https://discord.com/developers/docs/topics/oauth2#shared-resources-oauth2-urls
|
||||
var Discord = oauth2.Endpoint{
|
||||
AuthURL: "https://discord.com/oauth2/authorize",
|
||||
TokenURL: "https://discord.com/api/oauth2/token",
|
||||
}
|
||||
|
||||
// Dropbox is the endpoint for Dropbox.
|
||||
//
|
||||
// Documentation: https://developers.dropbox.com/oauth-guide
|
||||
var Dropbox = oauth2.Endpoint{
|
||||
AuthURL: "https://www.dropbox.com/oauth2/authorize",
|
||||
TokenURL: "https://api.dropboxapi.com/oauth2/token",
|
||||
}
|
||||
|
||||
// Endpoint is Ebay's OAuth 2.0 endpoint.
|
||||
//
|
||||
// Documentation: https://developer.ebay.com/api-docs/static/authorization_guide_landing.html
|
||||
var Endpoint = oauth2.Endpoint{
|
||||
AuthURL: "https://auth.ebay.com/oauth2/authorize",
|
||||
TokenURL: "https://api.ebay.com/identity/v1/oauth2/token",
|
||||
}
|
||||
|
||||
// Facebook is the endpoint for Facebook.
|
||||
//
|
||||
// Documentation: https://developers.facebook.com/docs/facebook-login/guides/advanced/manual-flow
|
||||
var Facebook = oauth2.Endpoint{
|
||||
AuthURL: "https://www.facebook.com/v3.2/dialog/oauth",
|
||||
TokenURL: "https://graph.facebook.com/v3.2/oauth/access_token",
|
||||
AuthURL: "https://www.facebook.com/v22.0/dialog/oauth",
|
||||
TokenURL: "https://graph.facebook.com/v22.0/oauth/access_token",
|
||||
}
|
||||
|
||||
// Foursquare is the endpoint for Foursquare.
|
||||
@ -62,14 +120,15 @@ var GitHub = oauth2.Endpoint{
|
||||
|
||||
// GitLab is the endpoint for GitLab.
|
||||
var GitLab = oauth2.Endpoint{
|
||||
AuthURL: "https://gitlab.com/oauth/authorize",
|
||||
TokenURL: "https://gitlab.com/oauth/token",
|
||||
AuthURL: "https://gitlab.com/oauth/authorize",
|
||||
TokenURL: "https://gitlab.com/oauth/token",
|
||||
DeviceAuthURL: "https://gitlab.com/oauth/authorize_device",
|
||||
}
|
||||
|
||||
// Google is the endpoint for Google.
|
||||
var Google = oauth2.Endpoint{
|
||||
AuthURL: "https://accounts.google.com/o/oauth2/auth",
|
||||
TokenURL: "https://oauth2.googleapis.com/token",
|
||||
AuthURL: "https://accounts.google.com/o/oauth2/auth",
|
||||
TokenURL: "https://oauth2.googleapis.com/token",
|
||||
DeviceAuthURL: "https://oauth2.googleapis.com/device/code",
|
||||
}
|
||||
|
||||
@ -97,6 +156,14 @@ var KaKao = oauth2.Endpoint{
|
||||
TokenURL: "https://kauth.kakao.com/oauth/token",
|
||||
}
|
||||
|
||||
// Line is the endpoint for Line.
|
||||
//
|
||||
// Documentation: https://developers.line.biz/en/docs/line-login/integrate-line-login/
|
||||
var Line = oauth2.Endpoint{
|
||||
AuthURL: "https://access.line.me/oauth2/v2.1/authorize",
|
||||
TokenURL: "https://api.line.me/oauth2/v2.1/token",
|
||||
}
|
||||
|
||||
// LinkedIn is the endpoint for LinkedIn.
|
||||
var LinkedIn = oauth2.Endpoint{
|
||||
AuthURL: "https://www.linkedin.com/oauth/v2/authorization",
|
||||
@ -133,7 +200,17 @@ var Microsoft = oauth2.Endpoint{
|
||||
TokenURL: "https://login.live.com/oauth20_token.srf",
|
||||
}
|
||||
|
||||
// Naver is the endpoint for Naver.
|
||||
//
|
||||
// Documentation: https://developers.naver.com/docs/login/devguide/devguide.md
|
||||
var Naver = oauth2.Endpoint{
|
||||
AuthURL: "https://nid.naver.com/oauth2/authorize",
|
||||
TokenURL: "https://nid.naver.com/oauth2/token",
|
||||
}
|
||||
|
||||
// NokiaHealth is the endpoint for Nokia Health.
|
||||
//
|
||||
// Deprecated: Nokia Health is now Withings.
|
||||
var NokiaHealth = oauth2.Endpoint{
|
||||
AuthURL: "https://account.health.nokia.com/oauth2_user/authorize2",
|
||||
TokenURL: "https://account.health.nokia.com/oauth2/token",
|
||||
@ -145,6 +222,20 @@ var Odnoklassniki = oauth2.Endpoint{
|
||||
TokenURL: "https://api.odnoklassniki.ru/oauth/token.do",
|
||||
}
|
||||
|
||||
// OpenStreetMap is the endpoint for OpenStreetMap.org.
|
||||
//
|
||||
// Documentation: https://wiki.openstreetmap.org/wiki/OAuth
|
||||
var OpenStreetMap = oauth2.Endpoint{
|
||||
AuthURL: "https://www.openstreetmap.org/oauth2/authorize",
|
||||
TokenURL: "https://www.openstreetmap.org/oauth2/token",
|
||||
}
|
||||
|
||||
// Patreon is the endpoint for Patreon.
|
||||
var Patreon = oauth2.Endpoint{
|
||||
AuthURL: "https://www.patreon.com/oauth2/authorize",
|
||||
TokenURL: "https://www.patreon.com/api/oauth2/token",
|
||||
}
|
||||
|
||||
// PayPal is the endpoint for PayPal.
|
||||
var PayPal = oauth2.Endpoint{
|
||||
AuthURL: "https://www.paypal.com/webapps/auth/protocol/openidconnect/v1/authorize",
|
||||
@ -157,10 +248,52 @@ var PayPalSandbox = oauth2.Endpoint{
|
||||
TokenURL: "https://api.sandbox.paypal.com/v1/identity/openidconnect/tokenservice",
|
||||
}
|
||||
|
||||
// Pinterest is the endpoint for Pinterest.
|
||||
//
|
||||
// Documentation: https://developers.pinterest.com/docs/getting-started/set-up-authentication-and-authorization/
|
||||
var Pinterest = oauth2.Endpoint{
|
||||
AuthURL: "https://www.pinterest.com/oauth",
|
||||
TokenURL: "https://api.pinterest.com/v5/oauth/token",
|
||||
}
|
||||
|
||||
// Pipedrive is the endpoint for Pipedrive.
|
||||
//
|
||||
// Documentation: https://developers.pipedrive.com/docs/api/v1/Oauth
|
||||
var Pipedrive = oauth2.Endpoint{
|
||||
AuthURL: "https://oauth.pipedrive.com/oauth/authorize",
|
||||
TokenURL: "https://oauth.pipedrive.com/oauth/token",
|
||||
}
|
||||
|
||||
// QQ is the endpoint for QQ.
|
||||
//
|
||||
// Documentation: https://wiki.connect.qq.com/%e5%bc%80%e5%8f%91%e6%94%bb%e7%95%a5_server-side
|
||||
var QQ = oauth2.Endpoint{
|
||||
AuthURL: "https://graph.qq.com/oauth2.0/authorize",
|
||||
TokenURL: "https://graph.qq.com/oauth2.0/token",
|
||||
}
|
||||
|
||||
// Rakuten is the endpoint for Rakuten.
|
||||
//
|
||||
// Documentation: https://webservice.rakuten.co.jp/documentation
|
||||
var Rakuten = oauth2.Endpoint{
|
||||
AuthURL: "https://app.rakuten.co.jp/services/authorize",
|
||||
TokenURL: "https://app.rakuten.co.jp/services/token",
|
||||
}
|
||||
|
||||
// Slack is the endpoint for Slack.
|
||||
//
|
||||
// Documentation: https://api.slack.com/authentication/oauth-v2
|
||||
var Slack = oauth2.Endpoint{
|
||||
AuthURL: "https://slack.com/oauth/authorize",
|
||||
TokenURL: "https://slack.com/api/oauth.access",
|
||||
AuthURL: "https://slack.com/oauth/v2/authorize",
|
||||
TokenURL: "https://slack.com/api/oauth.v2.access",
|
||||
}
|
||||
|
||||
// Splitwise is the endpoint for Splitwise.
|
||||
//
|
||||
// Documentation: https://dev.splitwise.com/
|
||||
var Splitwise = oauth2.Endpoint{
|
||||
AuthURL: "https://www.splitwise.com/oauth/authorize",
|
||||
TokenURL: "https://www.splitwise.com/oauth/token",
|
||||
}
|
||||
|
||||
// Spotify is the endpoint for Spotify.
|
||||
@ -199,6 +332,22 @@ var Vk = oauth2.Endpoint{
|
||||
TokenURL: "https://oauth.vk.com/access_token",
|
||||
}
|
||||
|
||||
// Withings is the endpoint for Withings.
|
||||
//
|
||||
// Documentation: https://account.withings.com/oauth2_user/authorize2
|
||||
var Withings = oauth2.Endpoint{
|
||||
AuthURL: "https://account.withings.com/oauth2_user/authorize2",
|
||||
TokenURL: "https://account.withings.com/oauth2/token",
|
||||
}
|
||||
|
||||
// X is the endpoint for X (Twitter).
|
||||
//
|
||||
// Documentation: https://docs.x.com/resources/fundamentals/authentication/oauth-2-0/user-access-token
|
||||
var X = oauth2.Endpoint{
|
||||
AuthURL: "https://x.com/i/oauth2/authorize",
|
||||
TokenURL: "https://api.x.com/2/oauth2/token",
|
||||
}
|
||||
|
||||
// Yahoo is the endpoint for Yahoo.
|
||||
var Yahoo = oauth2.Endpoint{
|
||||
AuthURL: "https://api.login.yahoo.com/oauth2/request_auth",
|
||||
@ -217,6 +366,20 @@ var Zoom = oauth2.Endpoint{
|
||||
TokenURL: "https://zoom.us/oauth/token",
|
||||
}
|
||||
|
||||
// Asgardeo returns a new oauth2.Endpoint for the given tenant.
|
||||
//
|
||||
// Documentation: https://wso2.com/asgardeo/docs/guides/authentication/oidc/discover-oidc-configs/
|
||||
func AsgardeoEndpoint(tenant string) oauth2.Endpoint {
|
||||
u := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "api.asgardeo.io",
|
||||
}
|
||||
return oauth2.Endpoint{
|
||||
AuthURL: u.JoinPath("t", tenant, "/oauth2/authorize").String(),
|
||||
TokenURL: u.JoinPath("t", tenant, "/oauth2/token").String(),
|
||||
}
|
||||
}
|
||||
|
||||
// AzureAD returns a new oauth2.Endpoint for the given tenant at Azure Active Directory.
|
||||
// If tenant is empty, it uses the tenant called `common`.
|
||||
//
|
||||
@ -226,18 +389,29 @@ func AzureAD(tenant string) oauth2.Endpoint {
|
||||
if tenant == "" {
|
||||
tenant = "common"
|
||||
}
|
||||
u := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "login.microsoftonline.com",
|
||||
}
|
||||
return oauth2.Endpoint{
|
||||
AuthURL: "https://login.microsoftonline.com/" + tenant + "/oauth2/v2.0/authorize",
|
||||
TokenURL: "https://login.microsoftonline.com/" + tenant + "/oauth2/v2.0/token",
|
||||
AuthURL: u.JoinPath(tenant, "/oauth2/v2.0/authorize").String(),
|
||||
TokenURL: u.JoinPath(tenant, "/oauth2/v2.0/token").String(),
|
||||
DeviceAuthURL: u.JoinPath(tenant, "/oauth2/v2.0/devicecode").String(),
|
||||
}
|
||||
}
|
||||
|
||||
// HipChatServer returns a new oauth2.Endpoint for a HipChat Server instance
|
||||
// running on the given domain or host.
|
||||
func HipChatServer(host string) oauth2.Endpoint {
|
||||
// AzureADB2CEndpoint returns a new oauth2.Endpoint for the given tenant and policy at Azure Active Directory B2C.
|
||||
// policy is the Azure B2C User flow name Example: `B2C_1_SignUpSignIn`.
|
||||
//
|
||||
// Documentation: https://docs.microsoft.com/en-us/azure/active-directory-b2c/tokens-overview#endpoints
|
||||
func AzureADB2CEndpoint(tenant string, policy string) oauth2.Endpoint {
|
||||
u := url.URL{
|
||||
Scheme: "https",
|
||||
Host: tenant + ".b2clogin.com",
|
||||
}
|
||||
return oauth2.Endpoint{
|
||||
AuthURL: "https://" + host + "/users/authorize",
|
||||
TokenURL: "https://" + host + "/v2/oauth/token",
|
||||
AuthURL: u.JoinPath(tenant+".onmicrosoft.com", policy, "/oauth2/v2.0/authorize").String(),
|
||||
TokenURL: u.JoinPath(tenant+".onmicrosoft.com", policy, "/oauth2/v2.0/token").String(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -250,9 +424,42 @@ func HipChatServer(host string) oauth2.Endpoint {
|
||||
// https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-assign-domain.html
|
||||
// https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-userpools-server-contract-reference.html
|
||||
func AWSCognito(domain string) oauth2.Endpoint {
|
||||
domain = strings.TrimRight(domain, "/")
|
||||
u, err := url.Parse(domain)
|
||||
if err != nil || u.Scheme == "" || u.Host == "" {
|
||||
panic("endpoints: invalid domain" + domain)
|
||||
}
|
||||
return oauth2.Endpoint{
|
||||
AuthURL: domain + "/oauth2/authorize",
|
||||
TokenURL: domain + "/oauth2/token",
|
||||
AuthURL: u.JoinPath("/oauth2/authorize").String(),
|
||||
TokenURL: u.JoinPath("/oauth2/token").String(),
|
||||
}
|
||||
}
|
||||
|
||||
// HipChatServer returns a new oauth2.Endpoint for a HipChat Server instance.
|
||||
// host should be a hostname, without any scheme prefix.
|
||||
//
|
||||
// Documentation: https://developer.atlassian.com/server/hipchat/hipchat-rest-api-access-tokens/
|
||||
func HipChatServer(host string) oauth2.Endpoint {
|
||||
u := url.URL{
|
||||
Scheme: "https",
|
||||
Host: host,
|
||||
}
|
||||
return oauth2.Endpoint{
|
||||
AuthURL: u.JoinPath("/users/authorize").String(),
|
||||
TokenURL: u.JoinPath("/v2/oauth/token").String(),
|
||||
}
|
||||
}
|
||||
|
||||
// Shopify returns a new oauth2.Endpoint for the supplied shop domain name.
|
||||
// host should be a hostname, without any scheme prefix.
|
||||
//
|
||||
// Documentation: https://shopify.dev/docs/apps/auth/oauth
|
||||
func Shopify(host string) oauth2.Endpoint {
|
||||
u := url.URL{
|
||||
Scheme: "https",
|
||||
Host: host,
|
||||
}
|
||||
return oauth2.Endpoint{
|
||||
AuthURL: u.JoinPath("/admin/oauth/authorize").String(),
|
||||
TokenURL: u.JoinPath("/admin/oauth/access_token").String(),
|
||||
}
|
||||
}
|
||||
|
@ -6,11 +6,8 @@
|
||||
package gitlab // import "golang.org/x/oauth2/gitlab"
|
||||
|
||||
import (
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/endpoints"
|
||||
)
|
||||
|
||||
// Endpoint is GitLab's OAuth 2.0 endpoint.
|
||||
var Endpoint = oauth2.Endpoint{
|
||||
AuthURL: "https://gitlab.com/oauth/authorize",
|
||||
TokenURL: "https://gitlab.com/oauth/token",
|
||||
}
|
||||
var Endpoint = endpoints.GitLab
|
||||
|
15
go.mod
15
go.mod
@ -1,16 +1,5 @@
|
||||
module golang.org/x/oauth2
|
||||
|
||||
go 1.18
|
||||
go 1.23.0
|
||||
|
||||
require (
|
||||
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 (
|
||||
cloud.google.com/go/compute v1.20.1 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
golang.org/x/net v0.19.0 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
)
|
||||
require cloud.google.com/go/compute/metadata v0.3.0
|
||||
|
28
go.sum
28
go.sum
@ -1,26 +1,2 @@
|
||||
cloud.google.com/go/compute v1.20.1 h1:6aKEtlUiwEpJzM001l0yFkpXmUVXaN8W+fbkb2AZNbg=
|
||||
cloud.google.com/go/compute v1.20.1/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM=
|
||||
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
|
||||
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
|
||||
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc=
|
||||
cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
|
||||
|
@ -6,16 +6,13 @@ package google
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
"log"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// Set at init time by appengine_gen1.go. If nil, we're not on App Engine standard first generation (<= Go 1.9) or App Engine flexible.
|
||||
var appengineTokenFunc func(c context.Context, scopes ...string) (token string, expiry time.Time, err error)
|
||||
|
||||
// Set at init time by appengine_gen1.go. If nil, we're not on App Engine standard first generation (<= Go 1.9) or App Engine flexible.
|
||||
var appengineAppIDFunc func(c context.Context) string
|
||||
var logOnce sync.Once // only spam about deprecation once
|
||||
|
||||
// AppEngineTokenSource returns a token source that fetches tokens from either
|
||||
// the current application's service account or from the metadata server,
|
||||
@ -23,8 +20,10 @@ var appengineAppIDFunc func(c context.Context) string
|
||||
// details. If you are implementing a 3-legged OAuth 2.0 flow on App Engine that
|
||||
// involves user accounts, see oauth2.Config instead.
|
||||
//
|
||||
// First generation App Engine runtimes (<= Go 1.9):
|
||||
// AppEngineTokenSource returns a token source that fetches tokens issued to the
|
||||
// The current version of this library requires at least Go 1.17 to build,
|
||||
// so first generation App Engine runtimes (<= Go 1.9) are unsupported.
|
||||
// Previously, on first generation App Engine runtimes, AppEngineTokenSource
|
||||
// returned a token source that fetches tokens issued to the
|
||||
// current App Engine application's service account. The provided context must have
|
||||
// come from appengine.NewContext.
|
||||
//
|
||||
@ -34,5 +33,8 @@ var appengineAppIDFunc func(c context.Context) string
|
||||
// context and scopes are not used. Please use DefaultTokenSource (or ComputeTokenSource,
|
||||
// which DefaultTokenSource will use in this case) instead.
|
||||
func AppEngineTokenSource(ctx context.Context, scope ...string) oauth2.TokenSource {
|
||||
return appEngineTokenSource(ctx, scope...)
|
||||
logOnce.Do(func() {
|
||||
log.Print("google: AppEngineTokenSource is deprecated on App Engine standard second generation runtimes (>= Go 1.11) and App Engine flexible. Please use DefaultTokenSource or ComputeTokenSource.")
|
||||
})
|
||||
return ComputeTokenSource("")
|
||||
}
|
||||
|
@ -1,77 +0,0 @@
|
||||
// Copyright 2018 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build appengine
|
||||
|
||||
// This file applies to App Engine first generation runtimes (<= Go 1.9).
|
||||
|
||||
package google
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
"google.golang.org/appengine"
|
||||
)
|
||||
|
||||
func init() {
|
||||
appengineTokenFunc = appengine.AccessToken
|
||||
appengineAppIDFunc = appengine.AppID
|
||||
}
|
||||
|
||||
// See comment on AppEngineTokenSource in appengine.go.
|
||||
func appEngineTokenSource(ctx context.Context, scope ...string) oauth2.TokenSource {
|
||||
scopes := append([]string{}, scope...)
|
||||
sort.Strings(scopes)
|
||||
return &gaeTokenSource{
|
||||
ctx: ctx,
|
||||
scopes: scopes,
|
||||
key: strings.Join(scopes, " "),
|
||||
}
|
||||
}
|
||||
|
||||
// aeTokens helps the fetched tokens to be reused until their expiration.
|
||||
var (
|
||||
aeTokensMu sync.Mutex
|
||||
aeTokens = make(map[string]*tokenLock) // key is space-separated scopes
|
||||
)
|
||||
|
||||
type tokenLock struct {
|
||||
mu sync.Mutex // guards t; held while fetching or updating t
|
||||
t *oauth2.Token
|
||||
}
|
||||
|
||||
type gaeTokenSource struct {
|
||||
ctx context.Context
|
||||
scopes []string
|
||||
key string // to aeTokens map; space-separated scopes
|
||||
}
|
||||
|
||||
func (ts *gaeTokenSource) Token() (*oauth2.Token, error) {
|
||||
aeTokensMu.Lock()
|
||||
tok, ok := aeTokens[ts.key]
|
||||
if !ok {
|
||||
tok = &tokenLock{}
|
||||
aeTokens[ts.key] = tok
|
||||
}
|
||||
aeTokensMu.Unlock()
|
||||
|
||||
tok.mu.Lock()
|
||||
defer tok.mu.Unlock()
|
||||
if tok.t.Valid() {
|
||||
return tok.t, nil
|
||||
}
|
||||
access, exp, err := appengineTokenFunc(ts.ctx, ts.scopes...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tok.t = &oauth2.Token{
|
||||
AccessToken: access,
|
||||
Expiry: exp,
|
||||
}
|
||||
return tok.t, nil
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
// Copyright 2018 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !appengine
|
||||
|
||||
// This file applies to App Engine second generation runtimes (>= Go 1.11) and App Engine flexible.
|
||||
|
||||
package google
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
var logOnce sync.Once // only spam about deprecation once
|
||||
|
||||
// See comment on AppEngineTokenSource in appengine.go.
|
||||
func appEngineTokenSource(ctx context.Context, scope ...string) oauth2.TokenSource {
|
||||
logOnce.Do(func() {
|
||||
log.Print("google: AppEngineTokenSource is deprecated on App Engine standard second generation runtimes (>= Go 1.11) and App Engine flexible. Please use DefaultTokenSource or ComputeTokenSource.")
|
||||
})
|
||||
return ComputeTokenSource("")
|
||||
}
|
@ -12,6 +12,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"cloud.google.com/go/compute/metadata"
|
||||
@ -21,7 +22,7 @@ import (
|
||||
|
||||
const (
|
||||
adcSetupURL = "https://cloud.google.com/docs/authentication/external/set-up-adc"
|
||||
universeDomainDefault = "googleapis.com"
|
||||
defaultUniverseDomain = "googleapis.com"
|
||||
)
|
||||
|
||||
// Credentials holds Google credentials, including "Application Default Credentials".
|
||||
@ -41,19 +42,65 @@ type Credentials struct {
|
||||
// running on Google Cloud Platform.
|
||||
JSON []byte
|
||||
|
||||
// UniverseDomainProvider returns the default service domain for a given
|
||||
// Cloud universe. Optional.
|
||||
//
|
||||
// On GCE, UniverseDomainProvider should return the universe domain value
|
||||
// from Google Compute Engine (GCE)'s metadata server. See also [The attached service
|
||||
// account](https://cloud.google.com/docs/authentication/application-default-credentials#attached-sa).
|
||||
// If the GCE metadata server returns a 404 error, the default universe
|
||||
// domain value should be returned. If the GCE metadata server returns an
|
||||
// error other than 404, the error should be returned.
|
||||
UniverseDomainProvider func() (string, error)
|
||||
|
||||
udMu sync.Mutex // guards universeDomain
|
||||
// universeDomain is the default service domain for a given Cloud universe.
|
||||
universeDomain string
|
||||
}
|
||||
|
||||
// UniverseDomain returns the default service domain for a given Cloud universe.
|
||||
//
|
||||
// The default value is "googleapis.com".
|
||||
//
|
||||
// Deprecated: Use instead (*Credentials).GetUniverseDomain(), which supports
|
||||
// obtaining the universe domain when authenticating via the GCE metadata server.
|
||||
// Unlike GetUniverseDomain, this method, UniverseDomain, will always return the
|
||||
// default value when authenticating via the GCE metadata server.
|
||||
// See also [The attached service account](https://cloud.google.com/docs/authentication/application-default-credentials#attached-sa).
|
||||
func (c *Credentials) UniverseDomain() string {
|
||||
if c.universeDomain == "" {
|
||||
return universeDomainDefault
|
||||
return defaultUniverseDomain
|
||||
}
|
||||
return c.universeDomain
|
||||
}
|
||||
|
||||
// GetUniverseDomain returns the default service domain for a given Cloud
|
||||
// universe. If present, UniverseDomainProvider will be invoked and its return
|
||||
// value will be cached.
|
||||
//
|
||||
// The default value is "googleapis.com".
|
||||
func (c *Credentials) GetUniverseDomain() (string, error) {
|
||||
c.udMu.Lock()
|
||||
defer c.udMu.Unlock()
|
||||
if c.universeDomain == "" && c.UniverseDomainProvider != nil {
|
||||
// On Google Compute Engine, an App Engine standard second generation
|
||||
// runtime, or App Engine flexible, use an externally provided function
|
||||
// to request the universe domain from the metadata server.
|
||||
ud, err := c.UniverseDomainProvider()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
c.universeDomain = ud
|
||||
}
|
||||
// If no UniverseDomainProvider (meaning not on Google Compute Engine), or
|
||||
// in case of any (non-error) empty return value from
|
||||
// UniverseDomainProvider, set the default universe domain.
|
||||
if c.universeDomain == "" {
|
||||
c.universeDomain = defaultUniverseDomain
|
||||
}
|
||||
return c.universeDomain, nil
|
||||
}
|
||||
|
||||
// DefaultCredentials is the old name of Credentials.
|
||||
//
|
||||
// Deprecated: use Credentials instead.
|
||||
@ -91,6 +138,12 @@ type CredentialsParams struct {
|
||||
// Note: This option is currently only respected when using credentials
|
||||
// fetched from the GCE metadata server.
|
||||
EarlyTokenRefresh time.Duration
|
||||
|
||||
// UniverseDomain is the default service domain for a given Cloud universe.
|
||||
// Only supported in authentication flows that support universe domains.
|
||||
// This value takes precedence over a universe domain explicitly specified
|
||||
// in a credentials config file or by the GCE metadata server. Optional.
|
||||
UniverseDomain string
|
||||
}
|
||||
|
||||
func (params CredentialsParams) deepCopy() CredentialsParams {
|
||||
@ -135,9 +188,7 @@ func DefaultTokenSource(ctx context.Context, scope ...string) (oauth2.TokenSourc
|
||||
// 2. A JSON file in a location known to the gcloud command-line tool.
|
||||
// On Windows, this is %APPDATA%/gcloud/application_default_credentials.json.
|
||||
// On other systems, $HOME/.config/gcloud/application_default_credentials.json.
|
||||
// 3. On Google App Engine standard first generation runtimes (<= Go 1.9) it uses
|
||||
// the appengine.AccessToken function.
|
||||
// 4. On Google Compute Engine, Google App Engine standard second generation runtimes
|
||||
// 3. On Google Compute Engine, Google App Engine standard second generation runtimes
|
||||
// (>= Go 1.11), and Google App Engine flexible environment, it fetches
|
||||
// credentials from the metadata server.
|
||||
func FindDefaultCredentialsWithParams(ctx context.Context, params CredentialsParams) (*Credentials, error) {
|
||||
@ -160,23 +211,27 @@ func FindDefaultCredentialsWithParams(ctx context.Context, params CredentialsPar
|
||||
return CredentialsFromJSONWithParams(ctx, b, params)
|
||||
}
|
||||
|
||||
// Third, if we're on a Google App Engine standard first generation runtime (<= Go 1.9)
|
||||
// use those credentials. App Engine standard second generation runtimes (>= Go 1.11)
|
||||
// and App Engine flexible use ComputeTokenSource and the metadata server.
|
||||
if appengineTokenFunc != nil {
|
||||
return &Credentials{
|
||||
ProjectID: appengineAppIDFunc(ctx),
|
||||
TokenSource: AppEngineTokenSource(ctx, params.Scopes...),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Fourth, if we're on Google Compute Engine, an App Engine standard second generation runtime,
|
||||
// Third, if we're on Google Compute Engine, an App Engine standard second generation runtime,
|
||||
// or App Engine flexible, use the metadata server.
|
||||
if metadata.OnGCE() {
|
||||
id, _ := metadata.ProjectID()
|
||||
universeDomainProvider := func() (string, error) {
|
||||
universeDomain, err := metadata.Get("universe/universe_domain")
|
||||
if err != nil {
|
||||
if _, ok := err.(metadata.NotDefinedError); ok {
|
||||
// http.StatusNotFound (404)
|
||||
return defaultUniverseDomain, nil
|
||||
} else {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
return universeDomain, nil
|
||||
}
|
||||
return &Credentials{
|
||||
ProjectID: id,
|
||||
TokenSource: computeTokenSource("", params.EarlyTokenRefresh, params.Scopes...),
|
||||
ProjectID: id,
|
||||
TokenSource: computeTokenSource("", params.EarlyTokenRefresh, params.Scopes...),
|
||||
UniverseDomainProvider: universeDomainProvider,
|
||||
universeDomain: params.UniverseDomain,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -196,6 +251,12 @@ func FindDefaultCredentials(ctx context.Context, scopes ...string) (*Credentials
|
||||
// a Google Developers service account key file, a gcloud user credentials file (a.k.a. refresh
|
||||
// token JSON), or the JSON configuration file for workload identity federation in non-Google cloud
|
||||
// platforms (see https://cloud.google.com/iam/docs/how-to#using-workload-identity-federation).
|
||||
//
|
||||
// Important: If you accept a credential configuration (credential JSON/File/Stream) from an
|
||||
// external source for authentication to Google Cloud Platform, you must validate it before
|
||||
// providing it to any Google API or library. Providing an unvalidated credential configuration to
|
||||
// Google APIs can compromise the security of your systems and data. For more information, refer to
|
||||
// [Validate credential configurations from external sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials).
|
||||
func CredentialsFromJSONWithParams(ctx context.Context, jsonData []byte, params CredentialsParams) (*Credentials, error) {
|
||||
// Make defensive copy of the slices in params.
|
||||
params = params.deepCopy()
|
||||
@ -217,9 +278,12 @@ func CredentialsFromJSONWithParams(ctx context.Context, jsonData []byte, params
|
||||
}
|
||||
|
||||
universeDomain := f.UniverseDomain
|
||||
if params.UniverseDomain != "" {
|
||||
universeDomain = params.UniverseDomain
|
||||
}
|
||||
// Authorized user credentials are only supported in the googleapis.com universe.
|
||||
if f.Type == userCredentialsKey {
|
||||
universeDomain = universeDomainDefault
|
||||
universeDomain = defaultUniverseDomain
|
||||
}
|
||||
|
||||
ts, err := f.tokenSource(ctx, params)
|
||||
@ -236,6 +300,12 @@ func CredentialsFromJSONWithParams(ctx context.Context, jsonData []byte, params
|
||||
}
|
||||
|
||||
// CredentialsFromJSON invokes CredentialsFromJSONWithParams with the specified scopes.
|
||||
//
|
||||
// Important: If you accept a credential configuration (credential JSON/File/Stream) from an
|
||||
// external source for authentication to Google Cloud Platform, you must validate it before
|
||||
// providing it to any Google API or library. Providing an unvalidated credential configuration to
|
||||
// Google APIs can compromise the security of your systems and data. For more information, refer to
|
||||
// [Validate credential configurations from external sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials).
|
||||
func CredentialsFromJSON(ctx context.Context, jsonData []byte, scopes ...string) (*Credentials, error) {
|
||||
var params CredentialsParams
|
||||
params.Scopes = scopes
|
||||
|
@ -6,7 +6,12 @@ package google
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"cloud.google.com/go/compute/metadata"
|
||||
)
|
||||
|
||||
var saJSONJWT = []byte(`{
|
||||
@ -53,6 +58,10 @@ var userJSONUniverseDomain = []byte(`{
|
||||
"universe_domain": "example.com"
|
||||
}`)
|
||||
|
||||
var universeDomain = "example.com"
|
||||
|
||||
var universeDomain2 = "apis-tpclp.goog"
|
||||
|
||||
func TestCredentialsFromJSONWithParams_SA(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
scope := "https://www.googleapis.com/auth/cloud-platform"
|
||||
@ -70,6 +79,32 @@ func TestCredentialsFromJSONWithParams_SA(t *testing.T) {
|
||||
if want := "googleapis.com"; creds.UniverseDomain() != want {
|
||||
t.Fatalf("got %q, want %q", creds.UniverseDomain(), want)
|
||||
}
|
||||
if want := "googleapis.com"; creds.UniverseDomain() != want {
|
||||
t.Fatalf("got %q, want %q", creds.UniverseDomain(), want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCredentialsFromJSONWithParams_SA_Params_UniverseDomain(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
scope := "https://www.googleapis.com/auth/cloud-platform"
|
||||
params := CredentialsParams{
|
||||
Scopes: []string{scope},
|
||||
UniverseDomain: universeDomain2,
|
||||
}
|
||||
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 creds.UniverseDomain() != universeDomain2 {
|
||||
t.Fatalf("got %q, want %q", creds.UniverseDomain(), universeDomain2)
|
||||
}
|
||||
if creds.UniverseDomain() != universeDomain2 {
|
||||
t.Fatalf("got %q, want %q", creds.UniverseDomain(), universeDomain2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCredentialsFromJSONWithParams_SA_UniverseDomain(t *testing.T) {
|
||||
@ -86,8 +121,42 @@ func TestCredentialsFromJSONWithParams_SA_UniverseDomain(t *testing.T) {
|
||||
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)
|
||||
if creds.UniverseDomain() != universeDomain {
|
||||
t.Fatalf("got %q, want %q", creds.UniverseDomain(), universeDomain)
|
||||
}
|
||||
got, err := creds.GetUniverseDomain()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != universeDomain {
|
||||
t.Fatalf("got %q, want %q", got, universeDomain)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCredentialsFromJSONWithParams_SA_UniverseDomain_Params_UniverseDomain(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
scope := "https://www.googleapis.com/auth/cloud-platform"
|
||||
params := CredentialsParams{
|
||||
Scopes: []string{scope},
|
||||
UniverseDomain: universeDomain2,
|
||||
}
|
||||
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 creds.UniverseDomain() != universeDomain2 {
|
||||
t.Fatalf("got %q, want %q", creds.UniverseDomain(), universeDomain2)
|
||||
}
|
||||
got, err := creds.GetUniverseDomain()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != universeDomain2 {
|
||||
t.Fatalf("got %q, want %q", got, universeDomain2)
|
||||
}
|
||||
}
|
||||
|
||||
@ -105,6 +174,37 @@ func TestCredentialsFromJSONWithParams_User(t *testing.T) {
|
||||
if want := "googleapis.com"; creds.UniverseDomain() != want {
|
||||
t.Fatalf("got %q, want %q", creds.UniverseDomain(), want)
|
||||
}
|
||||
got, err := creds.GetUniverseDomain()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if want := "googleapis.com"; got != want {
|
||||
t.Fatalf("got %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCredentialsFromJSONWithParams_User_Params_UniverseDomain(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
scope := "https://www.googleapis.com/auth/cloud-platform"
|
||||
params := CredentialsParams{
|
||||
Scopes: []string{scope},
|
||||
UniverseDomain: universeDomain2,
|
||||
}
|
||||
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)
|
||||
}
|
||||
got, err := creds.GetUniverseDomain()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if want := "googleapis.com"; got != want {
|
||||
t.Fatalf("got %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCredentialsFromJSONWithParams_User_UniverseDomain(t *testing.T) {
|
||||
@ -121,4 +221,92 @@ func TestCredentialsFromJSONWithParams_User_UniverseDomain(t *testing.T) {
|
||||
if want := "googleapis.com"; creds.UniverseDomain() != want {
|
||||
t.Fatalf("got %q, want %q", creds.UniverseDomain(), want)
|
||||
}
|
||||
got, err := creds.GetUniverseDomain()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if want := "googleapis.com"; got != want {
|
||||
t.Fatalf("got %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCredentialsFromJSONWithParams_User_UniverseDomain_Params_UniverseDomain(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
scope := "https://www.googleapis.com/auth/cloud-platform"
|
||||
params := CredentialsParams{
|
||||
Scopes: []string{scope},
|
||||
UniverseDomain: universeDomain2,
|
||||
}
|
||||
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)
|
||||
}
|
||||
got, err := creds.GetUniverseDomain()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if want := "googleapis.com"; got != want {
|
||||
t.Fatalf("got %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestComputeUniverseDomain(t *testing.T) {
|
||||
universeDomainPath := "/computeMetadata/v1/universe/universe_domain"
|
||||
universeDomainResponseBody := "example.com"
|
||||
var requests int
|
||||
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
requests++
|
||||
if r.URL.Path != universeDomainPath {
|
||||
t.Errorf("bad path, got %s, want %s", r.URL.Path, universeDomainPath)
|
||||
}
|
||||
if requests > 1 {
|
||||
t.Errorf("too many requests, got %d, want 1", requests)
|
||||
}
|
||||
w.Write([]byte(universeDomainResponseBody))
|
||||
}))
|
||||
defer s.Close()
|
||||
t.Setenv("GCE_METADATA_HOST", strings.TrimPrefix(s.URL, "http://"))
|
||||
|
||||
scope := "https://www.googleapis.com/auth/cloud-platform"
|
||||
params := CredentialsParams{
|
||||
Scopes: []string{scope},
|
||||
}
|
||||
universeDomainProvider := func() (string, error) {
|
||||
universeDomain, err := metadata.Get("universe/universe_domain")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return universeDomain, nil
|
||||
}
|
||||
// Copied from FindDefaultCredentialsWithParams, metadata.OnGCE() = true block
|
||||
creds := &Credentials{
|
||||
ProjectID: "fake_project",
|
||||
TokenSource: computeTokenSource("", params.EarlyTokenRefresh, params.Scopes...),
|
||||
UniverseDomainProvider: universeDomainProvider,
|
||||
universeDomain: params.UniverseDomain, // empty
|
||||
}
|
||||
c := make(chan bool)
|
||||
go func() {
|
||||
got, err := creds.GetUniverseDomain() // First conflicting access.
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if want := universeDomainResponseBody; got != want {
|
||||
t.Errorf("got %q, want %q", got, want)
|
||||
}
|
||||
c <- true
|
||||
}()
|
||||
got, err := creds.GetUniverseDomain() // Second conflicting (and potentially uncached) access.
|
||||
<-c
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if want := universeDomainResponseBody; got != want {
|
||||
t.Errorf("got %q, want %q", got, want)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -22,91 +22,9 @@
|
||||
// the other by JWTConfigFromJSON. The returned Config can be used to obtain a TokenSource or
|
||||
// create an http.Client.
|
||||
//
|
||||
// # Workload Identity Federation
|
||||
// # Workload and Workforce Identity Federation
|
||||
//
|
||||
// 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) 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.
|
||||
// This lets you access Google Cloud resources directly, eliminating the
|
||||
// maintenance and security burden associated with service account keys.
|
||||
//
|
||||
// Follow the detailed instructions on how to configure Workload Identity Federation
|
||||
// in various platforms:
|
||||
//
|
||||
// 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/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/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
|
||||
//
|
||||
// # Security considerations
|
||||
//
|
||||
// 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.
|
||||
// For information on how to use Workload and Workforce Identity Federation, see [golang.org/x/oauth2/google/externalaccount].
|
||||
//
|
||||
// # Credentials
|
||||
//
|
||||
|
@ -39,16 +39,19 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
var (
|
||||
identityBindingEndpoint = "https://sts.googleapis.com/v1/token"
|
||||
const (
|
||||
universeDomainPlaceholder = "UNIVERSE_DOMAIN"
|
||||
identityBindingEndpointTemplate = "https://sts.UNIVERSE_DOMAIN/v1/token"
|
||||
defaultUniverseDomain = "googleapis.com"
|
||||
)
|
||||
|
||||
type accessBoundary struct {
|
||||
@ -105,6 +108,18 @@ type DownscopingConfig struct {
|
||||
// access (or set of accesses) that the new token has to a given resource.
|
||||
// There can be a maximum of 10 AccessBoundaryRules.
|
||||
Rules []AccessBoundaryRule
|
||||
// UniverseDomain is the default service domain for a given Cloud universe.
|
||||
// The default value is "googleapis.com". Optional.
|
||||
UniverseDomain string
|
||||
}
|
||||
|
||||
// identityBindingEndpoint returns the identity binding endpoint with the
|
||||
// configured universe domain.
|
||||
func (dc *DownscopingConfig) identityBindingEndpoint() string {
|
||||
if dc.UniverseDomain == "" {
|
||||
return strings.Replace(identityBindingEndpointTemplate, universeDomainPlaceholder, defaultUniverseDomain, 1)
|
||||
}
|
||||
return strings.Replace(identityBindingEndpointTemplate, universeDomainPlaceholder, dc.UniverseDomain, 1)
|
||||
}
|
||||
|
||||
// A downscopingTokenSource is used to retrieve a downscoped token with restricted
|
||||
@ -114,6 +129,9 @@ type downscopingTokenSource struct {
|
||||
ctx context.Context
|
||||
// config holds the information necessary to generate a downscoped Token.
|
||||
config DownscopingConfig
|
||||
// identityBindingEndpoint is the identity binding endpoint with the
|
||||
// configured universe domain.
|
||||
identityBindingEndpoint string
|
||||
}
|
||||
|
||||
// NewTokenSource returns a configured downscopingTokenSource.
|
||||
@ -135,7 +153,11 @@ func NewTokenSource(ctx context.Context, conf DownscopingConfig) (oauth2.TokenSo
|
||||
return nil, fmt.Errorf("downscope: all rules must provide at least one permission: %+v", val)
|
||||
}
|
||||
}
|
||||
return downscopingTokenSource{ctx: ctx, config: conf}, nil
|
||||
return downscopingTokenSource{
|
||||
ctx: ctx,
|
||||
config: conf,
|
||||
identityBindingEndpoint: conf.identityBindingEndpoint(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Token() uses a downscopingTokenSource to generate an oauth2 Token.
|
||||
@ -171,12 +193,12 @@ func (dts downscopingTokenSource) Token() (*oauth2.Token, error) {
|
||||
form.Add("options", string(b))
|
||||
|
||||
myClient := oauth2.NewClient(dts.ctx, nil)
|
||||
resp, err := myClient.PostForm(identityBindingEndpoint, form)
|
||||
resp, err := myClient.PostForm(dts.identityBindingEndpoint, form)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to generate POST Request %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
respBody, err := ioutil.ReadAll(resp.Body)
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("downscope: unable to read response body: %v", err)
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ package downscope
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
@ -27,7 +27,7 @@ func Test_DownscopedTokenSource(t *testing.T) {
|
||||
if r.URL.String() != "/" {
|
||||
t.Errorf("Unexpected request URL, %v is found", r.URL)
|
||||
}
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read request body: %v", err)
|
||||
}
|
||||
@ -38,18 +38,43 @@ func Test_DownscopedTokenSource(t *testing.T) {
|
||||
w.Write([]byte(standardRespBody))
|
||||
|
||||
}))
|
||||
new := []AccessBoundaryRule{
|
||||
myTok := oauth2.Token{AccessToken: "Mellon"}
|
||||
tmpSrc := oauth2.StaticTokenSource(&myTok)
|
||||
rules := []AccessBoundaryRule{
|
||||
{
|
||||
AvailableResource: "test1",
|
||||
AvailablePermissions: []string{"Perm1", "Perm2"},
|
||||
},
|
||||
}
|
||||
myTok := oauth2.Token{AccessToken: "Mellon"}
|
||||
tmpSrc := oauth2.StaticTokenSource(&myTok)
|
||||
dts := downscopingTokenSource{context.Background(), DownscopingConfig{tmpSrc, new}}
|
||||
identityBindingEndpoint = ts.URL
|
||||
dts := downscopingTokenSource{
|
||||
ctx: context.Background(),
|
||||
config: DownscopingConfig{
|
||||
RootSource: tmpSrc,
|
||||
Rules: rules,
|
||||
},
|
||||
identityBindingEndpoint: ts.URL,
|
||||
}
|
||||
_, err := dts.Token()
|
||||
if err != nil {
|
||||
t.Fatalf("NewDownscopedTokenSource failed with error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_DownscopingConfig(t *testing.T) {
|
||||
tests := []struct {
|
||||
universeDomain string
|
||||
want string
|
||||
}{
|
||||
{"", "https://sts.googleapis.com/v1/token"},
|
||||
{"googleapis.com", "https://sts.googleapis.com/v1/token"},
|
||||
{"example.com", "https://sts.example.com/v1/token"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
c := DownscopingConfig{
|
||||
UniverseDomain: tt.universeDomain,
|
||||
}
|
||||
if got := c.identityBindingEndpoint(); got != tt.want {
|
||||
t.Errorf("got %q, want %q", got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,9 +7,9 @@ package google_test
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/google"
|
||||
@ -60,7 +60,7 @@ func ExampleJWTConfigFromJSON() {
|
||||
// To create a service account client, click "Create new Client ID",
|
||||
// select "Service Account", and click "Create Client ID". A JSON
|
||||
// key file will then be downloaded to your computer.
|
||||
data, err := ioutil.ReadFile("/path/to/your-project-key.json")
|
||||
data, err := os.ReadFile("/path/to/your-project-key.json")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@ -136,7 +136,7 @@ func ExampleComputeTokenSource() {
|
||||
|
||||
func ExampleCredentialsFromJSON() {
|
||||
ctx := context.Background()
|
||||
data, err := ioutil.ReadFile("/path/to/key-file.json")
|
||||
data, err := os.ReadFile("/path/to/key-file.json")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
@ -14,7 +14,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
@ -26,22 +25,28 @@ import (
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
type awsSecurityCredentials struct {
|
||||
AccessKeyID string `json:"AccessKeyID"`
|
||||
// AwsSecurityCredentials models AWS security credentials.
|
||||
type AwsSecurityCredentials struct {
|
||||
// AccessKeyID is the AWS Access Key ID - Required.
|
||||
AccessKeyID string `json:"AccessKeyID"`
|
||||
// SecretAccessKey is the AWS Secret Access Key - Required.
|
||||
SecretAccessKey string `json:"SecretAccessKey"`
|
||||
SecurityToken string `json:"Token"`
|
||||
// SessionToken is the AWS Session token. This should be provided for temporary AWS security credentials - Optional.
|
||||
SessionToken string `json:"Token"`
|
||||
}
|
||||
|
||||
// awsRequestSigner is a utility class to sign http requests using a AWS V4 signature.
|
||||
type awsRequestSigner struct {
|
||||
RegionName string
|
||||
AwsSecurityCredentials awsSecurityCredentials
|
||||
AwsSecurityCredentials *AwsSecurityCredentials
|
||||
}
|
||||
|
||||
// getenv aliases os.Getenv for testing
|
||||
var getenv = os.Getenv
|
||||
|
||||
const (
|
||||
defaultRegionalCredentialVerificationUrl = "https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15"
|
||||
|
||||
// AWS Signature Version 4 signing algorithm identifier.
|
||||
awsAlgorithm = "AWS4-HMAC-SHA256"
|
||||
|
||||
@ -164,7 +169,7 @@ func requestDataHash(req *http.Request) (string, error) {
|
||||
}
|
||||
defer requestBody.Close()
|
||||
|
||||
requestData, err = ioutil.ReadAll(io.LimitReader(requestBody, 1<<20))
|
||||
requestData, err = io.ReadAll(io.LimitReader(requestBody, 1<<20))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@ -197,8 +202,8 @@ func (rs *awsRequestSigner) SignRequest(req *http.Request) error {
|
||||
|
||||
signedRequest.Header.Add("host", requestHost(req))
|
||||
|
||||
if rs.AwsSecurityCredentials.SecurityToken != "" {
|
||||
signedRequest.Header.Add(awsSecurityTokenHeader, rs.AwsSecurityCredentials.SecurityToken)
|
||||
if rs.AwsSecurityCredentials.SessionToken != "" {
|
||||
signedRequest.Header.Add(awsSecurityTokenHeader, rs.AwsSecurityCredentials.SessionToken)
|
||||
}
|
||||
|
||||
if signedRequest.Header.Get("date") == "" {
|
||||
@ -251,16 +256,18 @@ func (rs *awsRequestSigner) generateAuthentication(req *http.Request, timestamp
|
||||
}
|
||||
|
||||
type awsCredentialSource struct {
|
||||
EnvironmentID string
|
||||
RegionURL string
|
||||
RegionalCredVerificationURL string
|
||||
CredVerificationURL string
|
||||
IMDSv2SessionTokenURL string
|
||||
TargetResource string
|
||||
requestSigner *awsRequestSigner
|
||||
region string
|
||||
ctx context.Context
|
||||
client *http.Client
|
||||
environmentID string
|
||||
regionURL string
|
||||
regionalCredVerificationURL string
|
||||
credVerificationURL string
|
||||
imdsv2SessionTokenURL string
|
||||
targetResource string
|
||||
requestSigner *awsRequestSigner
|
||||
region string
|
||||
ctx context.Context
|
||||
client *http.Client
|
||||
awsSecurityCredentialsSupplier AwsSecurityCredentialsSupplier
|
||||
supplierOptions SupplierOptions
|
||||
}
|
||||
|
||||
type awsRequestHeader struct {
|
||||
@ -292,18 +299,25 @@ func canRetrieveSecurityCredentialFromEnvironment() bool {
|
||||
return getenv(awsAccessKeyId) != "" && getenv(awsSecretAccessKey) != ""
|
||||
}
|
||||
|
||||
func shouldUseMetadataServer() bool {
|
||||
return !canRetrieveRegionFromEnvironment() || !canRetrieveSecurityCredentialFromEnvironment()
|
||||
func (cs awsCredentialSource) shouldUseMetadataServer() bool {
|
||||
return cs.awsSecurityCredentialsSupplier == nil && (!canRetrieveRegionFromEnvironment() || !canRetrieveSecurityCredentialFromEnvironment())
|
||||
}
|
||||
|
||||
func (cs awsCredentialSource) credentialSourceType() string {
|
||||
if cs.awsSecurityCredentialsSupplier != nil {
|
||||
return "programmatic"
|
||||
}
|
||||
return "aws"
|
||||
}
|
||||
|
||||
func (cs awsCredentialSource) subjectToken() (string, error) {
|
||||
// Set Defaults
|
||||
if cs.regionalCredVerificationURL == "" {
|
||||
cs.regionalCredVerificationURL = defaultRegionalCredentialVerificationUrl
|
||||
}
|
||||
if cs.requestSigner == nil {
|
||||
headers := make(map[string]string)
|
||||
if shouldUseMetadataServer() {
|
||||
if cs.shouldUseMetadataServer() {
|
||||
awsSessionToken, err := cs.getAWSSessionToken()
|
||||
if err != nil {
|
||||
return "", err
|
||||
@ -318,8 +332,8 @@ func (cs awsCredentialSource) subjectToken() (string, error) {
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if cs.region, err = cs.getRegion(headers); err != nil {
|
||||
cs.region, err = cs.getRegion(headers)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@ -331,7 +345,7 @@ func (cs awsCredentialSource) subjectToken() (string, error) {
|
||||
|
||||
// Generate the signed request to AWS STS GetCallerIdentity API.
|
||||
// Use the required regional endpoint. Otherwise, the request will fail.
|
||||
req, err := http.NewRequest("POST", strings.Replace(cs.RegionalCredVerificationURL, "{region}", cs.region, 1), nil)
|
||||
req, err := http.NewRequest("POST", strings.Replace(cs.regionalCredVerificationURL, "{region}", cs.region, 1), nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@ -339,8 +353,8 @@ func (cs awsCredentialSource) subjectToken() (string, error) {
|
||||
// provider, with or without the HTTPS prefix.
|
||||
// Including this header as part of the signature is recommended to
|
||||
// ensure data integrity.
|
||||
if cs.TargetResource != "" {
|
||||
req.Header.Add("x-goog-cloud-target-resource", cs.TargetResource)
|
||||
if cs.targetResource != "" {
|
||||
req.Header.Add("x-goog-cloud-target-resource", cs.targetResource)
|
||||
}
|
||||
cs.requestSigner.SignRequest(req)
|
||||
|
||||
@ -387,11 +401,11 @@ func (cs awsCredentialSource) subjectToken() (string, error) {
|
||||
}
|
||||
|
||||
func (cs *awsCredentialSource) getAWSSessionToken() (string, error) {
|
||||
if cs.IMDSv2SessionTokenURL == "" {
|
||||
if cs.imdsv2SessionTokenURL == "" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("PUT", cs.IMDSv2SessionTokenURL, nil)
|
||||
req, err := http.NewRequest("PUT", cs.imdsv2SessionTokenURL, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@ -404,31 +418,35 @@ func (cs *awsCredentialSource) getAWSSessionToken() (string, error) {
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||
respBody, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return "", fmt.Errorf("oauth2/google: unable to retrieve AWS session token - %s", string(respBody))
|
||||
return "", fmt.Errorf("oauth2/google/externalaccount: unable to retrieve AWS session token - %s", string(respBody))
|
||||
}
|
||||
|
||||
return string(respBody), nil
|
||||
}
|
||||
|
||||
func (cs *awsCredentialSource) getRegion(headers map[string]string) (string, error) {
|
||||
if cs.awsSecurityCredentialsSupplier != nil {
|
||||
return cs.awsSecurityCredentialsSupplier.AwsRegion(cs.ctx, cs.supplierOptions)
|
||||
}
|
||||
if canRetrieveRegionFromEnvironment() {
|
||||
if envAwsRegion := getenv(awsRegion); envAwsRegion != "" {
|
||||
cs.region = envAwsRegion
|
||||
return envAwsRegion, nil
|
||||
}
|
||||
return getenv("AWS_DEFAULT_REGION"), nil
|
||||
}
|
||||
|
||||
if cs.RegionURL == "" {
|
||||
return "", errors.New("oauth2/google: unable to determine AWS region")
|
||||
if cs.regionURL == "" {
|
||||
return "", errors.New("oauth2/google/externalaccount: unable to determine AWS region")
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", cs.RegionURL, nil)
|
||||
req, err := http.NewRequest("GET", cs.regionURL, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@ -443,13 +461,13 @@ func (cs *awsCredentialSource) getRegion(headers map[string]string) (string, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||
respBody, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return "", fmt.Errorf("oauth2/google: unable to retrieve AWS region - %s", string(respBody))
|
||||
return "", fmt.Errorf("oauth2/google/externalaccount: unable to retrieve AWS region - %s", string(respBody))
|
||||
}
|
||||
|
||||
// This endpoint will return the region in format: us-east-2b.
|
||||
@ -461,12 +479,15 @@ func (cs *awsCredentialSource) getRegion(headers map[string]string) (string, err
|
||||
return string(respBody[:respBodyEnd]), nil
|
||||
}
|
||||
|
||||
func (cs *awsCredentialSource) getSecurityCredentials(headers map[string]string) (result awsSecurityCredentials, err error) {
|
||||
func (cs *awsCredentialSource) getSecurityCredentials(headers map[string]string) (result *AwsSecurityCredentials, err error) {
|
||||
if cs.awsSecurityCredentialsSupplier != nil {
|
||||
return cs.awsSecurityCredentialsSupplier.AwsSecurityCredentials(cs.ctx, cs.supplierOptions)
|
||||
}
|
||||
if canRetrieveSecurityCredentialFromEnvironment() {
|
||||
return awsSecurityCredentials{
|
||||
return &AwsSecurityCredentials{
|
||||
AccessKeyID: getenv(awsAccessKeyId),
|
||||
SecretAccessKey: getenv(awsSecretAccessKey),
|
||||
SecurityToken: getenv(awsSessionToken),
|
||||
SessionToken: getenv(awsSessionToken),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -481,24 +502,23 @@ func (cs *awsCredentialSource) getSecurityCredentials(headers map[string]string)
|
||||
}
|
||||
|
||||
if credentials.AccessKeyID == "" {
|
||||
return result, errors.New("oauth2/google: missing AccessKeyId credential")
|
||||
return result, errors.New("oauth2/google/externalaccount: missing AccessKeyId credential")
|
||||
}
|
||||
|
||||
if credentials.SecretAccessKey == "" {
|
||||
return result, errors.New("oauth2/google: missing SecretAccessKey credential")
|
||||
return result, errors.New("oauth2/google/externalaccount: missing SecretAccessKey credential")
|
||||
}
|
||||
|
||||
return credentials, nil
|
||||
return &credentials, nil
|
||||
}
|
||||
|
||||
func (cs *awsCredentialSource) getMetadataSecurityCredentials(roleName string, headers map[string]string) (awsSecurityCredentials, error) {
|
||||
var result awsSecurityCredentials
|
||||
func (cs *awsCredentialSource) getMetadataSecurityCredentials(roleName string, headers map[string]string) (AwsSecurityCredentials, error) {
|
||||
var result AwsSecurityCredentials
|
||||
|
||||
req, err := http.NewRequest("GET", fmt.Sprintf("%s/%s", cs.CredVerificationURL, roleName), nil)
|
||||
req, err := http.NewRequest("GET", fmt.Sprintf("%s/%s", cs.credVerificationURL, roleName), nil)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
|
||||
for name, value := range headers {
|
||||
req.Header.Add(name, value)
|
||||
@ -510,13 +530,13 @@ func (cs *awsCredentialSource) getMetadataSecurityCredentials(roleName string, h
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||
respBody, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return result, fmt.Errorf("oauth2/google: unable to retrieve AWS security credentials - %s", string(respBody))
|
||||
return result, fmt.Errorf("oauth2/google/externalaccount: unable to retrieve AWS security credentials - %s", string(respBody))
|
||||
}
|
||||
|
||||
err = json.Unmarshal(respBody, &result)
|
||||
@ -524,11 +544,11 @@ func (cs *awsCredentialSource) getMetadataSecurityCredentials(roleName string, h
|
||||
}
|
||||
|
||||
func (cs *awsCredentialSource) getMetadataRoleName(headers map[string]string) (string, error) {
|
||||
if cs.CredVerificationURL == "" {
|
||||
return "", errors.New("oauth2/google: unable to determine the AWS metadata server security credentials endpoint")
|
||||
if cs.credVerificationURL == "" {
|
||||
return "", errors.New("oauth2/google/externalaccount: unable to determine the AWS metadata server security credentials endpoint")
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", cs.CredVerificationURL, nil)
|
||||
req, err := http.NewRequest("GET", cs.credVerificationURL, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@ -543,13 +563,13 @@ func (cs *awsCredentialSource) getMetadataRoleName(headers map[string]string) (s
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||
respBody, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return "", fmt.Errorf("oauth2/google: unable to retrieve AWS role name - %s", string(respBody))
|
||||
return "", fmt.Errorf("oauth2/google/externalaccount: unable to retrieve AWS role name - %s", string(respBody))
|
||||
}
|
||||
|
||||
return string(respBody), nil
|
@ -7,6 +7,7 @@ package externalaccount
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@ -36,7 +37,7 @@ func setEnvironment(env map[string]string) func(string) string {
|
||||
|
||||
var defaultRequestSigner = &awsRequestSigner{
|
||||
RegionName: "us-east-1",
|
||||
AwsSecurityCredentials: awsSecurityCredentials{
|
||||
AwsSecurityCredentials: &AwsSecurityCredentials{
|
||||
AccessKeyID: "AKIDEXAMPLE",
|
||||
SecretAccessKey: "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
|
||||
},
|
||||
@ -50,10 +51,10 @@ const (
|
||||
|
||||
var requestSignerWithToken = &awsRequestSigner{
|
||||
RegionName: "us-east-2",
|
||||
AwsSecurityCredentials: awsSecurityCredentials{
|
||||
AwsSecurityCredentials: &AwsSecurityCredentials{
|
||||
AccessKeyID: accessKeyID,
|
||||
SecretAccessKey: secretAccessKey,
|
||||
SecurityToken: securityToken,
|
||||
SessionToken: securityToken,
|
||||
},
|
||||
}
|
||||
|
||||
@ -388,7 +389,7 @@ func TestAWSv4Signature_PostRequestWithSecurityTokenAndAdditionalHeaders(t *test
|
||||
func TestAWSv4Signature_PostRequestWithAmzDateButNoSecurityToken(t *testing.T) {
|
||||
var requestSigner = &awsRequestSigner{
|
||||
RegionName: "us-east-2",
|
||||
AwsSecurityCredentials: awsSecurityCredentials{
|
||||
AwsSecurityCredentials: &AwsSecurityCredentials{
|
||||
AccessKeyID: accessKeyID,
|
||||
SecretAccessKey: secretAccessKey,
|
||||
},
|
||||
@ -526,8 +527,8 @@ func notFound(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func noHeaderValidation(r *http.Request) {}
|
||||
|
||||
func (server *testAwsServer) getCredentialSource(url string) CredentialSource {
|
||||
return CredentialSource{
|
||||
func (server *testAwsServer) getCredentialSource(url string) *CredentialSource {
|
||||
return &CredentialSource{
|
||||
EnvironmentID: "aws1",
|
||||
URL: url + server.url,
|
||||
RegionURL: url + server.regionURL,
|
||||
@ -541,10 +542,10 @@ func getExpectedSubjectToken(url, region, accessKeyID, secretAccessKey, security
|
||||
req.Header.Add("x-goog-cloud-target-resource", testFileConfig.Audience)
|
||||
signer := &awsRequestSigner{
|
||||
RegionName: region,
|
||||
AwsSecurityCredentials: awsSecurityCredentials{
|
||||
AwsSecurityCredentials: &AwsSecurityCredentials{
|
||||
AccessKeyID: accessKeyID,
|
||||
SecretAccessKey: secretAccessKey,
|
||||
SecurityToken: securityToken,
|
||||
SessionToken: securityToken,
|
||||
},
|
||||
}
|
||||
signer.SignRequest(req)
|
||||
@ -588,7 +589,6 @@ func TestAWSCredential_BasicRequest(t *testing.T) {
|
||||
|
||||
tfc := testFileConfig
|
||||
tfc.CredentialSource = server.getCredentialSource(ts.URL)
|
||||
|
||||
oldGetenv := getenv
|
||||
oldNow := now
|
||||
defer func() {
|
||||
@ -846,7 +846,7 @@ func TestAWSCredential_RequestWithBadVersion(t *testing.T) {
|
||||
if err == nil {
|
||||
t.Fatalf("parse() should have failed")
|
||||
}
|
||||
if got, want := err.Error(), "oauth2/google: aws version '3' is not supported in the current build"; !reflect.DeepEqual(got, want) {
|
||||
if got, want := err.Error(), "oauth2/google/externalaccount: aws version '3' is not supported in the current build"; !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("subjectToken = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
@ -875,7 +875,7 @@ func TestAWSCredential_RequestWithNoRegionURL(t *testing.T) {
|
||||
t.Fatalf("retrieveSubjectToken() should have failed")
|
||||
}
|
||||
|
||||
if got, want := err.Error(), "oauth2/google: unable to determine AWS region"; !reflect.DeepEqual(got, want) {
|
||||
if got, want := err.Error(), "oauth2/google/externalaccount: unable to determine AWS region"; !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("subjectToken = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
@ -905,7 +905,7 @@ func TestAWSCredential_RequestWithBadRegionURL(t *testing.T) {
|
||||
t.Fatalf("retrieveSubjectToken() should have failed")
|
||||
}
|
||||
|
||||
if got, want := err.Error(), "oauth2/google: unable to retrieve AWS region - Not Found"; !reflect.DeepEqual(got, want) {
|
||||
if got, want := err.Error(), "oauth2/google/externalaccount: unable to retrieve AWS region - Not Found"; !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("subjectToken = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
@ -937,7 +937,7 @@ func TestAWSCredential_RequestWithMissingCredential(t *testing.T) {
|
||||
t.Fatalf("retrieveSubjectToken() should have failed")
|
||||
}
|
||||
|
||||
if got, want := err.Error(), "oauth2/google: missing AccessKeyId credential"; !reflect.DeepEqual(got, want) {
|
||||
if got, want := err.Error(), "oauth2/google/externalaccount: missing AccessKeyId credential"; !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("subjectToken = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
@ -969,7 +969,7 @@ func TestAWSCredential_RequestWithIncompleteCredential(t *testing.T) {
|
||||
t.Fatalf("retrieveSubjectToken() should have failed")
|
||||
}
|
||||
|
||||
if got, want := err.Error(), "oauth2/google: missing SecretAccessKey credential"; !reflect.DeepEqual(got, want) {
|
||||
if got, want := err.Error(), "oauth2/google/externalaccount: missing SecretAccessKey credential"; !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("subjectToken = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
@ -998,7 +998,7 @@ func TestAWSCredential_RequestWithNoCredentialURL(t *testing.T) {
|
||||
t.Fatalf("retrieveSubjectToken() should have failed")
|
||||
}
|
||||
|
||||
if got, want := err.Error(), "oauth2/google: unable to determine the AWS metadata server security credentials endpoint"; !reflect.DeepEqual(got, want) {
|
||||
if got, want := err.Error(), "oauth2/google/externalaccount: unable to determine the AWS metadata server security credentials endpoint"; !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("subjectToken = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
@ -1027,7 +1027,7 @@ func TestAWSCredential_RequestWithBadCredentialURL(t *testing.T) {
|
||||
t.Fatalf("retrieveSubjectToken() should have failed")
|
||||
}
|
||||
|
||||
if got, want := err.Error(), "oauth2/google: unable to retrieve AWS role name - Not Found"; !reflect.DeepEqual(got, want) {
|
||||
if got, want := err.Error(), "oauth2/google/externalaccount: unable to retrieve AWS role name - Not Found"; !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("subjectToken = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
@ -1056,7 +1056,7 @@ func TestAWSCredential_RequestWithBadFinalCredentialURL(t *testing.T) {
|
||||
t.Fatalf("retrieveSubjectToken() should have failed")
|
||||
}
|
||||
|
||||
if got, want := err.Error(), "oauth2/google: unable to retrieve AWS security credentials - Not Found"; !reflect.DeepEqual(got, want) {
|
||||
if got, want := err.Error(), "oauth2/google/externalaccount: unable to retrieve AWS security credentials - Not Found"; !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("subjectToken = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
@ -1235,6 +1235,192 @@ func TestAWSCredential_ShouldCallMetadataEndpointWhenNoSecretAccessKey(t *testin
|
||||
}
|
||||
}
|
||||
|
||||
func TestAWSCredential_ProgrammaticAuth(t *testing.T) {
|
||||
tfc := testFileConfig
|
||||
securityCredentials := AwsSecurityCredentials{
|
||||
AccessKeyID: accessKeyID,
|
||||
SecretAccessKey: secretAccessKey,
|
||||
SessionToken: securityToken,
|
||||
}
|
||||
|
||||
tfc.AwsSecurityCredentialsSupplier = testAwsSupplier{
|
||||
awsRegion: "us-east-2",
|
||||
err: nil,
|
||||
credentials: &securityCredentials,
|
||||
}
|
||||
|
||||
oldNow := now
|
||||
defer func() {
|
||||
now = oldNow
|
||||
}()
|
||||
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,
|
||||
securityToken,
|
||||
)
|
||||
|
||||
if got, want := out, expected; !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("subjectToken = \n%q\n want \n%q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAWSCredential_ProgrammaticAuthNoSessionToken(t *testing.T) {
|
||||
tfc := testFileConfig
|
||||
securityCredentials := AwsSecurityCredentials{
|
||||
AccessKeyID: accessKeyID,
|
||||
SecretAccessKey: secretAccessKey,
|
||||
}
|
||||
|
||||
tfc.AwsSecurityCredentialsSupplier = testAwsSupplier{
|
||||
awsRegion: "us-east-2",
|
||||
err: nil,
|
||||
credentials: &securityCredentials,
|
||||
}
|
||||
|
||||
oldNow := now
|
||||
defer func() {
|
||||
now = oldNow
|
||||
}()
|
||||
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_ProgrammaticAuthError(t *testing.T) {
|
||||
tfc := testFileConfig
|
||||
testErr := errors.New("test error")
|
||||
tfc.AwsSecurityCredentialsSupplier = testAwsSupplier{
|
||||
awsRegion: "us-east-2",
|
||||
err: testErr,
|
||||
credentials: nil,
|
||||
}
|
||||
|
||||
base, err := tfc.parse(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("parse() failed %v", err)
|
||||
}
|
||||
|
||||
_, err = base.subjectToken()
|
||||
if err == nil {
|
||||
t.Fatalf("subjectToken() should have failed")
|
||||
}
|
||||
if err != testErr {
|
||||
t.Errorf("error = %e, want %e", err, testErr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAWSCredential_ProgrammaticAuthRegionError(t *testing.T) {
|
||||
tfc := testFileConfig
|
||||
securityCredentials := AwsSecurityCredentials{
|
||||
AccessKeyID: accessKeyID,
|
||||
SecretAccessKey: secretAccessKey,
|
||||
}
|
||||
|
||||
testErr := errors.New("test")
|
||||
tfc.AwsSecurityCredentialsSupplier = testAwsSupplier{
|
||||
awsRegion: "",
|
||||
regionErr: testErr,
|
||||
credentials: &securityCredentials,
|
||||
}
|
||||
|
||||
base, err := tfc.parse(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("parse() failed %v", err)
|
||||
}
|
||||
|
||||
_, err = base.subjectToken()
|
||||
if err == nil {
|
||||
t.Fatalf("subjectToken() should have failed")
|
||||
}
|
||||
if err != testErr {
|
||||
t.Errorf("error = %e, want %e", err, testErr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAWSCredential_ProgrammaticAuthOptions(t *testing.T) {
|
||||
tfc := testFileConfig
|
||||
securityCredentials := AwsSecurityCredentials{
|
||||
AccessKeyID: accessKeyID,
|
||||
SecretAccessKey: secretAccessKey,
|
||||
}
|
||||
expectedOptions := SupplierOptions{Audience: tfc.Audience, SubjectTokenType: tfc.SubjectTokenType}
|
||||
|
||||
tfc.AwsSecurityCredentialsSupplier = testAwsSupplier{
|
||||
awsRegion: "us-east-2",
|
||||
credentials: &securityCredentials,
|
||||
expectedOptions: &expectedOptions,
|
||||
}
|
||||
|
||||
base, err := tfc.parse(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("parse() failed %v", err)
|
||||
}
|
||||
|
||||
_, err = base.subjectToken()
|
||||
if err != nil {
|
||||
t.Fatalf("subjectToken() failed %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAWSCredential_ProgrammaticAuthContext(t *testing.T) {
|
||||
tfc := testFileConfig
|
||||
securityCredentials := AwsSecurityCredentials{
|
||||
AccessKeyID: accessKeyID,
|
||||
SecretAccessKey: secretAccessKey,
|
||||
}
|
||||
ctx := context.Background()
|
||||
|
||||
tfc.AwsSecurityCredentialsSupplier = testAwsSupplier{
|
||||
awsRegion: "us-east-2",
|
||||
credentials: &securityCredentials,
|
||||
expectedContext: ctx,
|
||||
}
|
||||
|
||||
base, err := tfc.parse(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("parse() failed %v", err)
|
||||
}
|
||||
|
||||
_, err = base.subjectToken()
|
||||
if err != nil {
|
||||
t.Fatalf("subjectToken() failed %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAwsCredential_CredentialSourceType(t *testing.T) {
|
||||
server := createDefaultAwsTestServer()
|
||||
ts := httptest.NewServer(server)
|
||||
@ -1251,3 +1437,52 @@ func TestAwsCredential_CredentialSourceType(t *testing.T) {
|
||||
t.Errorf("got %v but want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
type testAwsSupplier struct {
|
||||
err error
|
||||
regionErr error
|
||||
awsRegion string
|
||||
credentials *AwsSecurityCredentials
|
||||
expectedOptions *SupplierOptions
|
||||
expectedContext context.Context
|
||||
}
|
||||
|
||||
func (supp testAwsSupplier) AwsRegion(ctx context.Context, options SupplierOptions) (string, error) {
|
||||
if supp.regionErr != nil {
|
||||
return "", supp.regionErr
|
||||
}
|
||||
if supp.expectedOptions != nil {
|
||||
if supp.expectedOptions.Audience != options.Audience {
|
||||
return "", errors.New("Audience does not match")
|
||||
}
|
||||
if supp.expectedOptions.SubjectTokenType != options.SubjectTokenType {
|
||||
return "", errors.New("Audience does not match")
|
||||
}
|
||||
}
|
||||
if supp.expectedContext != nil {
|
||||
if supp.expectedContext != ctx {
|
||||
return "", errors.New("Context does not match")
|
||||
}
|
||||
}
|
||||
return supp.awsRegion, nil
|
||||
}
|
||||
|
||||
func (supp testAwsSupplier) AwsSecurityCredentials(ctx context.Context, options SupplierOptions) (*AwsSecurityCredentials, error) {
|
||||
if supp.err != nil {
|
||||
return nil, supp.err
|
||||
}
|
||||
if supp.expectedOptions != nil {
|
||||
if supp.expectedOptions.Audience != options.Audience {
|
||||
return nil, errors.New("Audience does not match")
|
||||
}
|
||||
if supp.expectedOptions.SubjectTokenType != options.SubjectTokenType {
|
||||
return nil, errors.New("Audience does not match")
|
||||
}
|
||||
}
|
||||
if supp.expectedContext != nil {
|
||||
if supp.expectedContext != ctx {
|
||||
return nil, errors.New("Context does not match")
|
||||
}
|
||||
}
|
||||
return supp.credentials, nil
|
||||
}
|
517
google/externalaccount/basecredentials.go
Normal file
517
google/externalaccount/basecredentials.go
Normal file
@ -0,0 +1,517 @@
|
||||
// Copyright 2020 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 provides support for creating workload identity
|
||||
federation and workforce identity federation token sources that can be
|
||||
used to access Google Cloud resources from external identity providers.
|
||||
|
||||
# Workload Identity Federation
|
||||
|
||||
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) 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.
|
||||
This lets you access Google Cloud resources directly, eliminating the
|
||||
maintenance and security burden associated with service account keys.
|
||||
|
||||
Follow the detailed instructions on how to configure Workload Identity Federation
|
||||
in various platforms:
|
||||
|
||||
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 fours ways:
|
||||
from a local file location (file-sourced credentials), from a server
|
||||
(URL-sourced credentials), from a local executable (executable-sourced
|
||||
credentials), or from a user defined function that returns an OIDC or SAML token.
|
||||
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/workload-identity-federation-with-other-providers#create_a_credential_configuration
|
||||
|
||||
To use a custom function to supply the token, define a struct that implements the [SubjectTokenSupplier] interface for OIDC/SAML providers,
|
||||
or one that implements [AwsSecurityCredentialsSupplier] for AWS providers. This can then be used when building a [Config].
|
||||
The [golang.org/x/oauth2.TokenSource] created from the config using [NewTokenSource] can then be used to access Google
|
||||
Cloud resources. For instance, you can create a new client from the
|
||||
[cloud.google.com/go/storage] package and pass in option.WithTokenSource(yourTokenSource))
|
||||
|
||||
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 four ways:
|
||||
from a local file location (file-sourced credentials), from a server
|
||||
(URL-sourced credentials), from a local executable (executable-sourced
|
||||
credentials), or from a user supplied function that returns an OIDC or SAML token.
|
||||
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
|
||||
|
||||
To use a custom function to supply the token, define a struct that implements the [SubjectTokenSupplier] interface for OIDC/SAML providers.
|
||||
This can then be used when building a [Config].
|
||||
The [golang.org/x/oauth2.TokenSource] created from the config using [NewTokenSource] can then be used access Google
|
||||
Cloud resources. For instance, you can create a new client from the
|
||||
[cloud.google.com/go/storage] package and pass in option.WithTokenSource(yourTokenSource))
|
||||
|
||||
# Security considerations
|
||||
|
||||
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.
|
||||
*/
|
||||
package externalaccount
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/google/internal/impersonate"
|
||||
"golang.org/x/oauth2/google/internal/stsexchange"
|
||||
)
|
||||
|
||||
const (
|
||||
universeDomainPlaceholder = "UNIVERSE_DOMAIN"
|
||||
defaultTokenURL = "https://sts.UNIVERSE_DOMAIN/v1/token"
|
||||
defaultUniverseDomain = "googleapis.com"
|
||||
)
|
||||
|
||||
// now aliases time.Now for testing
|
||||
var now = func() time.Time {
|
||||
return time.Now().UTC()
|
||||
}
|
||||
|
||||
// Config stores the configuration for fetching tokens with external credentials.
|
||||
type Config struct {
|
||||
// Audience is the Secure Token Service (STS) audience which contains the resource name for the workload
|
||||
// identity pool or the workforce pool and the provider identifier in that pool. Required.
|
||||
Audience string
|
||||
// SubjectTokenType is the STS token type based on the Oauth2.0 token exchange spec.
|
||||
// Expected values include:
|
||||
// “urn:ietf:params:oauth:token-type:jwt”
|
||||
// “urn:ietf:params:oauth:token-type:id-token”
|
||||
// “urn:ietf:params:oauth:token-type:saml2”
|
||||
// “urn:ietf:params:aws:token-type:aws4_request”
|
||||
// Required.
|
||||
SubjectTokenType string
|
||||
// TokenURL is the STS token exchange endpoint. If not provided, will default to
|
||||
// https://sts.UNIVERSE_DOMAIN/v1/token, with UNIVERSE_DOMAIN set to the
|
||||
// default service domain googleapis.com unless UniverseDomain is set.
|
||||
// Optional.
|
||||
TokenURL string
|
||||
// TokenInfoURL is the token_info endpoint used to retrieve the account related information (
|
||||
// user attributes like account identifier, eg. email, username, uid, etc). This is
|
||||
// needed for gCloud session account identification. Optional.
|
||||
TokenInfoURL string
|
||||
// ServiceAccountImpersonationURL is the URL for the service account impersonation request. This is only
|
||||
// required for workload identity pools when APIs to be accessed have not integrated with UberMint. Optional.
|
||||
ServiceAccountImpersonationURL string
|
||||
// ServiceAccountImpersonationLifetimeSeconds is the number of seconds the service account impersonation
|
||||
// token will be valid for. If not provided, it will default to 3600. Optional.
|
||||
ServiceAccountImpersonationLifetimeSeconds int
|
||||
// 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 ClientId as username and ClientSecret as password. Optional.
|
||||
ClientSecret string
|
||||
// ClientID is only required in conjunction with ClientSecret, as described above. Optional.
|
||||
ClientID string
|
||||
// CredentialSource contains the necessary information to retrieve the token itself, as well
|
||||
// as some environmental information. One of SubjectTokenSupplier, AWSSecurityCredentialSupplier or
|
||||
// CredentialSource must be provided. Optional.
|
||||
CredentialSource *CredentialSource
|
||||
// QuotaProjectID is injected by gCloud. If the value is non-empty, the Auth libraries
|
||||
// will set the x-goog-user-project header which overrides the project associated with the credentials. Optional.
|
||||
QuotaProjectID string
|
||||
// Scopes contains the desired scopes for the returned access token. Optional.
|
||||
Scopes []string
|
||||
// WorkforcePoolUserProject is the workforce pool user project number when the credential
|
||||
// corresponds to a workforce pool and not a workload identity pool.
|
||||
// The underlying principal must still have serviceusage.services.use IAM
|
||||
// permission to use the project for billing/quota. Optional.
|
||||
WorkforcePoolUserProject string
|
||||
// SubjectTokenSupplier is an optional token supplier for OIDC/SAML credentials.
|
||||
// One of SubjectTokenSupplier, AWSSecurityCredentialSupplier or CredentialSource must be provided. Optional.
|
||||
SubjectTokenSupplier SubjectTokenSupplier
|
||||
// AwsSecurityCredentialsSupplier is an AWS Security Credential supplier for AWS credentials.
|
||||
// One of SubjectTokenSupplier, AWSSecurityCredentialSupplier or CredentialSource must be provided. Optional.
|
||||
AwsSecurityCredentialsSupplier AwsSecurityCredentialsSupplier
|
||||
// UniverseDomain is the default service domain for a given Cloud universe.
|
||||
// This value will be used in the default STS token URL. The default value
|
||||
// is "googleapis.com". It will not be used if TokenURL is set. Optional.
|
||||
UniverseDomain string
|
||||
}
|
||||
|
||||
var (
|
||||
validWorkforceAudiencePattern *regexp.Regexp = regexp.MustCompile(`//iam\.googleapis\.com/locations/[^/]+/workforcePools/`)
|
||||
)
|
||||
|
||||
func validateWorkforceAudience(input string) bool {
|
||||
return validWorkforceAudiencePattern.MatchString(input)
|
||||
}
|
||||
|
||||
// NewTokenSource Returns an external account TokenSource using the provided external account config.
|
||||
func NewTokenSource(ctx context.Context, conf Config) (oauth2.TokenSource, error) {
|
||||
if conf.Audience == "" {
|
||||
return nil, fmt.Errorf("oauth2/google/externalaccount: Audience must be set")
|
||||
}
|
||||
if conf.SubjectTokenType == "" {
|
||||
return nil, fmt.Errorf("oauth2/google/externalaccount: Subject token type must be set")
|
||||
}
|
||||
if conf.WorkforcePoolUserProject != "" {
|
||||
valid := validateWorkforceAudience(conf.Audience)
|
||||
if !valid {
|
||||
return nil, fmt.Errorf("oauth2/google/externalaccount: Workforce pool user project should not be set for non-workforce pool credentials")
|
||||
}
|
||||
}
|
||||
count := 0
|
||||
if conf.CredentialSource != nil {
|
||||
count++
|
||||
}
|
||||
if conf.SubjectTokenSupplier != nil {
|
||||
count++
|
||||
}
|
||||
if conf.AwsSecurityCredentialsSupplier != nil {
|
||||
count++
|
||||
}
|
||||
if count == 0 {
|
||||
return nil, fmt.Errorf("oauth2/google/externalaccount: One of CredentialSource, SubjectTokenSupplier, or AwsSecurityCredentialsSupplier must be set")
|
||||
}
|
||||
if count > 1 {
|
||||
return nil, fmt.Errorf("oauth2/google/externalaccount: Only one of CredentialSource, SubjectTokenSupplier, or AwsSecurityCredentialsSupplier must be set")
|
||||
}
|
||||
return conf.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, scheme string) (oauth2.TokenSource, error) {
|
||||
|
||||
ts := tokenSource{
|
||||
ctx: ctx,
|
||||
conf: c,
|
||||
}
|
||||
if c.ServiceAccountImpersonationURL == "" {
|
||||
return oauth2.ReuseTokenSource(nil, ts), nil
|
||||
}
|
||||
scopes := c.Scopes
|
||||
ts.conf.Scopes = []string{"https://www.googleapis.com/auth/cloud-platform"}
|
||||
imp := impersonate.ImpersonateTokenSource{
|
||||
Ctx: ctx,
|
||||
URL: c.ServiceAccountImpersonationURL,
|
||||
Scopes: scopes,
|
||||
Ts: oauth2.ReuseTokenSource(nil, ts),
|
||||
TokenLifetimeSeconds: c.ServiceAccountImpersonationLifetimeSeconds,
|
||||
}
|
||||
return oauth2.ReuseTokenSource(nil, imp), nil
|
||||
}
|
||||
|
||||
// Subject token file types.
|
||||
const (
|
||||
fileTypeText = "text"
|
||||
fileTypeJSON = "json"
|
||||
)
|
||||
|
||||
// Format contains information needed to retrieve a subject token for URL or File sourced credentials.
|
||||
type Format struct {
|
||||
// Type should be either "text" or "json". This determines whether the file or URL sourced credentials
|
||||
// expect a simple text subject token or if the subject token will be contained in a JSON object.
|
||||
// When not provided "text" type is assumed.
|
||||
Type string `json:"type"`
|
||||
// SubjectTokenFieldName is only required for JSON format. This is the field name that the credentials will check
|
||||
// for the subject token in the file or URL response. This would be "access_token" for azure.
|
||||
SubjectTokenFieldName string `json:"subject_token_field_name"`
|
||||
}
|
||||
|
||||
// CredentialSource stores the information necessary to retrieve the credentials for the STS exchange.
|
||||
type CredentialSource struct {
|
||||
// File is the location for file sourced credentials.
|
||||
// One field amongst File, URL, Executable, or EnvironmentID should be provided, depending on the kind of credential in question.
|
||||
//
|
||||
// Important: If you accept a credential configuration (credential
|
||||
// JSON/File/Stream) from an external source for authentication to Google
|
||||
// Cloud Platform, you must validate it before providing it to any Google
|
||||
// API or library. Providing an unvalidated credential configuration to
|
||||
// Google APIs can compromise the security of your systems and data. For
|
||||
// more information, refer to [Validate credential configurations from
|
||||
// external sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials).
|
||||
File string `json:"file"`
|
||||
|
||||
// Url is the URL to call for URL sourced credentials.
|
||||
// One field amongst File, URL, Executable, or EnvironmentID should be provided, depending on the kind of credential in question.
|
||||
//
|
||||
// Important: If you accept a credential configuration (credential
|
||||
// JSON/File/Stream) from an external source for authentication to Google
|
||||
// Cloud Platform, you must validate it before providing it to any Google
|
||||
// API or library. Providing an unvalidated credential configuration to
|
||||
// Google APIs can compromise the security of your systems and data. For
|
||||
// more information, refer to [Validate credential configurations from
|
||||
// external sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials).
|
||||
URL string `json:"url"`
|
||||
// Headers are the headers to attach to the request for URL sourced credentials.
|
||||
Headers map[string]string `json:"headers"`
|
||||
|
||||
// Executable is the configuration object for executable sourced credentials.
|
||||
// One field amongst File, URL, Executable, or EnvironmentID should be provided, depending on the kind of credential in question.
|
||||
//
|
||||
// Important: If you accept a credential configuration (credential
|
||||
// JSON/File/Stream) from an external source for authentication to Google
|
||||
// Cloud Platform, you must validate it before providing it to any Google
|
||||
// API or library. Providing an unvalidated credential configuration to
|
||||
// Google APIs can compromise the security of your systems and data. For
|
||||
// more information, refer to [Validate credential configurations from
|
||||
// external sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials).
|
||||
Executable *ExecutableConfig `json:"executable"`
|
||||
|
||||
// EnvironmentID is the EnvironmentID used for AWS sourced credentials. This should start with "AWS".
|
||||
// One field amongst File, URL, Executable, or EnvironmentID should be provided, depending on the kind of credential in question.
|
||||
//
|
||||
// Important: If you accept a credential configuration (credential
|
||||
// JSON/File/Stream) from an external source for authentication to Google
|
||||
// Cloud Platform, you must validate it before providing it to any Google
|
||||
// API or library. Providing an unvalidated credential configuration to
|
||||
// Google APIs can compromise the security of your systems and data. For
|
||||
// more information, refer to [Validate credential configurations from
|
||||
// external sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials).
|
||||
EnvironmentID string `json:"environment_id"`
|
||||
// RegionURL is the metadata URL to retrieve the region from for EC2 AWS credentials.
|
||||
RegionURL string `json:"region_url"`
|
||||
// RegionalCredVerificationURL is the AWS regional credential verification URL, will default to
|
||||
// "https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15" if not provided."
|
||||
RegionalCredVerificationURL string `json:"regional_cred_verification_url"`
|
||||
// IMDSv2SessionTokenURL is the URL to retrieve the session token when using IMDSv2 in AWS.
|
||||
IMDSv2SessionTokenURL string `json:"imdsv2_session_token_url"`
|
||||
// Format is the format type for the subject token. Used for File and URL sourced credentials. Expected values are "text" or "json".
|
||||
Format Format `json:"format"`
|
||||
}
|
||||
|
||||
// ExecutableConfig contains information needed for executable sourced credentials.
|
||||
type ExecutableConfig struct {
|
||||
// Command is the the full command to run to retrieve the subject token.
|
||||
// This can include arguments. Must be an absolute path for the program. Required.
|
||||
Command string `json:"command"`
|
||||
// TimeoutMillis is the timeout duration, in milliseconds. Defaults to 30000 milliseconds when not provided. Optional.
|
||||
TimeoutMillis *int `json:"timeout_millis"`
|
||||
// OutputFile is the absolute path to the output file where the executable will cache the response.
|
||||
// If specified the auth libraries will first check this location before running the executable. Optional.
|
||||
OutputFile string `json:"output_file"`
|
||||
}
|
||||
|
||||
// SubjectTokenSupplier can be used to supply a subject token to exchange for a GCP access token.
|
||||
type SubjectTokenSupplier interface {
|
||||
// SubjectToken should return a valid subject token or an error.
|
||||
// The external account token source does not cache the returned subject token, so caching
|
||||
// logic should be implemented in the supplier to prevent multiple requests for the same subject token.
|
||||
SubjectToken(ctx context.Context, options SupplierOptions) (string, error)
|
||||
}
|
||||
|
||||
// AWSSecurityCredentialsSupplier can be used to supply AwsSecurityCredentials and an AWS Region to
|
||||
// exchange for a GCP access token.
|
||||
type AwsSecurityCredentialsSupplier interface {
|
||||
// AwsRegion should return the AWS region or an error.
|
||||
AwsRegion(ctx context.Context, options SupplierOptions) (string, error)
|
||||
// AwsSecurityCredentials should return a valid set of AwsSecurityCredentials or an error.
|
||||
// The external account token source does not cache the returned security credentials, so caching
|
||||
// logic should be implemented in the supplier to prevent multiple requests for the same security credentials.
|
||||
AwsSecurityCredentials(ctx context.Context, options SupplierOptions) (*AwsSecurityCredentials, error)
|
||||
}
|
||||
|
||||
// SupplierOptions contains information about the requested subject token or AWS security credentials from the
|
||||
// Google external account credential.
|
||||
type SupplierOptions struct {
|
||||
// Audience is the requested audience for the external account credential.
|
||||
Audience string
|
||||
// Subject token type is the requested subject token type for the external account credential. Expected values include:
|
||||
// “urn:ietf:params:oauth:token-type:jwt”
|
||||
// “urn:ietf:params:oauth:token-type:id-token”
|
||||
// “urn:ietf:params:oauth:token-type:saml2”
|
||||
// “urn:ietf:params:aws:token-type:aws4_request”
|
||||
SubjectTokenType string
|
||||
}
|
||||
|
||||
// tokenURL returns the default STS token endpoint with the configured universe
|
||||
// domain.
|
||||
func (c *Config) tokenURL() string {
|
||||
if c.UniverseDomain == "" {
|
||||
return strings.Replace(defaultTokenURL, universeDomainPlaceholder, defaultUniverseDomain, 1)
|
||||
}
|
||||
return strings.Replace(defaultTokenURL, universeDomainPlaceholder, c.UniverseDomain, 1)
|
||||
}
|
||||
|
||||
// parse determines the type of CredentialSource needed.
|
||||
func (c *Config) parse(ctx context.Context) (baseCredentialSource, error) {
|
||||
//set Defaults
|
||||
if c.TokenURL == "" {
|
||||
c.TokenURL = c.tokenURL()
|
||||
}
|
||||
supplierOptions := SupplierOptions{Audience: c.Audience, SubjectTokenType: c.SubjectTokenType}
|
||||
|
||||
if c.AwsSecurityCredentialsSupplier != nil {
|
||||
awsCredSource := awsCredentialSource{
|
||||
awsSecurityCredentialsSupplier: c.AwsSecurityCredentialsSupplier,
|
||||
targetResource: c.Audience,
|
||||
supplierOptions: supplierOptions,
|
||||
ctx: ctx,
|
||||
}
|
||||
return awsCredSource, nil
|
||||
} else if c.SubjectTokenSupplier != nil {
|
||||
return programmaticRefreshCredentialSource{subjectTokenSupplier: c.SubjectTokenSupplier, supplierOptions: supplierOptions, ctx: ctx}, nil
|
||||
} else if len(c.CredentialSource.EnvironmentID) > 3 && c.CredentialSource.EnvironmentID[:3] == "aws" {
|
||||
if awsVersion, err := strconv.Atoi(c.CredentialSource.EnvironmentID[3:]); err == nil {
|
||||
if awsVersion != 1 {
|
||||
return nil, fmt.Errorf("oauth2/google/externalaccount: aws version '%d' is not supported in the current build", awsVersion)
|
||||
}
|
||||
|
||||
awsCredSource := awsCredentialSource{
|
||||
environmentID: c.CredentialSource.EnvironmentID,
|
||||
regionURL: c.CredentialSource.RegionURL,
|
||||
regionalCredVerificationURL: c.CredentialSource.RegionalCredVerificationURL,
|
||||
credVerificationURL: c.CredentialSource.URL,
|
||||
targetResource: c.Audience,
|
||||
ctx: ctx,
|
||||
}
|
||||
if c.CredentialSource.IMDSv2SessionTokenURL != "" {
|
||||
awsCredSource.imdsv2SessionTokenURL = c.CredentialSource.IMDSv2SessionTokenURL
|
||||
}
|
||||
|
||||
return awsCredSource, nil
|
||||
}
|
||||
} else if c.CredentialSource.File != "" {
|
||||
return fileCredentialSource{File: c.CredentialSource.File, Format: c.CredentialSource.Format}, nil
|
||||
} else if c.CredentialSource.URL != "" {
|
||||
return urlCredentialSource{URL: c.CredentialSource.URL, Headers: c.CredentialSource.Headers, Format: c.CredentialSource.Format, ctx: ctx}, nil
|
||||
} else if c.CredentialSource.Executable != nil {
|
||||
return createExecutableCredential(ctx, c.CredentialSource.Executable, c)
|
||||
}
|
||||
return nil, fmt.Errorf("oauth2/google/externalaccount: unable to parse credential source")
|
||||
}
|
||||
|
||||
type baseCredentialSource interface {
|
||||
credentialSourceType() string
|
||||
subjectToken() (string, error)
|
||||
}
|
||||
|
||||
// tokenSource is the source that handles external credentials. It is used to retrieve Tokens.
|
||||
type tokenSource struct {
|
||||
ctx context.Context
|
||||
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
|
||||
|
||||
credSource, err := conf.parse(ts.ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
subjectToken, err := credSource.subjectToken()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stsRequest := stsexchange.TokenExchangeRequest{
|
||||
GrantType: "urn:ietf:params:oauth:grant-type:token-exchange",
|
||||
Audience: conf.Audience,
|
||||
Scope: conf.Scopes,
|
||||
RequestedTokenType: "urn:ietf:params:oauth:token-type:access_token",
|
||||
SubjectToken: subjectToken,
|
||||
SubjectTokenType: conf.SubjectTokenType,
|
||||
}
|
||||
header := make(http.Header)
|
||||
header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
header.Add("x-goog-api-client", getMetricsHeaderValue(conf, credSource))
|
||||
clientAuth := stsexchange.ClientAuthentication{
|
||||
AuthStyle: oauth2.AuthStyleInHeader,
|
||||
ClientID: conf.ClientID,
|
||||
ClientSecret: conf.ClientSecret,
|
||||
}
|
||||
var options map[string]any
|
||||
// Do not pass workforce_pool_user_project when client authentication is used.
|
||||
// The client ID is sufficient for determining the user project.
|
||||
if conf.WorkforcePoolUserProject != "" && conf.ClientID == "" {
|
||||
options = map[string]any{
|
||||
"userProject": conf.WorkforcePoolUserProject,
|
||||
}
|
||||
}
|
||||
stsResp, err := stsexchange.ExchangeToken(ts.ctx, conf.TokenURL, &stsRequest, clientAuth, header, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
accessToken := &oauth2.Token{
|
||||
AccessToken: stsResp.AccessToken,
|
||||
TokenType: stsResp.TokenType,
|
||||
}
|
||||
|
||||
// The RFC8693 doesn't define the explicit 0 of "expires_in" field behavior.
|
||||
if stsResp.ExpiresIn <= 0 {
|
||||
return nil, fmt.Errorf("oauth2/google/externalaccount: got invalid expiry from security token service")
|
||||
}
|
||||
accessToken.Expiry = now().Add(time.Duration(stsResp.ExpiresIn) * time.Second)
|
||||
|
||||
if stsResp.RefreshToken != "" {
|
||||
accessToken.RefreshToken = stsResp.RefreshToken
|
||||
}
|
||||
return accessToken, nil
|
||||
}
|
574
google/externalaccount/basecredentials_test.go
Normal file
574
google/externalaccount/basecredentials_test.go
Normal file
@ -0,0 +1,574 @@
|
||||
// Copyright 2020 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 (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
const (
|
||||
textBaseCredPath = "testdata/3pi_cred.txt"
|
||||
jsonBaseCredPath = "testdata/3pi_cred.json"
|
||||
baseImpersonateCredsReqBody = "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%2Fcloud-platform&subject_token=street123&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Ajwt"
|
||||
baseImpersonateCredsRespBody = `{"accessToken":"Second.Access.Token","expireTime":"2020-12-28T15:01:23Z"}`
|
||||
)
|
||||
|
||||
var testBaseCredSource = CredentialSource{
|
||||
File: textBaseCredPath,
|
||||
Format: Format{Type: fileTypeText},
|
||||
}
|
||||
|
||||
var testConfig = Config{
|
||||
Audience: "32555940559.apps.googleusercontent.com",
|
||||
SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt",
|
||||
TokenInfoURL: "http://localhost:8080/v1/tokeninfo",
|
||||
ClientSecret: "notsosecret",
|
||||
ClientID: "rbrgnognrhongo3bi4gb9ghg9g",
|
||||
CredentialSource: &testBaseCredSource,
|
||||
Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"},
|
||||
}
|
||||
|
||||
var (
|
||||
baseCredsRequestBody = "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=street123&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aid_token"
|
||||
baseCredsResponseBody = `{"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"}`
|
||||
workforcePoolRequestBodyWithClientId = "audience=%2F%2Fiam.googleapis.com%2Flocations%2Feu%2FworkforcePools%2Fpool-id%2Fproviders%2Fprovider-id&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=street123&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aid_token"
|
||||
workforcePoolRequestBodyWithoutClientId = "audience=%2F%2Fiam.googleapis.com%2Flocations%2Feu%2FworkforcePools%2Fpool-id%2Fproviders%2Fprovider-id&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&options=%7B%22userProject%22%3A%22myProject%22%7D&requested_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fdevstorage.full_control&subject_token=street123&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aid_token"
|
||||
correctAT = "Sample.Access.Token"
|
||||
expiry int64 = 234852
|
||||
)
|
||||
var (
|
||||
testNow = func() time.Time { return time.Unix(expiry, 0) }
|
||||
)
|
||||
|
||||
type testExchangeTokenServer struct {
|
||||
url string
|
||||
authorization string
|
||||
contentType string
|
||||
metricsHeader string
|
||||
body string
|
||||
response string
|
||||
}
|
||||
|
||||
func run(t *testing.T, config *Config, tets *testExchangeTokenServer) (*oauth2.Token, error) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if got, want := r.URL.String(), tets.url; got != want {
|
||||
t.Errorf("URL.String(): got %v but want %v", got, want)
|
||||
}
|
||||
headerAuth := r.Header.Get("Authorization")
|
||||
if got, want := headerAuth, tets.authorization; got != want {
|
||||
t.Errorf("got %v but want %v", got, want)
|
||||
}
|
||||
headerContentType := r.Header.Get("Content-Type")
|
||||
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 := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed reading request body: %s.", err)
|
||||
}
|
||||
if got, want := string(body), tets.body; got != want {
|
||||
t.Errorf("Unexpected exchange payload: got %v but want %v", got, want)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(tets.response))
|
||||
}))
|
||||
defer server.Close()
|
||||
config.TokenURL = server.URL
|
||||
|
||||
oldNow := now
|
||||
defer func() { now = oldNow }()
|
||||
now = testNow
|
||||
|
||||
ts := tokenSource{
|
||||
ctx: context.Background(),
|
||||
conf: config,
|
||||
}
|
||||
|
||||
return ts.Token()
|
||||
}
|
||||
|
||||
func validateToken(t *testing.T, tok *oauth2.Token, expectToken *oauth2.Token) {
|
||||
if expectToken == nil {
|
||||
return
|
||||
}
|
||||
if got, want := tok.AccessToken, expectToken.AccessToken; got != want {
|
||||
t.Errorf("Unexpected access token: got %v, but wanted %v", got, want)
|
||||
}
|
||||
if got, want := tok.TokenType, expectToken.TokenType; got != want {
|
||||
t.Errorf("Unexpected TokenType: got %v, but wanted %v", got, want)
|
||||
}
|
||||
|
||||
if got, want := tok.Expiry, expectToken.Expiry; got != want {
|
||||
t.Errorf("Unexpected Expiry: got %v, but wanted %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func createImpersonationServer(urlWanted, authWanted, bodyWanted, response string, t *testing.T) *httptest.Server {
|
||||
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if got, want := r.URL.String(), urlWanted; got != want {
|
||||
t.Errorf("URL.String(): got %v but want %v", got, want)
|
||||
}
|
||||
headerAuth := r.Header.Get("Authorization")
|
||||
if got, want := headerAuth, authWanted; got != want {
|
||||
t.Errorf("got %v but want %v", got, want)
|
||||
}
|
||||
headerContentType := r.Header.Get("Content-Type")
|
||||
if got, want := headerContentType, "application/json"; got != want {
|
||||
t.Errorf("got %v but want %v", got, want)
|
||||
}
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed reading request body: %v.", err)
|
||||
}
|
||||
if got, want := string(body), bodyWanted; got != want {
|
||||
t.Errorf("Unexpected impersonation payload: got %v but want %v", got, want)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(response))
|
||||
}))
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
headerAuth := r.Header.Get("Authorization")
|
||||
if got, want := headerAuth, "Basic cmJyZ25vZ25yaG9uZ28zYmk0Z2I5Z2hnOWc6bm90c29zZWNyZXQ="; got != want {
|
||||
t.Errorf("got %v but want %v", got, want)
|
||||
}
|
||||
headerContentType := r.Header.Get("Content-Type")
|
||||
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 := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed reading request body: %v.", err)
|
||||
}
|
||||
if got, want := string(body), baseImpersonateCredsReqBody; got != want {
|
||||
t.Errorf("Unexpected exchange payload: got %v but want %v", got, want)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(baseCredsResponseBody))
|
||||
}))
|
||||
}
|
||||
|
||||
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) {
|
||||
type MockSTSResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
IssuedTokenType string `json:"issued_token_type"`
|
||||
TokenType string `json:"token_type"`
|
||||
ExpiresIn int32 `json:"expires_in,omitempty"`
|
||||
Scope string `json:"scopre,omitenpty"`
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
responseBody MockSTSResponse
|
||||
expectToken *oauth2.Token
|
||||
expectErrorMsg string
|
||||
}{
|
||||
{
|
||||
name: "happy case",
|
||||
responseBody: MockSTSResponse{
|
||||
AccessToken: correctAT,
|
||||
IssuedTokenType: "urn:ietf:params:oauth:token-type:access_token",
|
||||
TokenType: "Bearer",
|
||||
ExpiresIn: 3600,
|
||||
Scope: "https://www.googleapis.com/auth/cloud-platform",
|
||||
},
|
||||
expectToken: &oauth2.Token{
|
||||
AccessToken: correctAT,
|
||||
TokenType: "Bearer",
|
||||
Expiry: testNow().Add(time.Duration(3600) * time.Second),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no expiry time on token",
|
||||
responseBody: MockSTSResponse{
|
||||
AccessToken: correctAT,
|
||||
IssuedTokenType: "urn:ietf:params:oauth:token-type:access_token",
|
||||
TokenType: "Bearer",
|
||||
Scope: "https://www.googleapis.com/auth/cloud-platform",
|
||||
},
|
||||
expectToken: nil,
|
||||
expectErrorMsg: "oauth2/google/externalaccount: got invalid expiry from security token service",
|
||||
},
|
||||
{
|
||||
name: "negative expiry time",
|
||||
responseBody: MockSTSResponse{
|
||||
AccessToken: correctAT,
|
||||
IssuedTokenType: "urn:ietf:params:oauth:token-type:access_token",
|
||||
TokenType: "Bearer",
|
||||
ExpiresIn: -1,
|
||||
Scope: "https://www.googleapis.com/auth/cloud-platform",
|
||||
},
|
||||
expectToken: nil,
|
||||
expectErrorMsg: "oauth2/google/externalaccount: got invalid expiry from security token service",
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
config := Config{
|
||||
Audience: "32555940559.apps.googleusercontent.com",
|
||||
SubjectTokenType: "urn:ietf:params:oauth:token-type:id_token",
|
||||
ClientSecret: "notsosecret",
|
||||
ClientID: "rbrgnognrhongo3bi4gb9ghg9g",
|
||||
CredentialSource: &testBaseCredSource,
|
||||
Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"},
|
||||
}
|
||||
|
||||
responseBody, err := json.Marshal(testCase.responseBody)
|
||||
if err != nil {
|
||||
t.Errorf("Invalid response received.")
|
||||
}
|
||||
|
||||
server := testExchangeTokenServer{
|
||||
url: "/",
|
||||
authorization: "Basic cmJyZ25vZ25yaG9uZ28zYmk0Z2I5Z2hnOWc6bm90c29zZWNyZXQ=",
|
||||
contentType: "application/x-www-form-urlencoded",
|
||||
metricsHeader: getExpectedMetricsHeader("file", false, false),
|
||||
body: baseCredsRequestBody,
|
||||
response: string(responseBody),
|
||||
}
|
||||
|
||||
tok, err := run(t, &config, &server)
|
||||
|
||||
if err != nil && err.Error() != testCase.expectErrorMsg {
|
||||
t.Errorf("Error not as expected: got = %v, and want = %v", err, testCase.expectErrorMsg)
|
||||
}
|
||||
validateToken(t, tok, testCase.expectToken)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkforcePoolTokenWithClientID(t *testing.T) {
|
||||
config := Config{
|
||||
Audience: "//iam.googleapis.com/locations/eu/workforcePools/pool-id/providers/provider-id",
|
||||
SubjectTokenType: "urn:ietf:params:oauth:token-type:id_token",
|
||||
ClientSecret: "notsosecret",
|
||||
ClientID: "rbrgnognrhongo3bi4gb9ghg9g",
|
||||
CredentialSource: &testBaseCredSource,
|
||||
Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"},
|
||||
WorkforcePoolUserProject: "myProject",
|
||||
}
|
||||
|
||||
server := testExchangeTokenServer{
|
||||
url: "/",
|
||||
authorization: "Basic cmJyZ25vZ25yaG9uZ28zYmk0Z2I5Z2hnOWc6bm90c29zZWNyZXQ=",
|
||||
contentType: "application/x-www-form-urlencoded",
|
||||
metricsHeader: getExpectedMetricsHeader("file", false, false),
|
||||
body: workforcePoolRequestBodyWithClientId,
|
||||
response: baseCredsResponseBody,
|
||||
}
|
||||
|
||||
tok, err := run(t, &config, &server)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %e", err)
|
||||
}
|
||||
expectToken := oauth2.Token{
|
||||
AccessToken: correctAT,
|
||||
TokenType: "Bearer",
|
||||
Expiry: testNow().Add(time.Duration(3600) * time.Second),
|
||||
}
|
||||
validateToken(t, tok, &expectToken)
|
||||
}
|
||||
|
||||
func TestWorkforcePoolTokenWithoutClientID(t *testing.T) {
|
||||
config := Config{
|
||||
Audience: "//iam.googleapis.com/locations/eu/workforcePools/pool-id/providers/provider-id",
|
||||
SubjectTokenType: "urn:ietf:params:oauth:token-type:id_token",
|
||||
ClientSecret: "notsosecret",
|
||||
CredentialSource: &testBaseCredSource,
|
||||
Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"},
|
||||
WorkforcePoolUserProject: "myProject",
|
||||
}
|
||||
|
||||
server := testExchangeTokenServer{
|
||||
url: "/",
|
||||
authorization: "",
|
||||
contentType: "application/x-www-form-urlencoded",
|
||||
metricsHeader: getExpectedMetricsHeader("file", false, false),
|
||||
body: workforcePoolRequestBodyWithoutClientId,
|
||||
response: baseCredsResponseBody,
|
||||
}
|
||||
|
||||
tok, err := run(t, &config, &server)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %e", err)
|
||||
}
|
||||
expectToken := oauth2.Token{
|
||||
AccessToken: correctAT,
|
||||
TokenType: "Bearer",
|
||||
Expiry: testNow().Add(time.Duration(3600) * time.Second),
|
||||
}
|
||||
validateToken(t, tok, &expectToken)
|
||||
}
|
||||
|
||||
func TestNonworkforceWithWorkforcePoolUserProject(t *testing.T) {
|
||||
config := Config{
|
||||
Audience: "32555940559.apps.googleusercontent.com",
|
||||
SubjectTokenType: "urn:ietf:params:oauth:token-type:id_token",
|
||||
TokenURL: "https://sts.googleapis.com",
|
||||
ClientSecret: "notsosecret",
|
||||
ClientID: "rbrgnognrhongo3bi4gb9ghg9g",
|
||||
CredentialSource: &testBaseCredSource,
|
||||
Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"},
|
||||
WorkforcePoolUserProject: "myProject",
|
||||
}
|
||||
|
||||
_, err := NewTokenSource(context.Background(), config)
|
||||
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error but found none")
|
||||
}
|
||||
if got, want := err.Error(), "oauth2/google/externalaccount: Workforce pool user project should not be set for non-workforce pool credentials"; got != want {
|
||||
t.Errorf("Incorrect error received.\nExpected: %s\nReceived: %s", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkforcePoolCreation(t *testing.T) {
|
||||
var audienceValidityTests = []struct {
|
||||
audience string
|
||||
expectSuccess bool
|
||||
}{
|
||||
{"//iam.googleapis.com/locations/global/workforcePools/pool-id/providers/provider-id", true},
|
||||
{"//iam.googleapis.com/locations/eu/workforcePools/pool-id/providers/provider-id", true},
|
||||
{"//iam.googleapis.com/locations/eu/workforcePools/workloadIdentityPools/providers/provider-id", true},
|
||||
{"identitynamespace:1f12345:my_provider", false},
|
||||
{"//iam.googleapis.com/projects/123456/locations/global/workloadIdentityPools/pool-id/providers/provider-id", false},
|
||||
{"//iam.googleapis.com/projects/123456/locations/eu/workloadIdentityPools/pool-id/providers/provider-id", false},
|
||||
{"//iam.googleapis.com/projects/123456/locations/global/workloadIdentityPools/workforcePools/providers/provider-id", false},
|
||||
{"//iamgoogleapis.com/locations/eu/workforcePools/pool-id/providers/provider-id", false},
|
||||
{"//iam.googleapiscom/locations/eu/workforcePools/pool-id/providers/provider-id", false},
|
||||
{"//iam.googleapis.com/locations/workforcePools/pool-id/providers/provider-id", false},
|
||||
{"//iam.googleapis.com/locations/eu/workforcePool/pool-id/providers/provider-id", false},
|
||||
{"//iam.googleapis.com/locations//workforcePool/pool-id/providers/provider-id", false},
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
for _, tt := range audienceValidityTests {
|
||||
t.Run(" "+tt.audience, func(t *testing.T) { // We prepend a space ahead of the test input when outputting for sake of readability.
|
||||
config := testConfig
|
||||
config.TokenURL = "https://sts.googleapis.com" // Setting the most basic acceptable tokenURL
|
||||
config.ServiceAccountImpersonationURL = "https://iamcredentials.googleapis.com"
|
||||
config.Audience = tt.audience
|
||||
config.WorkforcePoolUserProject = "myProject"
|
||||
_, err := NewTokenSource(ctx, config)
|
||||
|
||||
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")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var impersonationTests = []struct {
|
||||
name string
|
||||
config Config
|
||||
expectedImpersonationBody string
|
||||
expectedMetricsHeader string
|
||||
}{
|
||||
{
|
||||
name: "Base Impersonation",
|
||||
config: Config{
|
||||
Audience: "32555940559.apps.googleusercontent.com",
|
||||
SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt",
|
||||
TokenInfoURL: "http://localhost:8080/v1/tokeninfo",
|
||||
ClientSecret: "notsosecret",
|
||||
ClientID: "rbrgnognrhongo3bi4gb9ghg9g",
|
||||
CredentialSource: &testBaseCredSource,
|
||||
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",
|
||||
config: Config{
|
||||
Audience: "32555940559.apps.googleusercontent.com",
|
||||
SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt",
|
||||
TokenInfoURL: "http://localhost:8080/v1/tokeninfo",
|
||||
ClientSecret: "notsosecret",
|
||||
ClientID: "rbrgnognrhongo3bi4gb9ghg9g",
|
||||
CredentialSource: &testBaseCredSource,
|
||||
Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"},
|
||||
ServiceAccountImpersonationLifetimeSeconds: 10000,
|
||||
},
|
||||
expectedImpersonationBody: "{\"lifetime\":\"10000s\",\"scope\":[\"https://www.googleapis.com/auth/devstorage.full_control\"]}",
|
||||
expectedMetricsHeader: getExpectedMetricsHeader("file", true, true),
|
||||
},
|
||||
}
|
||||
|
||||
func TestImpersonation(t *testing.T) {
|
||||
for _, tt := range impersonationTests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
testImpersonateConfig := tt.config
|
||||
impersonateServer := createImpersonationServer("/", "Bearer Sample.Access.Token", tt.expectedImpersonationBody, baseImpersonateCredsRespBody, t)
|
||||
defer impersonateServer.Close()
|
||||
testImpersonateConfig.ServiceAccountImpersonationURL = impersonateServer.URL
|
||||
|
||||
targetServer := createTargetServer(tt.expectedMetricsHeader, t)
|
||||
defer targetServer.Close()
|
||||
testImpersonateConfig.TokenURL = targetServer.URL
|
||||
|
||||
ourTS, err := testImpersonateConfig.tokenSource(context.Background(), "http")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create TokenSource: %v", err)
|
||||
}
|
||||
|
||||
oldNow := now
|
||||
defer func() { now = oldNow }()
|
||||
now = testNow
|
||||
|
||||
tok, err := ourTS.Token()
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %e", err)
|
||||
}
|
||||
if got, want := tok.AccessToken, "Second.Access.Token"; got != want {
|
||||
t.Errorf("Unexpected access token: got %v, but wanted %v", got, want)
|
||||
}
|
||||
if got, want := tok.TokenType, "Bearer"; got != want {
|
||||
t.Errorf("Unexpected TokenType: got %v, but wanted %v", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var newTokenTests = []struct {
|
||||
name string
|
||||
config Config
|
||||
}{
|
||||
{
|
||||
name: "Missing Audience",
|
||||
config: Config{
|
||||
SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt",
|
||||
TokenInfoURL: "http://localhost:8080/v1/tokeninfo",
|
||||
ClientSecret: "notsosecret",
|
||||
ClientID: "rbrgnognrhongo3bi4gb9ghg9g",
|
||||
CredentialSource: &testBaseCredSource,
|
||||
Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"},
|
||||
ServiceAccountImpersonationLifetimeSeconds: 10000,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Missing Subject Token Type",
|
||||
config: Config{
|
||||
Audience: "32555940559.apps.googleusercontent.com",
|
||||
TokenInfoURL: "http://localhost:8080/v1/tokeninfo",
|
||||
ClientSecret: "notsosecret",
|
||||
ClientID: "rbrgnognrhongo3bi4gb9ghg9g",
|
||||
CredentialSource: &testBaseCredSource,
|
||||
Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"},
|
||||
ServiceAccountImpersonationLifetimeSeconds: 10000,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "No Cred Source",
|
||||
config: Config{
|
||||
Audience: "32555940559.apps.googleusercontent.com",
|
||||
SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt",
|
||||
TokenInfoURL: "http://localhost:8080/v1/tokeninfo",
|
||||
ClientSecret: "notsosecret",
|
||||
ClientID: "rbrgnognrhongo3bi4gb9ghg9g",
|
||||
Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"},
|
||||
ServiceAccountImpersonationLifetimeSeconds: 10000,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Cred Source and Supplier",
|
||||
config: Config{
|
||||
Audience: "32555940559.apps.googleusercontent.com",
|
||||
SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt",
|
||||
TokenInfoURL: "http://localhost:8080/v1/tokeninfo",
|
||||
CredentialSource: &testBaseCredSource,
|
||||
AwsSecurityCredentialsSupplier: testAwsSupplier{},
|
||||
ClientSecret: "notsosecret",
|
||||
ClientID: "rbrgnognrhongo3bi4gb9ghg9g",
|
||||
Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"},
|
||||
ServiceAccountImpersonationLifetimeSeconds: 10000,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func TestNewToken(t *testing.T) {
|
||||
for _, tt := range newTokenTests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
testConfig := tt.config
|
||||
|
||||
_, err := NewTokenSource(context.Background(), testConfig)
|
||||
if err == nil {
|
||||
t.Fatalf("expected error when calling NewToken()")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfig_TokenURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
tokenURL string
|
||||
universeDomain string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
tokenURL: "https://sts.googleapis.com/v1/token",
|
||||
universeDomain: "",
|
||||
want: "https://sts.googleapis.com/v1/token",
|
||||
},
|
||||
{
|
||||
tokenURL: "",
|
||||
universeDomain: "",
|
||||
want: "https://sts.googleapis.com/v1/token",
|
||||
},
|
||||
{
|
||||
tokenURL: "",
|
||||
universeDomain: "googleapis.com",
|
||||
want: "https://sts.googleapis.com/v1/token",
|
||||
},
|
||||
{
|
||||
tokenURL: "",
|
||||
universeDomain: "example.com",
|
||||
want: "https://sts.example.com/v1/token",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
config := &Config{
|
||||
Audience: "//iam.googleapis.com/locations/eu/workforcePools/pool-id/providers/provider-id",
|
||||
SubjectTokenType: "urn:ietf:params:oauth:token-type:id_token",
|
||||
CredentialSource: &testBaseCredSource,
|
||||
Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"},
|
||||
}
|
||||
config.TokenURL = tt.tokenURL
|
||||
config.UniverseDomain = tt.universeDomain
|
||||
config.parse(context.Background())
|
||||
if got := config.TokenURL; got != tt.want {
|
||||
t.Errorf("got %q, want %q", got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
@ -11,7 +11,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
@ -19,7 +18,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
var serviceAccountImpersonationRE = regexp.MustCompile("https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/(.*@.*):generateAccessToken")
|
||||
var serviceAccountImpersonationRE = regexp.MustCompile("https://iamcredentials\\..+/v1/projects/-/serviceAccounts/(.*@.*):generateAccessToken")
|
||||
|
||||
const (
|
||||
executableSupportedMaxVersion = 1
|
||||
@ -39,51 +38,51 @@ func (nce nonCacheableError) Error() string {
|
||||
}
|
||||
|
||||
func missingFieldError(source, field string) error {
|
||||
return fmt.Errorf("oauth2/google: %v missing `%q` field", source, field)
|
||||
return fmt.Errorf("oauth2/google/externalaccount: %v missing `%q` field", source, field)
|
||||
}
|
||||
|
||||
func jsonParsingError(source, data string) error {
|
||||
return fmt.Errorf("oauth2/google: unable to parse %v\nResponse: %v", source, data)
|
||||
return fmt.Errorf("oauth2/google/externalaccount: unable to parse %v\nResponse: %v", source, data)
|
||||
}
|
||||
|
||||
func malformedFailureError() error {
|
||||
return nonCacheableError{"oauth2/google: response must include `error` and `message` fields when unsuccessful"}
|
||||
return nonCacheableError{"oauth2/google/externalaccount: response must include `error` and `message` fields when unsuccessful"}
|
||||
}
|
||||
|
||||
func userDefinedError(code, message string) error {
|
||||
return nonCacheableError{fmt.Sprintf("oauth2/google: response contains unsuccessful response: (%v) %v", code, message)}
|
||||
return nonCacheableError{fmt.Sprintf("oauth2/google/externalaccount: response contains unsuccessful response: (%v) %v", code, message)}
|
||||
}
|
||||
|
||||
func unsupportedVersionError(source string, version int) error {
|
||||
return fmt.Errorf("oauth2/google: %v contains unsupported version: %v", source, version)
|
||||
return fmt.Errorf("oauth2/google/externalaccount: %v contains unsupported version: %v", source, version)
|
||||
}
|
||||
|
||||
func tokenExpiredError() error {
|
||||
return nonCacheableError{"oauth2/google: the token returned by the executable is expired"}
|
||||
return nonCacheableError{"oauth2/google/externalaccount: the token returned by the executable is expired"}
|
||||
}
|
||||
|
||||
func tokenTypeError(source string) error {
|
||||
return fmt.Errorf("oauth2/google: %v contains unsupported token type", source)
|
||||
return fmt.Errorf("oauth2/google/externalaccount: %v contains unsupported token type", source)
|
||||
}
|
||||
|
||||
func exitCodeError(exitCode int) error {
|
||||
return fmt.Errorf("oauth2/google: executable command failed with exit code %v", exitCode)
|
||||
return fmt.Errorf("oauth2/google/externalaccount: executable command failed with exit code %v", exitCode)
|
||||
}
|
||||
|
||||
func executableError(err error) error {
|
||||
return fmt.Errorf("oauth2/google: executable command failed: %v", err)
|
||||
return fmt.Errorf("oauth2/google/externalaccount: executable command failed: %v", err)
|
||||
}
|
||||
|
||||
func executablesDisallowedError() error {
|
||||
return errors.New("oauth2/google: executables need to be explicitly allowed (set GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES to '1') to run")
|
||||
return errors.New("oauth2/google/externalaccount: executables need to be explicitly allowed (set GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES to '1') to run")
|
||||
}
|
||||
|
||||
func timeoutRangeError() error {
|
||||
return errors.New("oauth2/google: invalid `timeout_millis` field — executable timeout must be between 5 and 120 seconds")
|
||||
return errors.New("oauth2/google/externalaccount: invalid `timeout_millis` field — executable timeout must be between 5 and 120 seconds")
|
||||
}
|
||||
|
||||
func commandMissingError() error {
|
||||
return errors.New("oauth2/google: missing `command` field — executable command must be provided")
|
||||
return errors.New("oauth2/google/externalaccount: missing `command` field — executable command must be provided")
|
||||
}
|
||||
|
||||
type environment interface {
|
||||
@ -146,7 +145,7 @@ type executableCredentialSource struct {
|
||||
|
||||
// CreateExecutableCredential creates an executableCredentialSource given an ExecutableConfig.
|
||||
// It also performs defaulting and type conversions.
|
||||
func CreateExecutableCredential(ctx context.Context, ec *ExecutableConfig, config *Config) (executableCredentialSource, error) {
|
||||
func createExecutableCredential(ctx context.Context, ec *ExecutableConfig, config *Config) (executableCredentialSource, error) {
|
||||
if ec.Command == "" {
|
||||
return executableCredentialSource{}, commandMissingError()
|
||||
}
|
||||
@ -258,7 +257,7 @@ func (cs executableCredentialSource) getTokenFromOutputFile() (token string, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
data, err := ioutil.ReadAll(io.LimitReader(file, 1<<20))
|
||||
data, err := io.ReadAll(io.LimitReader(file, 1<<20))
|
||||
if err != nil || len(data) == 0 {
|
||||
// Cachefile exists, but no data found. Get new credential.
|
||||
return "", nil
|
@ -8,13 +8,10 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"sort"
|
||||
"slices"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
type testEnvironment struct {
|
||||
@ -128,7 +125,7 @@ var creationTests = []struct {
|
||||
func TestCreateExecutableCredential(t *testing.T) {
|
||||
for _, tt := range creationTests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ecs, err := CreateExecutableCredential(context.Background(), &tt.executableConfig, nil)
|
||||
ecs, err := createExecutableCredential(context.Background(), &tt.executableConfig, nil)
|
||||
if tt.expectedErr != nil {
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error but found none")
|
||||
@ -169,7 +166,7 @@ var getEnvironmentTests = []struct {
|
||||
config: Config{
|
||||
Audience: "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/oidc",
|
||||
SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt",
|
||||
CredentialSource: CredentialSource{
|
||||
CredentialSource: &CredentialSource{
|
||||
Executable: &ExecutableConfig{
|
||||
Command: "blarg",
|
||||
},
|
||||
@ -193,7 +190,7 @@ var getEnvironmentTests = []struct {
|
||||
Audience: "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/oidc",
|
||||
ServiceAccountImpersonationURL: "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test@project.iam.gserviceaccount.com:generateAccessToken",
|
||||
SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt",
|
||||
CredentialSource: CredentialSource{
|
||||
CredentialSource: &CredentialSource{
|
||||
Executable: &ExecutableConfig{
|
||||
Command: "blarg",
|
||||
OutputFile: "/path/to/generated/cached/credentials",
|
||||
@ -220,7 +217,7 @@ var getEnvironmentTests = []struct {
|
||||
Audience: "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/oidc",
|
||||
ServiceAccountImpersonationURL: "test@project.iam.gserviceaccount.com",
|
||||
SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt",
|
||||
CredentialSource: CredentialSource{
|
||||
CredentialSource: &CredentialSource{
|
||||
Executable: &ExecutableConfig{
|
||||
Command: "blarg",
|
||||
OutputFile: "/path/to/generated/cached/credentials",
|
||||
@ -247,21 +244,19 @@ func TestExecutableCredentialGetEnvironment(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
config := tt.config
|
||||
|
||||
ecs, err := CreateExecutableCredential(context.Background(), config.CredentialSource.Executable, &config)
|
||||
ecs, err := createExecutableCredential(context.Background(), config.CredentialSource.Executable, &config)
|
||||
if err != nil {
|
||||
t.Fatalf("creation failed %v", err)
|
||||
}
|
||||
|
||||
ecs.env = &tt.environment
|
||||
|
||||
// This Transformer sorts a []string.
|
||||
sorter := cmp.Transformer("Sort", func(in []string) []string {
|
||||
out := append([]string(nil), in...) // Copy input to avoid mutating it
|
||||
sort.Strings(out)
|
||||
return out
|
||||
})
|
||||
got := ecs.executableEnvironment()
|
||||
slices.Sort(got)
|
||||
want := tt.expectedEnvironment
|
||||
slices.Sort(want)
|
||||
|
||||
if got, want := ecs.executableEnvironment(), tt.expectedEnvironment; !cmp.Equal(got, want, sorter) {
|
||||
if !slices.Equal(got, want) {
|
||||
t.Errorf("Incorrect environment received.\nReceived: %s\nExpected: %s", got, want)
|
||||
}
|
||||
})
|
||||
@ -471,7 +466,7 @@ func TestRetrieveExecutableSubjectTokenExecutableErrors(t *testing.T) {
|
||||
}
|
||||
|
||||
tfc := testFileConfig
|
||||
tfc.CredentialSource = cs
|
||||
tfc.CredentialSource = &cs
|
||||
|
||||
base, err := tfc.parse(context.Background())
|
||||
if err != nil {
|
||||
@ -578,7 +573,7 @@ func TestRetrieveExecutableSubjectTokenSuccesses(t *testing.T) {
|
||||
}
|
||||
|
||||
tfc := testFileConfig
|
||||
tfc.CredentialSource = cs
|
||||
tfc.CredentialSource = &cs
|
||||
|
||||
base, err := tfc.parse(context.Background())
|
||||
if err != nil {
|
||||
@ -614,7 +609,7 @@ func TestRetrieveExecutableSubjectTokenSuccesses(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRetrieveOutputFileSubjectTokenNotJSON(t *testing.T) {
|
||||
outputFile, err := ioutil.TempFile("testdata", "result.*.json")
|
||||
outputFile, err := os.CreateTemp("testdata", "result.*.json")
|
||||
if err != nil {
|
||||
t.Fatalf("Tempfile failed: %v", err)
|
||||
}
|
||||
@ -629,7 +624,7 @@ func TestRetrieveOutputFileSubjectTokenNotJSON(t *testing.T) {
|
||||
}
|
||||
|
||||
tfc := testFileConfig
|
||||
tfc.CredentialSource = cs
|
||||
tfc.CredentialSource = &cs
|
||||
|
||||
base, err := tfc.parse(context.Background())
|
||||
if err != nil {
|
||||
@ -654,7 +649,7 @@ func TestRetrieveOutputFileSubjectTokenNotJSON(t *testing.T) {
|
||||
if _, err = base.subjectToken(); err == nil {
|
||||
t.Fatalf("Expected error but found none")
|
||||
} else if got, want := err.Error(), jsonParsingError(outputFileSource, "tokentokentoken").Error(); got != want {
|
||||
t.Errorf("Incorrect error received.\nExpected: %s\nRecieved: %s", want, got)
|
||||
t.Errorf("Incorrect error received.\nExpected: %s\nReceived: %s", want, got)
|
||||
}
|
||||
|
||||
_, deadlineSet := te.getDeadline()
|
||||
@ -763,7 +758,7 @@ var cacheFailureTests = []struct {
|
||||
func TestRetrieveOutputFileSubjectTokenFailureTests(t *testing.T) {
|
||||
for _, tt := range cacheFailureTests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
outputFile, err := ioutil.TempFile("testdata", "result.*.json")
|
||||
outputFile, err := os.CreateTemp("testdata", "result.*.json")
|
||||
if err != nil {
|
||||
t.Fatalf("Tempfile failed: %v", err)
|
||||
}
|
||||
@ -778,7 +773,7 @@ func TestRetrieveOutputFileSubjectTokenFailureTests(t *testing.T) {
|
||||
}
|
||||
|
||||
tfc := testFileConfig
|
||||
tfc.CredentialSource = cs
|
||||
tfc.CredentialSource = &cs
|
||||
|
||||
base, err := tfc.parse(context.Background())
|
||||
if err != nil {
|
||||
@ -801,7 +796,7 @@ func TestRetrieveOutputFileSubjectTokenFailureTests(t *testing.T) {
|
||||
if _, err = ecs.subjectToken(); err == nil {
|
||||
t.Errorf("Expected error but found none")
|
||||
} else if got, want := err.Error(), tt.expectedErr.Error(); got != want {
|
||||
t.Errorf("Incorrect error received.\nExpected: %s\nRecieved: %s", want, got)
|
||||
t.Errorf("Incorrect error received.\nExpected: %s\nReceived: %s", want, got)
|
||||
}
|
||||
|
||||
if _, deadlineSet := te.getDeadline(); deadlineSet {
|
||||
@ -866,7 +861,7 @@ var invalidCacheTests = []struct {
|
||||
func TestRetrieveOutputFileSubjectTokenInvalidCache(t *testing.T) {
|
||||
for _, tt := range invalidCacheTests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
outputFile, err := ioutil.TempFile("testdata", "result.*.json")
|
||||
outputFile, err := os.CreateTemp("testdata", "result.*.json")
|
||||
if err != nil {
|
||||
t.Fatalf("Tempfile failed: %v", err)
|
||||
}
|
||||
@ -881,7 +876,7 @@ func TestRetrieveOutputFileSubjectTokenInvalidCache(t *testing.T) {
|
||||
}
|
||||
|
||||
tfc := testFileConfig
|
||||
tfc.CredentialSource = cs
|
||||
tfc.CredentialSource = &cs
|
||||
|
||||
base, err := tfc.parse(context.Background())
|
||||
if err != nil {
|
||||
@ -923,7 +918,7 @@ func TestRetrieveOutputFileSubjectTokenInvalidCache(t *testing.T) {
|
||||
}
|
||||
|
||||
if got, want := out, "tokentokentoken"; got != want {
|
||||
t.Errorf("Incorrect token received.\nExpected: %s\nRecieved: %s", want, got)
|
||||
t.Errorf("Incorrect token received.\nExpected: %s\nReceived: %s", want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -970,8 +965,7 @@ var cacheSuccessTests = []struct {
|
||||
func TestRetrieveOutputFileSubjectTokenJwt(t *testing.T) {
|
||||
for _, tt := range cacheSuccessTests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
outputFile, err := ioutil.TempFile("testdata", "result.*.json")
|
||||
outputFile, err := os.CreateTemp("testdata", "result.*.json")
|
||||
if err != nil {
|
||||
t.Fatalf("Tempfile failed: %v", err)
|
||||
}
|
||||
@ -986,7 +980,7 @@ func TestRetrieveOutputFileSubjectTokenJwt(t *testing.T) {
|
||||
}
|
||||
|
||||
tfc := testFileConfig
|
||||
tfc.CredentialSource = cs
|
||||
tfc.CredentialSource = &cs
|
||||
|
||||
base, err := tfc.parse(context.Background())
|
||||
if err != nil {
|
||||
@ -1012,7 +1006,7 @@ func TestRetrieveOutputFileSubjectTokenJwt(t *testing.T) {
|
||||
if out, err := ecs.subjectToken(); err != nil {
|
||||
t.Errorf("retrieveSubjectToken() failed: %v", err)
|
||||
} else if got, want := out, "tokentokentoken"; got != want {
|
||||
t.Errorf("Incorrect token received.\nExpected: %s\nRecieved: %s", want, got)
|
||||
t.Errorf("Incorrect token received.\nExpected: %s\nReceived: %s", want, got)
|
||||
}
|
||||
|
||||
if _, deadlineSet := te.getDeadline(); deadlineSet {
|
||||
@ -1021,3 +1015,37 @@ func TestRetrieveOutputFileSubjectTokenJwt(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceAccountImpersonationRE(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
serviceAccountImpersonationURL string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "universe domain Google Default Universe (GDU) googleapis.com",
|
||||
serviceAccountImpersonationURL: "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test@project.iam.gserviceaccount.com:generateAccessToken",
|
||||
want: "test@project.iam.gserviceaccount.com",
|
||||
},
|
||||
{
|
||||
name: "email does not match",
|
||||
serviceAccountImpersonationURL: "test@project.iam.gserviceaccount.com",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "universe domain non-GDU",
|
||||
serviceAccountImpersonationURL: "https://iamcredentials.apis-tpclp.goog/v1/projects/-/serviceAccounts/test@project.iam.gserviceaccount.com:generateAccessToken",
|
||||
want: "test@project.iam.gserviceaccount.com",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
matches := serviceAccountImpersonationRE.FindStringSubmatch(tt.serviceAccountImpersonationURL)
|
||||
if matches == nil {
|
||||
if tt.want != "" {
|
||||
t.Errorf("%q: got nil, want %q", tt.name, tt.want)
|
||||
}
|
||||
} else if matches[1] != tt.want {
|
||||
t.Errorf("%q: got %q, want %q", tt.name, matches[1], tt.want)
|
||||
}
|
||||
}
|
||||
}
|
@ -10,13 +10,12 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
)
|
||||
|
||||
type fileCredentialSource struct {
|
||||
File string
|
||||
Format format
|
||||
Format Format
|
||||
}
|
||||
|
||||
func (cs fileCredentialSource) credentialSourceType() string {
|
||||
@ -26,28 +25,28 @@ func (cs fileCredentialSource) credentialSourceType() string {
|
||||
func (cs fileCredentialSource) subjectToken() (string, error) {
|
||||
tokenFile, err := os.Open(cs.File)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("oauth2/google: failed to open credential file %q", cs.File)
|
||||
return "", fmt.Errorf("oauth2/google/externalaccount: failed to open credential file %q", cs.File)
|
||||
}
|
||||
defer tokenFile.Close()
|
||||
tokenBytes, err := ioutil.ReadAll(io.LimitReader(tokenFile, 1<<20))
|
||||
tokenBytes, err := io.ReadAll(io.LimitReader(tokenFile, 1<<20))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("oauth2/google: failed to read credential file: %v", err)
|
||||
return "", fmt.Errorf("oauth2/google/externalaccount: failed to read credential file: %v", err)
|
||||
}
|
||||
tokenBytes = bytes.TrimSpace(tokenBytes)
|
||||
switch cs.Format.Type {
|
||||
case "json":
|
||||
jsonData := make(map[string]interface{})
|
||||
jsonData := make(map[string]any)
|
||||
err = json.Unmarshal(tokenBytes, &jsonData)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("oauth2/google: failed to unmarshal subject token file: %v", err)
|
||||
return "", fmt.Errorf("oauth2/google/externalaccount: failed to unmarshal subject token file: %v", err)
|
||||
}
|
||||
val, ok := jsonData[cs.Format.SubjectTokenFieldName]
|
||||
if !ok {
|
||||
return "", errors.New("oauth2/google: provided subject_token_field_name not found in credentials")
|
||||
return "", errors.New("oauth2/google/externalaccount: provided subject_token_field_name not found in credentials")
|
||||
}
|
||||
token, ok := val.(string)
|
||||
if !ok {
|
||||
return "", errors.New("oauth2/google: improperly formatted subject token")
|
||||
return "", errors.New("oauth2/google/externalaccount: improperly formatted subject token")
|
||||
}
|
||||
return token, nil
|
||||
case "text":
|
||||
@ -55,7 +54,7 @@ func (cs fileCredentialSource) subjectToken() (string, error) {
|
||||
case "":
|
||||
return string(tokenBytes), nil
|
||||
default:
|
||||
return "", errors.New("oauth2/google: invalid credential_source file format type")
|
||||
return "", errors.New("oauth2/google/externalaccount: invalid credential_source file format type")
|
||||
}
|
||||
|
||||
}
|
@ -36,7 +36,7 @@ func TestRetrieveFileSubjectToken(t *testing.T) {
|
||||
name: "TextFileSource",
|
||||
cs: CredentialSource{
|
||||
File: textBaseCredPath,
|
||||
Format: format{Type: fileTypeText},
|
||||
Format: Format{Type: fileTypeText},
|
||||
},
|
||||
want: "street123",
|
||||
},
|
||||
@ -44,7 +44,7 @@ func TestRetrieveFileSubjectToken(t *testing.T) {
|
||||
name: "JSONFileSource",
|
||||
cs: CredentialSource{
|
||||
File: jsonBaseCredPath,
|
||||
Format: format{Type: fileTypeJSON, SubjectTokenFieldName: "SubjToken"},
|
||||
Format: Format{Type: fileTypeJSON, SubjectTokenFieldName: "SubjToken"},
|
||||
},
|
||||
want: "321road",
|
||||
},
|
||||
@ -53,7 +53,7 @@ func TestRetrieveFileSubjectToken(t *testing.T) {
|
||||
for _, test := range fileSourceTests {
|
||||
test := test
|
||||
tfc := testFileConfig
|
||||
tfc.CredentialSource = test.cs
|
||||
tfc.CredentialSource = &test.cs
|
||||
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
base, err := tfc.parse(context.Background())
|
@ -7,8 +7,6 @@ package externalaccount
|
||||
import (
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestGoVersion(t *testing.T) {
|
||||
@ -40,8 +38,8 @@ func TestGoVersion(t *testing.T) {
|
||||
} {
|
||||
version = tst.v
|
||||
got := goVersion()
|
||||
if diff := cmp.Diff(got, tst.want); diff != "" {
|
||||
t.Errorf("got(-),want(+):\n%s", diff)
|
||||
if got != tst.want {
|
||||
t.Errorf("go version = %q, want = %q", got, tst.want)
|
||||
}
|
||||
}
|
||||
version = runtime.Version
|
21
google/externalaccount/programmaticrefreshcredsource.go
Normal file
21
google/externalaccount/programmaticrefreshcredsource.go
Normal file
@ -0,0 +1,21 @@
|
||||
// Copyright 2024 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 "context"
|
||||
|
||||
type programmaticRefreshCredentialSource struct {
|
||||
supplierOptions SupplierOptions
|
||||
subjectTokenSupplier SubjectTokenSupplier
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func (cs programmaticRefreshCredentialSource) credentialSourceType() string {
|
||||
return "programmatic"
|
||||
}
|
||||
|
||||
func (cs programmaticRefreshCredentialSource) subjectToken() (string, error) {
|
||||
return cs.subjectTokenSupplier.SubjectToken(cs.ctx, cs.supplierOptions)
|
||||
}
|
122
google/externalaccount/programmaticrefreshcredsource_test.go
Normal file
122
google/externalaccount/programmaticrefreshcredsource_test.go
Normal file
@ -0,0 +1,122 @@
|
||||
// Copyright 2024 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 (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRetrieveSubjectToken_ProgrammaticAuth(t *testing.T) {
|
||||
tfc := testConfig
|
||||
|
||||
tfc.SubjectTokenSupplier = testSubjectTokenSupplier{
|
||||
subjectToken: "subjectToken",
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
if out != "subjectToken" {
|
||||
t.Errorf("subjectToken = \n%q\n want \nSubjectToken", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetrieveSubjectToken_ProgrammaticAuthFails(t *testing.T) {
|
||||
tfc := testConfig
|
||||
testError := errors.New("test error")
|
||||
|
||||
tfc.SubjectTokenSupplier = testSubjectTokenSupplier{
|
||||
err: testError,
|
||||
}
|
||||
|
||||
base, err := tfc.parse(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("parse() failed %v", err)
|
||||
}
|
||||
|
||||
_, err = base.subjectToken()
|
||||
if err == nil {
|
||||
t.Fatalf("subjectToken() should have failed")
|
||||
}
|
||||
if testError != err {
|
||||
t.Errorf("subjectToken = %e, want %e", err, testError)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetrieveSubjectToken_ProgrammaticAuthOptions(t *testing.T) {
|
||||
tfc := testConfig
|
||||
expectedOptions := SupplierOptions{Audience: tfc.Audience, SubjectTokenType: tfc.SubjectTokenType}
|
||||
|
||||
tfc.SubjectTokenSupplier = testSubjectTokenSupplier{
|
||||
subjectToken: "subjectToken",
|
||||
expectedOptions: &expectedOptions,
|
||||
}
|
||||
|
||||
base, err := tfc.parse(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("parse() failed %v", err)
|
||||
}
|
||||
|
||||
_, err = base.subjectToken()
|
||||
if err != nil {
|
||||
t.Fatalf("retrieveSubjectToken() failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetrieveSubjectToken_ProgrammaticAuthContext(t *testing.T) {
|
||||
tfc := testConfig
|
||||
ctx := context.Background()
|
||||
|
||||
tfc.SubjectTokenSupplier = testSubjectTokenSupplier{
|
||||
subjectToken: "subjectToken",
|
||||
expectedContext: ctx,
|
||||
}
|
||||
|
||||
base, err := tfc.parse(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("parse() failed %v", err)
|
||||
}
|
||||
|
||||
_, err = base.subjectToken()
|
||||
if err != nil {
|
||||
t.Fatalf("retrieveSubjectToken() failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
type testSubjectTokenSupplier struct {
|
||||
err error
|
||||
subjectToken string
|
||||
expectedOptions *SupplierOptions
|
||||
expectedContext context.Context
|
||||
}
|
||||
|
||||
func (supp testSubjectTokenSupplier) SubjectToken(ctx context.Context, options SupplierOptions) (string, error) {
|
||||
if supp.err != nil {
|
||||
return "", supp.err
|
||||
}
|
||||
if supp.expectedOptions != nil {
|
||||
if supp.expectedOptions.Audience != options.Audience {
|
||||
return "", errors.New("Audience does not match")
|
||||
}
|
||||
if supp.expectedOptions.SubjectTokenType != options.SubjectTokenType {
|
||||
return "", errors.New("Audience does not match")
|
||||
}
|
||||
}
|
||||
if supp.expectedContext != nil {
|
||||
if supp.expectedContext != ctx {
|
||||
return "", errors.New("Context does not match")
|
||||
}
|
||||
}
|
||||
return supp.subjectToken, nil
|
||||
}
|
@ -10,7 +10,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
@ -19,7 +18,7 @@ import (
|
||||
type urlCredentialSource struct {
|
||||
URL string
|
||||
Headers map[string]string
|
||||
Format format
|
||||
Format Format
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
@ -31,7 +30,7 @@ func (cs urlCredentialSource) subjectToken() (string, error) {
|
||||
client := oauth2.NewClient(cs.ctx, nil)
|
||||
req, err := http.NewRequest("GET", cs.URL, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("oauth2/google: HTTP request for URL-sourced credential failed: %v", err)
|
||||
return "", fmt.Errorf("oauth2/google/externalaccount: HTTP request for URL-sourced credential failed: %v", err)
|
||||
}
|
||||
req = req.WithContext(cs.ctx)
|
||||
|
||||
@ -40,32 +39,32 @@ func (cs urlCredentialSource) subjectToken() (string, error) {
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("oauth2/google: invalid response when retrieving subject token: %v", err)
|
||||
return "", fmt.Errorf("oauth2/google/externalaccount: invalid response when retrieving subject token: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||
respBody, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("oauth2/google: invalid body in subject token URL query: %v", err)
|
||||
return "", fmt.Errorf("oauth2/google/externalaccount: invalid body in subject token URL query: %v", err)
|
||||
}
|
||||
if c := resp.StatusCode; c < 200 || c > 299 {
|
||||
return "", fmt.Errorf("oauth2/google: status code %d: %s", c, respBody)
|
||||
return "", fmt.Errorf("oauth2/google/externalaccount: status code %d: %s", c, respBody)
|
||||
}
|
||||
|
||||
switch cs.Format.Type {
|
||||
case "json":
|
||||
jsonData := make(map[string]interface{})
|
||||
jsonData := make(map[string]any)
|
||||
err = json.Unmarshal(respBody, &jsonData)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("oauth2/google: failed to unmarshal subject token file: %v", err)
|
||||
return "", fmt.Errorf("oauth2/google/externalaccount: failed to unmarshal subject token file: %v", err)
|
||||
}
|
||||
val, ok := jsonData[cs.Format.SubjectTokenFieldName]
|
||||
if !ok {
|
||||
return "", errors.New("oauth2/google: provided subject_token_field_name not found in credentials")
|
||||
return "", errors.New("oauth2/google/externalaccount: provided subject_token_field_name not found in credentials")
|
||||
}
|
||||
token, ok := val.(string)
|
||||
if !ok {
|
||||
return "", errors.New("oauth2/google: improperly formatted subject token")
|
||||
return "", errors.New("oauth2/google/externalaccount: improperly formatted subject token")
|
||||
}
|
||||
return token, nil
|
||||
case "text":
|
||||
@ -73,7 +72,7 @@ func (cs urlCredentialSource) subjectToken() (string, error) {
|
||||
case "":
|
||||
return string(respBody), nil
|
||||
default:
|
||||
return "", errors.New("oauth2/google: invalid credential_source file format type")
|
||||
return "", errors.New("oauth2/google/externalaccount: invalid credential_source file format type")
|
||||
}
|
||||
|
||||
}
|
@ -28,11 +28,11 @@ func TestRetrieveURLSubjectToken_Text(t *testing.T) {
|
||||
heads["Metadata"] = "True"
|
||||
cs := CredentialSource{
|
||||
URL: ts.URL,
|
||||
Format: format{Type: fileTypeText},
|
||||
Format: Format{Type: fileTypeText},
|
||||
Headers: heads,
|
||||
}
|
||||
tfc := testFileConfig
|
||||
tfc.CredentialSource = cs
|
||||
tfc.CredentialSource = &cs
|
||||
|
||||
base, err := tfc.parse(context.Background())
|
||||
if err != nil {
|
||||
@ -60,7 +60,7 @@ func TestRetrieveURLSubjectToken_Untyped(t *testing.T) {
|
||||
URL: ts.URL,
|
||||
}
|
||||
tfc := testFileConfig
|
||||
tfc.CredentialSource = cs
|
||||
tfc.CredentialSource = &cs
|
||||
|
||||
base, err := tfc.parse(context.Background())
|
||||
if err != nil {
|
||||
@ -93,10 +93,10 @@ func TestRetrieveURLSubjectToken_JSON(t *testing.T) {
|
||||
}))
|
||||
cs := CredentialSource{
|
||||
URL: ts.URL,
|
||||
Format: format{Type: fileTypeJSON, SubjectTokenFieldName: "SubjToken"},
|
||||
Format: Format{Type: fileTypeJSON, SubjectTokenFieldName: "SubjToken"},
|
||||
}
|
||||
tfc := testFileConfig
|
||||
tfc.CredentialSource = cs
|
||||
tfc.CredentialSource = &cs
|
||||
|
||||
base, err := tfc.parse(context.Background())
|
||||
if err != nil {
|
||||
@ -115,10 +115,10 @@ func TestRetrieveURLSubjectToken_JSON(t *testing.T) {
|
||||
func TestURLCredential_CredentialSourceType(t *testing.T) {
|
||||
cs := CredentialSource{
|
||||
URL: "http://example.com",
|
||||
Format: format{Type: fileTypeText},
|
||||
Format: Format{Type: fileTypeText},
|
||||
}
|
||||
tfc := testFileConfig
|
||||
tfc.CredentialSource = cs
|
||||
tfc.CredentialSource = &cs
|
||||
|
||||
base, err := tfc.parse(context.Background())
|
||||
if err != nil {
|
@ -15,8 +15,9 @@ import (
|
||||
|
||||
"cloud.google.com/go/compute/metadata"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/google/internal/externalaccount"
|
||||
"golang.org/x/oauth2/google/externalaccount"
|
||||
"golang.org/x/oauth2/google/internal/externalaccountauthorizeduser"
|
||||
"golang.org/x/oauth2/google/internal/impersonate"
|
||||
"golang.org/x/oauth2/jwt"
|
||||
)
|
||||
|
||||
@ -200,12 +201,12 @@ func (f *credentialsFile) tokenSource(ctx context.Context, params CredentialsPar
|
||||
ServiceAccountImpersonationLifetimeSeconds: f.ServiceAccountImpersonation.TokenLifetimeSeconds,
|
||||
ClientSecret: f.ClientSecret,
|
||||
ClientID: f.ClientID,
|
||||
CredentialSource: f.CredentialSource,
|
||||
CredentialSource: &f.CredentialSource,
|
||||
QuotaProjectID: f.QuotaProjectID,
|
||||
Scopes: params.Scopes,
|
||||
WorkforcePoolUserProject: f.WorkforcePoolUserProject,
|
||||
}
|
||||
return cfg.TokenSource(ctx)
|
||||
return externalaccount.NewTokenSource(ctx, *cfg)
|
||||
case externalAccountAuthorizedUserKey:
|
||||
cfg := &externalaccountauthorizeduser.Config{
|
||||
Audience: f.Audience,
|
||||
@ -228,7 +229,7 @@ func (f *credentialsFile) tokenSource(ctx context.Context, params CredentialsPar
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
imp := externalaccount.ImpersonateTokenSource{
|
||||
imp := impersonate.ImpersonateTokenSource{
|
||||
Ctx: ctx,
|
||||
URL: f.ServiceAccountImpersonationURL,
|
||||
Scopes: params.Scopes,
|
||||
@ -251,7 +252,10 @@ func (f *credentialsFile) tokenSource(ctx context.Context, params CredentialsPar
|
||||
// Further information about retrieving access tokens from the GCE metadata
|
||||
// server can be found at https://cloud.google.com/compute/docs/authentication.
|
||||
func ComputeTokenSource(account string, scope ...string) oauth2.TokenSource {
|
||||
return computeTokenSource(account, 0, scope...)
|
||||
// refresh 3 minutes and 45 seconds early. The shortest MDS cache is currently 4 minutes, so any
|
||||
// refreshes earlier are a waste of compute.
|
||||
earlyExpirySecs := 225 * time.Second
|
||||
return computeTokenSource(account, earlyExpirySecs, scope...)
|
||||
}
|
||||
|
||||
func computeTokenSource(account string, earlyExpiry time.Duration, scope ...string) oauth2.TokenSource {
|
||||
@ -281,27 +285,23 @@ func (cs computeSource) Token() (*oauth2.Token, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var res struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
ExpiresInSec int `json:"expires_in"`
|
||||
TokenType string `json:"token_type"`
|
||||
}
|
||||
var res oauth2.Token
|
||||
err = json.NewDecoder(strings.NewReader(tokenJSON)).Decode(&res)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("oauth2/google: invalid token JSON from metadata: %v", err)
|
||||
}
|
||||
if res.ExpiresInSec == 0 || res.AccessToken == "" {
|
||||
if res.ExpiresIn == 0 || res.AccessToken == "" {
|
||||
return nil, fmt.Errorf("oauth2/google: incomplete token received from metadata")
|
||||
}
|
||||
tok := &oauth2.Token{
|
||||
AccessToken: res.AccessToken,
|
||||
TokenType: res.TokenType,
|
||||
Expiry: time.Now().Add(time.Duration(res.ExpiresInSec) * time.Second),
|
||||
Expiry: time.Now().Add(time.Duration(res.ExpiresIn) * time.Second),
|
||||
}
|
||||
// NOTE(cbro): add hidden metadata about where the token is from.
|
||||
// This is needed for detection by client libraries to know that credentials come from the metadata server.
|
||||
// This may be removed in a future version of this library.
|
||||
return tok.WithExtra(map[string]interface{}{
|
||||
return tok.WithExtra(map[string]any{
|
||||
"oauth2.google.tokenSource": "compute-metadata",
|
||||
"oauth2.google.serviceAccount": acct,
|
||||
}), nil
|
||||
|
@ -5,6 +5,8 @@
|
||||
package google
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
@ -70,7 +72,7 @@ func TestConfigFromJSON(t *testing.T) {
|
||||
t.Errorf("ClientSecret = %q; want %q", got, want)
|
||||
}
|
||||
if got, want := conf.RedirectURL, "https://www.example.com/oauth2callback"; got != want {
|
||||
t.Errorf("RedictURL = %q; want %q", got, want)
|
||||
t.Errorf("RedirectURL = %q; want %q", got, want)
|
||||
}
|
||||
if got, want := strings.Join(conf.Scopes, ","), "scope1,scope2"; got != want {
|
||||
t.Errorf("Scopes = %q; want %q", got, want)
|
||||
@ -137,3 +139,21 @@ func TestJWTConfigFromJSONNoAudience(t *testing.T) {
|
||||
t.Errorf("Audience = %q; want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestComputeTokenSource(t *testing.T) {
|
||||
tokenPath := "/computeMetadata/v1/instance/service-accounts/default/token"
|
||||
tokenResponseBody := `{"access_token":"Sample.Access.Token","token_type":"Bearer","expires_in":3600}`
|
||||
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != tokenPath {
|
||||
t.Errorf("got %s, want %s", r.URL.Path, tokenPath)
|
||||
}
|
||||
w.Write([]byte(tokenResponseBody))
|
||||
}))
|
||||
defer s.Close()
|
||||
t.Setenv("GCE_METADATA_HOST", strings.TrimPrefix(s.URL, "http://"))
|
||||
ts := ComputeTokenSource("")
|
||||
_, err := ts.Token()
|
||||
if err != nil {
|
||||
t.Errorf("ts.Token() = %v", err)
|
||||
}
|
||||
}
|
||||
|
@ -1,254 +0,0 @@
|
||||
// Copyright 2020 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 (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"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()
|
||||
}
|
||||
|
||||
// Config stores the configuration for fetching tokens with external credentials.
|
||||
type Config struct {
|
||||
// Audience is the Secure Token Service (STS) audience which contains the resource name for the workload
|
||||
// identity pool or the workforce pool and the provider identifier in that pool.
|
||||
Audience string
|
||||
// SubjectTokenType is the STS token type based on the Oauth2.0 token exchange spec
|
||||
// e.g. `urn:ietf:params:oauth:token-type:jwt`.
|
||||
SubjectTokenType string
|
||||
// TokenURL is the STS token exchange endpoint.
|
||||
TokenURL string
|
||||
// TokenInfoURL is the token_info endpoint used to retrieve the account related information (
|
||||
// user attributes like account identifier, eg. email, username, uid, etc). This is
|
||||
// needed for gCloud session account identification.
|
||||
TokenInfoURL string
|
||||
// ServiceAccountImpersonationURL is the URL for the service account impersonation request. This is only
|
||||
// required for workload identity pools when APIs to be accessed have not integrated with UberMint.
|
||||
ServiceAccountImpersonationURL string
|
||||
// ServiceAccountImpersonationLifetimeSeconds is the number of seconds the service account impersonation
|
||||
// token will be valid for.
|
||||
ServiceAccountImpersonationLifetimeSeconds int
|
||||
// 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
|
||||
// ClientID is only required in conjunction with ClientSecret, as described above.
|
||||
ClientID string
|
||||
// CredentialSource contains the necessary information to retrieve the token itself, as well
|
||||
// as some environmental information.
|
||||
CredentialSource CredentialSource
|
||||
// QuotaProjectID is injected by gCloud. If the value is non-empty, the Auth libraries
|
||||
// will set the x-goog-user-project which overrides the project associated with the credentials.
|
||||
QuotaProjectID string
|
||||
// Scopes contains the desired scopes for the returned access token.
|
||||
Scopes []string
|
||||
// The optional workforce pool user project number when the credential
|
||||
// corresponds to a workforce pool and not a workload identity pool.
|
||||
// The underlying principal must still have serviceusage.services.use IAM
|
||||
// permission to use the project for billing/quota.
|
||||
WorkforcePoolUserProject string
|
||||
}
|
||||
|
||||
var (
|
||||
validWorkforceAudiencePattern *regexp.Regexp = regexp.MustCompile(`//iam\.googleapis\.com/locations/[^/]+/workforcePools/`)
|
||||
)
|
||||
|
||||
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, "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, scheme string) (oauth2.TokenSource, error) {
|
||||
if c.WorkforcePoolUserProject != "" {
|
||||
valid := validateWorkforceAudience(c.Audience)
|
||||
if !valid {
|
||||
return nil, fmt.Errorf("oauth2/google: workforce_pool_user_project should not be set for non-workforce pool credentials")
|
||||
}
|
||||
}
|
||||
|
||||
ts := tokenSource{
|
||||
ctx: ctx,
|
||||
conf: c,
|
||||
}
|
||||
if c.ServiceAccountImpersonationURL == "" {
|
||||
return oauth2.ReuseTokenSource(nil, ts), nil
|
||||
}
|
||||
scopes := c.Scopes
|
||||
ts.conf.Scopes = []string{"https://www.googleapis.com/auth/cloud-platform"}
|
||||
imp := ImpersonateTokenSource{
|
||||
Ctx: ctx,
|
||||
URL: c.ServiceAccountImpersonationURL,
|
||||
Scopes: scopes,
|
||||
Ts: oauth2.ReuseTokenSource(nil, ts),
|
||||
TokenLifetimeSeconds: c.ServiceAccountImpersonationLifetimeSeconds,
|
||||
}
|
||||
return oauth2.ReuseTokenSource(nil, imp), nil
|
||||
}
|
||||
|
||||
// Subject token file types.
|
||||
const (
|
||||
fileTypeText = "text"
|
||||
fileTypeJSON = "json"
|
||||
)
|
||||
|
||||
type format struct {
|
||||
// Type is either "text" or "json". When not provided "text" type is assumed.
|
||||
Type string `json:"type"`
|
||||
// SubjectTokenFieldName is only required for JSON format. This would be "access_token" for azure.
|
||||
SubjectTokenFieldName string `json:"subject_token_field_name"`
|
||||
}
|
||||
|
||||
// CredentialSource stores the information necessary to retrieve the credentials for the STS exchange.
|
||||
// One field amongst File, URL, and Executable should be filled, depending on the kind of credential in question.
|
||||
// The EnvironmentID should start with AWS if being used for an AWS credential.
|
||||
type CredentialSource struct {
|
||||
File string `json:"file"`
|
||||
|
||||
URL string `json:"url"`
|
||||
Headers map[string]string `json:"headers"`
|
||||
|
||||
Executable *ExecutableConfig `json:"executable"`
|
||||
|
||||
EnvironmentID string `json:"environment_id"`
|
||||
RegionURL string `json:"region_url"`
|
||||
RegionalCredVerificationURL string `json:"regional_cred_verification_url"`
|
||||
CredVerificationURL string `json:"cred_verification_url"`
|
||||
IMDSv2SessionTokenURL string `json:"imdsv2_session_token_url"`
|
||||
Format format `json:"format"`
|
||||
}
|
||||
|
||||
type ExecutableConfig struct {
|
||||
Command string `json:"command"`
|
||||
TimeoutMillis *int `json:"timeout_millis"`
|
||||
OutputFile string `json:"output_file"`
|
||||
}
|
||||
|
||||
// parse determines the type of CredentialSource needed.
|
||||
func (c *Config) parse(ctx context.Context) (baseCredentialSource, error) {
|
||||
if len(c.CredentialSource.EnvironmentID) > 3 && c.CredentialSource.EnvironmentID[:3] == "aws" {
|
||||
if awsVersion, err := strconv.Atoi(c.CredentialSource.EnvironmentID[3:]); err == nil {
|
||||
if awsVersion != 1 {
|
||||
return nil, fmt.Errorf("oauth2/google: aws version '%d' is not supported in the current build", awsVersion)
|
||||
}
|
||||
|
||||
awsCredSource := awsCredentialSource{
|
||||
EnvironmentID: c.CredentialSource.EnvironmentID,
|
||||
RegionURL: c.CredentialSource.RegionURL,
|
||||
RegionalCredVerificationURL: c.CredentialSource.RegionalCredVerificationURL,
|
||||
CredVerificationURL: c.CredentialSource.URL,
|
||||
TargetResource: c.Audience,
|
||||
ctx: ctx,
|
||||
}
|
||||
if c.CredentialSource.IMDSv2SessionTokenURL != "" {
|
||||
awsCredSource.IMDSv2SessionTokenURL = c.CredentialSource.IMDSv2SessionTokenURL
|
||||
}
|
||||
|
||||
return awsCredSource, nil
|
||||
}
|
||||
} else if c.CredentialSource.File != "" {
|
||||
return fileCredentialSource{File: c.CredentialSource.File, Format: c.CredentialSource.Format}, nil
|
||||
} else if c.CredentialSource.URL != "" {
|
||||
return urlCredentialSource{URL: c.CredentialSource.URL, Headers: c.CredentialSource.Headers, Format: c.CredentialSource.Format, ctx: ctx}, nil
|
||||
} else if c.CredentialSource.Executable != nil {
|
||||
return CreateExecutableCredential(ctx, c.CredentialSource.Executable, c)
|
||||
}
|
||||
return nil, fmt.Errorf("oauth2/google: unable to parse credential source")
|
||||
}
|
||||
|
||||
type baseCredentialSource interface {
|
||||
credentialSourceType() string
|
||||
subjectToken() (string, error)
|
||||
}
|
||||
|
||||
// tokenSource is the source that handles external credentials. It is used to retrieve Tokens.
|
||||
type tokenSource struct {
|
||||
ctx context.Context
|
||||
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
|
||||
|
||||
credSource, err := conf.parse(ts.ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
subjectToken, err := credSource.subjectToken()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stsRequest := stsexchange.TokenExchangeRequest{
|
||||
GrantType: "urn:ietf:params:oauth:grant-type:token-exchange",
|
||||
Audience: conf.Audience,
|
||||
Scope: conf.Scopes,
|
||||
RequestedTokenType: "urn:ietf:params:oauth:token-type:access_token",
|
||||
SubjectToken: subjectToken,
|
||||
SubjectTokenType: conf.SubjectTokenType,
|
||||
}
|
||||
header := make(http.Header)
|
||||
header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
header.Add("x-goog-api-client", getMetricsHeaderValue(conf, credSource))
|
||||
clientAuth := stsexchange.ClientAuthentication{
|
||||
AuthStyle: oauth2.AuthStyleInHeader,
|
||||
ClientID: conf.ClientID,
|
||||
ClientSecret: conf.ClientSecret,
|
||||
}
|
||||
var options map[string]interface{}
|
||||
// Do not pass workforce_pool_user_project when client authentication is used.
|
||||
// The client ID is sufficient for determining the user project.
|
||||
if conf.WorkforcePoolUserProject != "" && conf.ClientID == "" {
|
||||
options = map[string]interface{}{
|
||||
"userProject": conf.WorkforcePoolUserProject,
|
||||
}
|
||||
}
|
||||
stsResp, err := stsexchange.ExchangeToken(ts.ctx, conf.TokenURL, &stsRequest, clientAuth, header, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
accessToken := &oauth2.Token{
|
||||
AccessToken: stsResp.AccessToken,
|
||||
TokenType: stsResp.TokenType,
|
||||
}
|
||||
if stsResp.ExpiresIn < 0 {
|
||||
return nil, fmt.Errorf("oauth2/google: got invalid expiry from security token service")
|
||||
} else if stsResp.ExpiresIn >= 0 {
|
||||
accessToken.Expiry = now().Add(time.Duration(stsResp.ExpiresIn) * time.Second)
|
||||
}
|
||||
|
||||
if stsResp.RefreshToken != "" {
|
||||
accessToken.RefreshToken = stsResp.RefreshToken
|
||||
}
|
||||
return accessToken, nil
|
||||
}
|
@ -1,259 +0,0 @@
|
||||
// Copyright 2020 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 (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
const (
|
||||
textBaseCredPath = "testdata/3pi_cred.txt"
|
||||
jsonBaseCredPath = "testdata/3pi_cred.json"
|
||||
)
|
||||
|
||||
var testBaseCredSource = CredentialSource{
|
||||
File: textBaseCredPath,
|
||||
Format: format{Type: fileTypeText},
|
||||
}
|
||||
|
||||
var testConfig = Config{
|
||||
Audience: "32555940559.apps.googleusercontent.com",
|
||||
SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt",
|
||||
TokenInfoURL: "http://localhost:8080/v1/tokeninfo",
|
||||
ClientSecret: "notsosecret",
|
||||
ClientID: "rbrgnognrhongo3bi4gb9ghg9g",
|
||||
CredentialSource: testBaseCredSource,
|
||||
Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"},
|
||||
}
|
||||
|
||||
var (
|
||||
baseCredsRequestBody = "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=street123&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aid_token"
|
||||
baseCredsResponseBody = `{"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"}`
|
||||
workforcePoolRequestBodyWithClientId = "audience=%2F%2Fiam.googleapis.com%2Flocations%2Feu%2FworkforcePools%2Fpool-id%2Fproviders%2Fprovider-id&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=street123&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aid_token"
|
||||
workforcePoolRequestBodyWithoutClientId = "audience=%2F%2Fiam.googleapis.com%2Flocations%2Feu%2FworkforcePools%2Fpool-id%2Fproviders%2Fprovider-id&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&options=%7B%22userProject%22%3A%22myProject%22%7D&requested_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fdevstorage.full_control&subject_token=street123&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aid_token"
|
||||
correctAT = "Sample.Access.Token"
|
||||
expiry int64 = 234852
|
||||
)
|
||||
var (
|
||||
testNow = func() time.Time { return time.Unix(expiry, 0) }
|
||||
)
|
||||
|
||||
type testExchangeTokenServer struct {
|
||||
url string
|
||||
authorization string
|
||||
contentType string
|
||||
metricsHeader string
|
||||
body string
|
||||
response string
|
||||
}
|
||||
|
||||
func run(t *testing.T, config *Config, tets *testExchangeTokenServer) (*oauth2.Token, error) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if got, want := r.URL.String(), tets.url; got != want {
|
||||
t.Errorf("URL.String(): got %v but want %v", got, want)
|
||||
}
|
||||
headerAuth := r.Header.Get("Authorization")
|
||||
if got, want := headerAuth, tets.authorization; got != want {
|
||||
t.Errorf("got %v but want %v", got, want)
|
||||
}
|
||||
headerContentType := r.Header.Get("Content-Type")
|
||||
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)
|
||||
}
|
||||
if got, want := string(body), tets.body; got != want {
|
||||
t.Errorf("Unexpected exchange payload: got %v but want %v", got, want)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(tets.response))
|
||||
}))
|
||||
defer server.Close()
|
||||
config.TokenURL = server.URL
|
||||
|
||||
oldNow := now
|
||||
defer func() { now = oldNow }()
|
||||
now = testNow
|
||||
|
||||
ts := tokenSource{
|
||||
ctx: context.Background(),
|
||||
conf: config,
|
||||
}
|
||||
|
||||
return ts.Token()
|
||||
}
|
||||
|
||||
func validateToken(t *testing.T, tok *oauth2.Token) {
|
||||
if got, want := tok.AccessToken, correctAT; got != want {
|
||||
t.Errorf("Unexpected access token: got %v, but wanted %v", got, want)
|
||||
}
|
||||
if got, want := tok.TokenType, "Bearer"; got != want {
|
||||
t.Errorf("Unexpected TokenType: got %v, but wanted %v", got, want)
|
||||
}
|
||||
|
||||
if got, want := tok.Expiry, testNow().Add(time.Duration(3600)*time.Second); got != want {
|
||||
t.Errorf("Unexpected Expiry: got %v, but wanted %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
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",
|
||||
SubjectTokenType: "urn:ietf:params:oauth:token-type:id_token",
|
||||
ClientSecret: "notsosecret",
|
||||
ClientID: "rbrgnognrhongo3bi4gb9ghg9g",
|
||||
CredentialSource: testBaseCredSource,
|
||||
Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"},
|
||||
}
|
||||
|
||||
server := testExchangeTokenServer{
|
||||
url: "/",
|
||||
authorization: "Basic cmJyZ25vZ25yaG9uZ28zYmk0Z2I5Z2hnOWc6bm90c29zZWNyZXQ=",
|
||||
contentType: "application/x-www-form-urlencoded",
|
||||
metricsHeader: getExpectedMetricsHeader("file", false, false),
|
||||
body: baseCredsRequestBody,
|
||||
response: baseCredsResponseBody,
|
||||
}
|
||||
|
||||
tok, err := run(t, &config, &server)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %e", err)
|
||||
}
|
||||
validateToken(t, tok)
|
||||
}
|
||||
|
||||
func TestWorkforcePoolTokenWithClientID(t *testing.T) {
|
||||
config := Config{
|
||||
Audience: "//iam.googleapis.com/locations/eu/workforcePools/pool-id/providers/provider-id",
|
||||
SubjectTokenType: "urn:ietf:params:oauth:token-type:id_token",
|
||||
ClientSecret: "notsosecret",
|
||||
ClientID: "rbrgnognrhongo3bi4gb9ghg9g",
|
||||
CredentialSource: testBaseCredSource,
|
||||
Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"},
|
||||
WorkforcePoolUserProject: "myProject",
|
||||
}
|
||||
|
||||
server := testExchangeTokenServer{
|
||||
url: "/",
|
||||
authorization: "Basic cmJyZ25vZ25yaG9uZ28zYmk0Z2I5Z2hnOWc6bm90c29zZWNyZXQ=",
|
||||
contentType: "application/x-www-form-urlencoded",
|
||||
metricsHeader: getExpectedMetricsHeader("file", false, false),
|
||||
body: workforcePoolRequestBodyWithClientId,
|
||||
response: baseCredsResponseBody,
|
||||
}
|
||||
|
||||
tok, err := run(t, &config, &server)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %e", err)
|
||||
}
|
||||
validateToken(t, tok)
|
||||
}
|
||||
|
||||
func TestWorkforcePoolTokenWithoutClientID(t *testing.T) {
|
||||
config := Config{
|
||||
Audience: "//iam.googleapis.com/locations/eu/workforcePools/pool-id/providers/provider-id",
|
||||
SubjectTokenType: "urn:ietf:params:oauth:token-type:id_token",
|
||||
ClientSecret: "notsosecret",
|
||||
CredentialSource: testBaseCredSource,
|
||||
Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"},
|
||||
WorkforcePoolUserProject: "myProject",
|
||||
}
|
||||
|
||||
server := testExchangeTokenServer{
|
||||
url: "/",
|
||||
authorization: "",
|
||||
contentType: "application/x-www-form-urlencoded",
|
||||
metricsHeader: getExpectedMetricsHeader("file", false, false),
|
||||
body: workforcePoolRequestBodyWithoutClientId,
|
||||
response: baseCredsResponseBody,
|
||||
}
|
||||
|
||||
tok, err := run(t, &config, &server)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %e", err)
|
||||
}
|
||||
validateToken(t, tok)
|
||||
}
|
||||
|
||||
func TestNonworkforceWithWorkforcePoolUserProject(t *testing.T) {
|
||||
config := Config{
|
||||
Audience: "32555940559.apps.googleusercontent.com",
|
||||
SubjectTokenType: "urn:ietf:params:oauth:token-type:id_token",
|
||||
TokenURL: "https://sts.googleapis.com",
|
||||
ClientSecret: "notsosecret",
|
||||
ClientID: "rbrgnognrhongo3bi4gb9ghg9g",
|
||||
CredentialSource: testBaseCredSource,
|
||||
Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"},
|
||||
WorkforcePoolUserProject: "myProject",
|
||||
}
|
||||
|
||||
_, err := config.TokenSource(context.Background())
|
||||
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error but found none")
|
||||
}
|
||||
if got, want := err.Error(), "oauth2/google: workforce_pool_user_project should not be set for non-workforce pool credentials"; got != want {
|
||||
t.Errorf("Incorrect error received.\nExpected: %s\nRecieved: %s", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkforcePoolCreation(t *testing.T) {
|
||||
var audienceValidatyTests = []struct {
|
||||
audience string
|
||||
expectSuccess bool
|
||||
}{
|
||||
{"//iam.googleapis.com/locations/global/workforcePools/pool-id/providers/provider-id", true},
|
||||
{"//iam.googleapis.com/locations/eu/workforcePools/pool-id/providers/provider-id", true},
|
||||
{"//iam.googleapis.com/locations/eu/workforcePools/workloadIdentityPools/providers/provider-id", true},
|
||||
{"identitynamespace:1f12345:my_provider", false},
|
||||
{"//iam.googleapis.com/projects/123456/locations/global/workloadIdentityPools/pool-id/providers/provider-id", false},
|
||||
{"//iam.googleapis.com/projects/123456/locations/eu/workloadIdentityPools/pool-id/providers/provider-id", false},
|
||||
{"//iam.googleapis.com/projects/123456/locations/global/workloadIdentityPools/workforcePools/providers/provider-id", false},
|
||||
{"//iamgoogleapis.com/locations/eu/workforcePools/pool-id/providers/provider-id", false},
|
||||
{"//iam.googleapiscom/locations/eu/workforcePools/pool-id/providers/provider-id", false},
|
||||
{"//iam.googleapis.com/locations/workforcePools/pool-id/providers/provider-id", false},
|
||||
{"//iam.googleapis.com/locations/eu/workforcePool/pool-id/providers/provider-id", false},
|
||||
{"//iam.googleapis.com/locations//workforcePool/pool-id/providers/provider-id", false},
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
for _, tt := range audienceValidatyTests {
|
||||
t.Run(" "+tt.audience, func(t *testing.T) { // We prepend a space ahead of the test input when outputting for sake of readability.
|
||||
config := testConfig
|
||||
config.TokenURL = "https://sts.googleapis.com" // Setting the most basic acceptable tokenURL
|
||||
config.ServiceAccountImpersonationURL = "https://iamcredentials.googleapis.com"
|
||||
config.Audience = tt.audience
|
||||
config.WorkforcePoolUserProject = "myProject"
|
||||
_, 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")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
// Copyright 2020 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 "fmt"
|
||||
|
||||
// Error for handling OAuth related error responses as stated in rfc6749#5.2.
|
||||
type Error struct {
|
||||
Code string
|
||||
URI string
|
||||
Description string
|
||||
}
|
||||
|
||||
func (err *Error) Error() string {
|
||||
return fmt.Sprintf("got error code %s from %s: %s", err.Code, err.URI, err.Description)
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
// Copyright 2020 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 "testing"
|
||||
|
||||
func TestError(t *testing.T) {
|
||||
e := Error{
|
||||
"42",
|
||||
"http:thisIsAPlaceholder",
|
||||
"The Answer!",
|
||||
}
|
||||
want := "got error code 42 from http:thisIsAPlaceholder: The Answer!"
|
||||
if got := e.Error(); got != want {
|
||||
t.Errorf("Got error message %q; want %q", got, want)
|
||||
}
|
||||
}
|
@ -1,144 +0,0 @@
|
||||
// Copyright 2021 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 (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var (
|
||||
baseImpersonateCredsReqBody = "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%2Fcloud-platform&subject_token=street123&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Ajwt"
|
||||
baseImpersonateCredsRespBody = `{"accessToken":"Second.Access.Token","expireTime":"2020-12-28T15:01:23Z"}`
|
||||
)
|
||||
|
||||
func createImpersonationServer(urlWanted, authWanted, bodyWanted, response string, t *testing.T) *httptest.Server {
|
||||
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if got, want := r.URL.String(), urlWanted; got != want {
|
||||
t.Errorf("URL.String(): got %v but want %v", got, want)
|
||||
}
|
||||
headerAuth := r.Header.Get("Authorization")
|
||||
if got, want := headerAuth, authWanted; got != want {
|
||||
t.Errorf("got %v but want %v", got, want)
|
||||
}
|
||||
headerContentType := r.Header.Get("Content-Type")
|
||||
if got, want := headerContentType, "application/json"; 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)
|
||||
}
|
||||
if got, want := string(body), bodyWanted; got != want {
|
||||
t.Errorf("Unexpected impersonation payload: got %v but want %v", got, want)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(response))
|
||||
}))
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
headerAuth := r.Header.Get("Authorization")
|
||||
if got, want := headerAuth, "Basic cmJyZ25vZ25yaG9uZ28zYmk0Z2I5Z2hnOWc6bm90c29zZWNyZXQ="; got != want {
|
||||
t.Errorf("got %v but want %v", got, want)
|
||||
}
|
||||
headerContentType := r.Header.Get("Content-Type")
|
||||
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)
|
||||
}
|
||||
if got, want := string(body), baseImpersonateCredsReqBody; got != want {
|
||||
t.Errorf("Unexpected exchange payload: got %v but want %v", got, want)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(baseCredsResponseBody))
|
||||
}))
|
||||
}
|
||||
|
||||
var impersonationTests = []struct {
|
||||
name string
|
||||
config Config
|
||||
expectedImpersonationBody string
|
||||
expectedMetricsHeader string
|
||||
}{
|
||||
{
|
||||
name: "Base Impersonation",
|
||||
config: Config{
|
||||
Audience: "32555940559.apps.googleusercontent.com",
|
||||
SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt",
|
||||
TokenInfoURL: "http://localhost:8080/v1/tokeninfo",
|
||||
ClientSecret: "notsosecret",
|
||||
ClientID: "rbrgnognrhongo3bi4gb9ghg9g",
|
||||
CredentialSource: testBaseCredSource,
|
||||
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",
|
||||
config: Config{
|
||||
Audience: "32555940559.apps.googleusercontent.com",
|
||||
SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt",
|
||||
TokenInfoURL: "http://localhost:8080/v1/tokeninfo",
|
||||
ClientSecret: "notsosecret",
|
||||
ClientID: "rbrgnognrhongo3bi4gb9ghg9g",
|
||||
CredentialSource: testBaseCredSource,
|
||||
Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"},
|
||||
ServiceAccountImpersonationLifetimeSeconds: 10000,
|
||||
},
|
||||
expectedImpersonationBody: "{\"lifetime\":\"10000s\",\"scope\":[\"https://www.googleapis.com/auth/devstorage.full_control\"]}",
|
||||
expectedMetricsHeader: getExpectedMetricsHeader("file", true, true),
|
||||
},
|
||||
}
|
||||
|
||||
func TestImpersonation(t *testing.T) {
|
||||
for _, tt := range impersonationTests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
testImpersonateConfig := tt.config
|
||||
impersonateServer := createImpersonationServer("/", "Bearer Sample.Access.Token", tt.expectedImpersonationBody, baseImpersonateCredsRespBody, t)
|
||||
defer impersonateServer.Close()
|
||||
testImpersonateConfig.ServiceAccountImpersonationURL = impersonateServer.URL
|
||||
|
||||
targetServer := createTargetServer(tt.expectedMetricsHeader, t)
|
||||
defer targetServer.Close()
|
||||
testImpersonateConfig.TokenURL = targetServer.URL
|
||||
|
||||
ourTS, err := testImpersonateConfig.tokenSource(context.Background(), "http")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create TokenSource: %v", err)
|
||||
}
|
||||
|
||||
oldNow := now
|
||||
defer func() { now = oldNow }()
|
||||
now = testNow
|
||||
|
||||
tok, err := ourTS.Token()
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %e", err)
|
||||
}
|
||||
if got, want := tok.AccessToken, "Second.Access.Token"; got != want {
|
||||
t.Errorf("Unexpected access token: got %v, but wanted %v", got, want)
|
||||
}
|
||||
if got, want := tok.TokenType, "Bearer"; got != want {
|
||||
t.Errorf("Unexpected TokenType: got %v, but wanted %v", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -8,7 +8,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
@ -38,7 +38,7 @@ type testRefreshTokenServer struct {
|
||||
server *httptest.Server
|
||||
}
|
||||
|
||||
func TestExernalAccountAuthorizedUser_JustToken(t *testing.T) {
|
||||
func TestExternalAccountAuthorizedUser_JustToken(t *testing.T) {
|
||||
config := &Config{
|
||||
Token: "AAAAAAA",
|
||||
Expiry: now().Add(time.Hour),
|
||||
@ -57,7 +57,7 @@ func TestExernalAccountAuthorizedUser_JustToken(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestExernalAccountAuthorizedUser_TokenRefreshWithRefreshTokenInRespondse(t *testing.T) {
|
||||
func TestExternalAccountAuthorizedUser_TokenRefreshWithRefreshTokenInResponse(t *testing.T) {
|
||||
server := &testRefreshTokenServer{
|
||||
URL: "/",
|
||||
Authorization: "Basic Q0xJRU5UX0lEOkNMSUVOVF9TRUNSRVQ=",
|
||||
@ -99,7 +99,7 @@ func TestExernalAccountAuthorizedUser_TokenRefreshWithRefreshTokenInRespondse(t
|
||||
}
|
||||
}
|
||||
|
||||
func TestExernalAccountAuthorizedUser_MinimumFieldsRequiredForRefresh(t *testing.T) {
|
||||
func TestExternalAccountAuthorizedUser_MinimumFieldsRequiredForRefresh(t *testing.T) {
|
||||
server := &testRefreshTokenServer{
|
||||
URL: "/",
|
||||
Authorization: "Basic Q0xJRU5UX0lEOkNMSUVOVF9TRUNSRVQ=",
|
||||
@ -187,7 +187,7 @@ func TestExternalAccountAuthorizedUser_MissingRefreshFields(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing client secrect",
|
||||
name: "missing client secret",
|
||||
config: Config{
|
||||
RefreshToken: "BBBBBBBBB",
|
||||
TokenURL: url,
|
||||
@ -227,7 +227,7 @@ func (trts *testRefreshTokenServer) run(t *testing.T) (string, error) {
|
||||
if got, want := headerContentType, trts.ContentType; got != want {
|
||||
t.Errorf("got %v but want %v", got, want)
|
||||
}
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed reading request body: %s.", err)
|
||||
}
|
||||
|
@ -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 impersonate
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@ -10,7 +10,6 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
@ -81,7 +80,7 @@ func (its ImpersonateTokenSource) Token() (*oauth2.Token, error) {
|
||||
return nil, fmt.Errorf("oauth2/google: unable to generate access token: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("oauth2/google: unable to read body: %v", err)
|
||||
}
|
@ -9,7 +9,6 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
@ -28,7 +27,7 @@ func defaultHeader() http.Header {
|
||||
// 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 *TokenExchangeRequest, authentication ClientAuthentication, headers http.Header, options map[string]interface{}) (*Response, error) {
|
||||
func ExchangeToken(ctx context.Context, endpoint string, request *TokenExchangeRequest, authentication ClientAuthentication, headers http.Header, options map[string]any) (*Response, error) {
|
||||
data := url.Values{}
|
||||
data.Set("audience", request.Audience)
|
||||
data.Set("grant_type", "urn:ietf:params:oauth:grant-type:token-exchange")
|
||||
@ -82,7 +81,7 @@ func makeRequest(ctx context.Context, endpoint string, data url.Values, authenti
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ package stsexchange
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
@ -73,7 +73,7 @@ func TestExchangeToken(t *testing.T) {
|
||||
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)
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
t.Errorf("Failed reading request body: %v.", err)
|
||||
}
|
||||
@ -132,7 +132,7 @@ var optsValues = [][]string{{"foo", "bar"}, {"cat", "pan"}}
|
||||
|
||||
func TestExchangeToken_Opts(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed reading request body: %v.", err)
|
||||
}
|
||||
@ -142,11 +142,11 @@ func TestExchangeToken_Opts(t *testing.T) {
|
||||
}
|
||||
strOpts, ok := data["options"]
|
||||
if !ok {
|
||||
t.Errorf("Server didn't recieve an \"options\" field.")
|
||||
t.Errorf("Server didn't receive an \"options\" field.")
|
||||
} else if len(strOpts) < 1 {
|
||||
t.Errorf("\"options\" field has length 0.")
|
||||
}
|
||||
var opts map[string]interface{}
|
||||
var opts map[string]any
|
||||
err = json.Unmarshal([]byte(strOpts[0]), &opts)
|
||||
if err != nil {
|
||||
t.Fatalf("Couldn't parse received \"options\" field.")
|
||||
@ -159,7 +159,7 @@ func TestExchangeToken_Opts(t *testing.T) {
|
||||
if !ok {
|
||||
t.Errorf("Couldn't find first option parameter.")
|
||||
} else {
|
||||
tOpts1, ok := val.(map[string]interface{})
|
||||
tOpts1, ok := val.(map[string]any)
|
||||
if !ok {
|
||||
t.Errorf("Failed to assert the first option parameter as type testOpts.")
|
||||
} else {
|
||||
@ -176,7 +176,7 @@ func TestExchangeToken_Opts(t *testing.T) {
|
||||
if !ok {
|
||||
t.Errorf("Couldn't find second option parameter.")
|
||||
} else {
|
||||
tOpts2, ok := val2.(map[string]interface{})
|
||||
tOpts2, ok := val2.(map[string]any)
|
||||
if !ok {
|
||||
t.Errorf("Failed to assert the second option parameter as type testOpts.")
|
||||
} else {
|
||||
@ -200,7 +200,7 @@ func TestExchangeToken_Opts(t *testing.T) {
|
||||
|
||||
firstOption := testOpts{optsValues[0][0], optsValues[0][1]}
|
||||
secondOption := testOpts{optsValues[1][0], optsValues[1][1]}
|
||||
inputOpts := make(map[string]interface{})
|
||||
inputOpts := make(map[string]any)
|
||||
inputOpts["one"] = firstOption
|
||||
inputOpts["two"] = secondOption
|
||||
ExchangeToken(context.Background(), ts.URL, &exchangeTokenRequest, auth, headers, inputOpts)
|
||||
@ -220,7 +220,7 @@ func TestRefreshToken(t *testing.T) {
|
||||
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)
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
t.Errorf("Failed reading request body: %v.", err)
|
||||
}
|
||||
|
@ -1,13 +0,0 @@
|
||||
// Copyright 2018 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build appengine
|
||||
|
||||
package internal
|
||||
|
||||
import "google.golang.org/appengine/urlfetch"
|
||||
|
||||
func init() {
|
||||
appengineClientHook = urlfetch.Client
|
||||
}
|
@ -2,5 +2,5 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package internal contains support packages for oauth2 package.
|
||||
// Package internal contains support packages for [golang.org/x/oauth2].
|
||||
package internal
|
||||
|
@ -13,7 +13,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
|
||||
// to an [*rsa.PrivateKey]. It detects whether the private key is in a
|
||||
// PEM container or not. If so, it extracts the private key
|
||||
// from PEM container before conversion. It only supports PEM
|
||||
// containers with no passphrase.
|
||||
|
@ -10,7 +10,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"mime"
|
||||
"net/http"
|
||||
@ -26,9 +25,9 @@ import (
|
||||
// the requests to access protected resources on the OAuth 2.0
|
||||
// provider's backend.
|
||||
//
|
||||
// This type is a mirror of oauth2.Token and exists to break
|
||||
// This type is a mirror of [golang.org/x/oauth2.Token] and exists to break
|
||||
// an otherwise-circular dependency. Other internal packages
|
||||
// should convert this Token into an oauth2.Token before use.
|
||||
// should convert this Token into an [golang.org/x/oauth2.Token] before use.
|
||||
type Token struct {
|
||||
// AccessToken is the token that authorizes and authenticates
|
||||
// the requests.
|
||||
@ -50,9 +49,16 @@ type Token struct {
|
||||
// mechanisms for that TokenSource will not be used.
|
||||
Expiry time.Time
|
||||
|
||||
// ExpiresIn is the OAuth2 wire format "expires_in" field,
|
||||
// which specifies how many seconds later the token expires,
|
||||
// relative to an unknown time base approximately around "now".
|
||||
// It is the application's responsibility to populate
|
||||
// `Expiry` from `ExpiresIn` when required.
|
||||
ExpiresIn int64 `json:"expires_in,omitempty"`
|
||||
|
||||
// Raw optionally contains extra metadata from the server
|
||||
// when updating a token.
|
||||
Raw interface{}
|
||||
Raw any
|
||||
}
|
||||
|
||||
// tokenJSON is the struct representing the HTTP response from OAuth2
|
||||
@ -99,14 +105,6 @@ func (e *expirationTime) UnmarshalJSON(b []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegisterBrokenAuthHeaderProvider previously did something. It is now a no-op.
|
||||
//
|
||||
// Deprecated: this function no longer does anything. Caller code that
|
||||
// wants to avoid potential extra HTTP requests made during
|
||||
// auto-probing of the provider's auth style should set
|
||||
// Endpoint.AuthStyle.
|
||||
func RegisterBrokenAuthHeaderProvider(tokenURL string) {}
|
||||
|
||||
// AuthStyle is a copy of the golang.org/x/oauth2 package's AuthStyle type.
|
||||
type AuthStyle int
|
||||
|
||||
@ -143,6 +141,11 @@ func (lc *LazyAuthStyleCache) Get() *AuthStyleCache {
|
||||
return c
|
||||
}
|
||||
|
||||
type authStyleCacheKey struct {
|
||||
url string
|
||||
clientID string
|
||||
}
|
||||
|
||||
// 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
|
||||
@ -150,26 +153,26 @@ func (lc *LazyAuthStyleCache) Get() *AuthStyleCache {
|
||||
// small.
|
||||
type AuthStyleCache struct {
|
||||
mu sync.Mutex
|
||||
m map[string]AuthStyle // keyed by tokenURL
|
||||
m map[authStyleCacheKey]AuthStyle
|
||||
}
|
||||
|
||||
// lookupAuthStyle reports which auth style we last used with tokenURL
|
||||
// when calling RetrieveToken and whether we have ever done so.
|
||||
func (c *AuthStyleCache) lookupAuthStyle(tokenURL string) (style AuthStyle, ok bool) {
|
||||
func (c *AuthStyleCache) lookupAuthStyle(tokenURL, clientID string) (style AuthStyle, ok bool) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
style, ok = c.m[tokenURL]
|
||||
style, ok = c.m[authStyleCacheKey{tokenURL, clientID}]
|
||||
return
|
||||
}
|
||||
|
||||
// setAuthStyle adds an entry to authStyleCache, documented above.
|
||||
func (c *AuthStyleCache) setAuthStyle(tokenURL string, v AuthStyle) {
|
||||
func (c *AuthStyleCache) setAuthStyle(tokenURL, clientID string, v AuthStyle) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.m == nil {
|
||||
c.m = make(map[string]AuthStyle)
|
||||
c.m = make(map[authStyleCacheKey]AuthStyle)
|
||||
}
|
||||
c.m[tokenURL] = v
|
||||
c.m[authStyleCacheKey{tokenURL, clientID}] = v
|
||||
}
|
||||
|
||||
// newTokenRequest returns a new *http.Request to retrieve a new token
|
||||
@ -210,9 +213,9 @@ func cloneURLValues(v url.Values) url.Values {
|
||||
}
|
||||
|
||||
func RetrieveToken(ctx context.Context, clientID, clientSecret, tokenURL string, v url.Values, authStyle AuthStyle, styleCache *AuthStyleCache) (*Token, error) {
|
||||
needsAuthStyleProbe := authStyle == 0
|
||||
needsAuthStyleProbe := authStyle == AuthStyleUnknown
|
||||
if needsAuthStyleProbe {
|
||||
if style, ok := styleCache.lookupAuthStyle(tokenURL); ok {
|
||||
if style, ok := styleCache.lookupAuthStyle(tokenURL, clientID); ok {
|
||||
authStyle = style
|
||||
needsAuthStyleProbe = false
|
||||
} else {
|
||||
@ -242,7 +245,7 @@ func RetrieveToken(ctx context.Context, clientID, clientSecret, tokenURL string,
|
||||
token, err = doTokenRoundTrip(ctx, req)
|
||||
}
|
||||
if needsAuthStyleProbe && err == nil {
|
||||
styleCache.setAuthStyle(tokenURL, authStyle)
|
||||
styleCache.setAuthStyle(tokenURL, clientID, authStyle)
|
||||
}
|
||||
// Don't overwrite `RefreshToken` with an empty value
|
||||
// if this was a token refreshing request.
|
||||
@ -257,7 +260,7 @@ func doTokenRoundTrip(ctx context.Context, req *http.Request) (*Token, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1<<20))
|
||||
body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20))
|
||||
r.Body.Close()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err)
|
||||
@ -312,7 +315,8 @@ func doTokenRoundTrip(ctx context.Context, req *http.Request) (*Token, error) {
|
||||
TokenType: tj.TokenType,
|
||||
RefreshToken: tj.RefreshToken,
|
||||
Expiry: tj.expiry(),
|
||||
Raw: make(map[string]interface{}),
|
||||
ExpiresIn: int64(tj.ExpiresIn),
|
||||
Raw: make(map[string]any),
|
||||
}
|
||||
json.Unmarshal(body, &token.Raw) // no error checks for optional fields
|
||||
}
|
||||
|
@ -75,3 +75,48 @@ func TestExpiresInUpperBound(t *testing.T) {
|
||||
t.Errorf("expiration time = %v; want %v", e, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthStyleCache(t *testing.T) {
|
||||
var c LazyAuthStyleCache
|
||||
|
||||
cases := []struct {
|
||||
url string
|
||||
clientID string
|
||||
style AuthStyle
|
||||
}{
|
||||
{
|
||||
"https://host1.example.com/token",
|
||||
"client_1",
|
||||
AuthStyleInHeader,
|
||||
}, {
|
||||
"https://host2.example.com/token",
|
||||
"client_2",
|
||||
AuthStyleInParams,
|
||||
}, {
|
||||
"https://host1.example.com/token",
|
||||
"client_3",
|
||||
AuthStyleInParams,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range cases {
|
||||
t.Run(tt.clientID, func(t *testing.T) {
|
||||
cc := c.Get()
|
||||
got, ok := cc.lookupAuthStyle(tt.url, tt.clientID)
|
||||
if ok {
|
||||
t.Fatalf("unexpected auth style found on first request: %v", got)
|
||||
}
|
||||
|
||||
cc.setAuthStyle(tt.url, tt.clientID, tt.style)
|
||||
|
||||
got, ok = cc.lookupAuthStyle(tt.url, tt.clientID)
|
||||
if !ok {
|
||||
t.Fatalf("auth style not found in cache")
|
||||
}
|
||||
|
||||
if got != tt.style {
|
||||
t.Fatalf("auth style mismatch, got=%v, want=%v", got, tt.style)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -9,8 +9,8 @@ import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// HTTPClient is the context key to use with golang.org/x/net/context's
|
||||
// WithValue function to associate an *http.Client value with a context.
|
||||
// HTTPClient is the context key to use with [context.WithValue]
|
||||
// to associate an [*http.Client] value with a context.
|
||||
var HTTPClient ContextKey
|
||||
|
||||
// ContextKey is just an empty struct. It exists so HTTPClient can be
|
||||
@ -18,16 +18,11 @@ var HTTPClient ContextKey
|
||||
// because nobody else can create a ContextKey, being unexported.
|
||||
type ContextKey struct{}
|
||||
|
||||
var appengineClientHook func(context.Context) *http.Client
|
||||
|
||||
func ContextClient(ctx context.Context) *http.Client {
|
||||
if ctx != nil {
|
||||
if hc, ok := ctx.Value(HTTPClient).(*http.Client); ok {
|
||||
return hc
|
||||
}
|
||||
}
|
||||
if appengineClientHook != nil {
|
||||
return appengineClientHook(ctx)
|
||||
}
|
||||
return http.DefaultClient
|
||||
}
|
||||
|
@ -13,7 +13,6 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
@ -114,7 +113,7 @@ func (js jwtSource) Token() (*oauth2.Token, error) {
|
||||
return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err)
|
||||
}
|
||||
@ -123,11 +122,7 @@ func (js jwtSource) Token() (*oauth2.Token, error) {
|
||||
}
|
||||
|
||||
// tokenRes is the JSON response body.
|
||||
var tokenRes struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
TokenType string `json:"token_type"`
|
||||
ExpiresIn int64 `json:"expires_in"` // relative seconds from now
|
||||
}
|
||||
var tokenRes oauth2.Token
|
||||
if err := json.Unmarshal(body, &tokenRes); err != nil {
|
||||
return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err)
|
||||
}
|
||||
|
40
jws/jws.go
40
jws/jws.go
@ -4,7 +4,7 @@
|
||||
|
||||
// Package jws provides a partial implementation
|
||||
// of JSON Web Signature encoding and decoding.
|
||||
// It exists to support the golang.org/x/oauth2 package.
|
||||
// It exists to support the [golang.org/x/oauth2] package.
|
||||
//
|
||||
// See RFC 7515.
|
||||
//
|
||||
@ -48,7 +48,7 @@ type ClaimSet struct {
|
||||
|
||||
// See http://tools.ietf.org/html/draft-jones-json-web-token-10#section-4.3
|
||||
// This array is marshalled using custom code (see (c *ClaimSet) encode()).
|
||||
PrivateClaims map[string]interface{} `json:"-"`
|
||||
PrivateClaims map[string]any `json:"-"`
|
||||
}
|
||||
|
||||
func (c *ClaimSet) encode() (string, error) {
|
||||
@ -116,12 +116,12 @@ func (h *Header) encode() (string, error) {
|
||||
// Decode decodes a claim set from a JWS payload.
|
||||
func Decode(payload string) (*ClaimSet, error) {
|
||||
// decode returned id token to get expiry
|
||||
s := strings.Split(payload, ".")
|
||||
if len(s) < 2 {
|
||||
_, claims, _, ok := parseToken(payload)
|
||||
if !ok {
|
||||
// TODO(jbd): Provide more context about the error.
|
||||
return nil, errors.New("jws: invalid token received")
|
||||
}
|
||||
decoded, err := base64.RawURLEncoding.DecodeString(s[1])
|
||||
decoded, err := base64.RawURLEncoding.DecodeString(claims)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -152,7 +152,7 @@ func EncodeWithSigner(header *Header, c *ClaimSet, sg Signer) (string, error) {
|
||||
}
|
||||
|
||||
// Encode encodes a signed JWS with provided header and claim set.
|
||||
// This invokes EncodeWithSigner using crypto/rsa.SignPKCS1v15 with the given RSA private key.
|
||||
// This invokes [EncodeWithSigner] using [crypto/rsa.SignPKCS1v15] with the given RSA private key.
|
||||
func Encode(header *Header, c *ClaimSet, key *rsa.PrivateKey) (string, error) {
|
||||
sg := func(data []byte) (sig []byte, err error) {
|
||||
h := sha256.New()
|
||||
@ -165,18 +165,34 @@ func Encode(header *Header, c *ClaimSet, key *rsa.PrivateKey) (string, error) {
|
||||
// Verify tests whether the provided JWT token's signature was produced by the private key
|
||||
// associated with the supplied public key.
|
||||
func Verify(token string, key *rsa.PublicKey) error {
|
||||
parts := strings.Split(token, ".")
|
||||
if len(parts) != 3 {
|
||||
header, claims, sig, ok := parseToken(token)
|
||||
if !ok {
|
||||
return errors.New("jws: invalid token received, token must have 3 parts")
|
||||
}
|
||||
|
||||
signedContent := parts[0] + "." + parts[1]
|
||||
signatureString, err := base64.RawURLEncoding.DecodeString(parts[2])
|
||||
signatureString, err := base64.RawURLEncoding.DecodeString(sig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
h := sha256.New()
|
||||
h.Write([]byte(signedContent))
|
||||
h.Write([]byte(header + tokenDelim + claims))
|
||||
return rsa.VerifyPKCS1v15(key, crypto.SHA256, h.Sum(nil), signatureString)
|
||||
}
|
||||
|
||||
func parseToken(s string) (header, claims, sig string, ok bool) {
|
||||
header, s, ok = strings.Cut(s, tokenDelim)
|
||||
if !ok { // no period found
|
||||
return "", "", "", false
|
||||
}
|
||||
claims, s, ok = strings.Cut(s, tokenDelim)
|
||||
if !ok { // only one period found
|
||||
return "", "", "", false
|
||||
}
|
||||
sig, _, ok = strings.Cut(s, tokenDelim)
|
||||
if ok { // three periods found
|
||||
return "", "", "", false
|
||||
}
|
||||
return header, claims, sig, true
|
||||
}
|
||||
|
||||
const tokenDelim = "."
|
||||
|
@ -7,6 +7,8 @@ package jws
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@ -39,8 +41,57 @@ func TestSignAndVerify(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestVerifyFailsOnMalformedClaim(t *testing.T) {
|
||||
err := Verify("abc.def", nil)
|
||||
if err == nil {
|
||||
t.Error("got no errors; want improperly formed JWT not to be verified")
|
||||
cases := []struct {
|
||||
desc string
|
||||
token string
|
||||
}{
|
||||
{
|
||||
desc: "no periods",
|
||||
token: "aa",
|
||||
}, {
|
||||
desc: "only one period",
|
||||
token: "a.a",
|
||||
}, {
|
||||
desc: "more than two periods",
|
||||
token: "a.a.a.a",
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
f := func(t *testing.T) {
|
||||
err := Verify(tc.token, nil)
|
||||
if err == nil {
|
||||
t.Error("got no errors; want improperly formed JWT not to be verified")
|
||||
}
|
||||
}
|
||||
t.Run(tc.desc, f)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkVerify(b *testing.B) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
token string
|
||||
}{
|
||||
{
|
||||
desc: "full of periods",
|
||||
token: strings.Repeat(".", http.DefaultMaxHeaderBytes),
|
||||
}, {
|
||||
desc: "two trailing periods",
|
||||
token: strings.Repeat("a", http.DefaultMaxHeaderBytes-2) + "..",
|
||||
},
|
||||
}
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
for _, bc := range cases {
|
||||
f := func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
Verify(bc.token, &privateKey.PublicKey)
|
||||
}
|
||||
}
|
||||
b.Run(bc.desc, f)
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ import (
|
||||
"golang.org/x/oauth2/jwt"
|
||||
)
|
||||
|
||||
func ExampleJWTConfig() {
|
||||
func ExampleConfig() {
|
||||
ctx := context.Background()
|
||||
conf := &jwt.Config{
|
||||
Email: "xxx@developer.com",
|
||||
|
13
jwt/jwt.go
13
jwt/jwt.go
@ -13,7 +13,6 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
@ -69,7 +68,7 @@ type Config struct {
|
||||
|
||||
// PrivateClaims optionally specifies custom private claims in the JWT.
|
||||
// See http://tools.ietf.org/html/draft-jones-json-web-token-10#section-4.3
|
||||
PrivateClaims map[string]interface{}
|
||||
PrivateClaims map[string]any
|
||||
|
||||
// UseIDToken optionally specifies whether ID token should be used instead
|
||||
// of access token when the server returns both.
|
||||
@ -136,7 +135,7 @@ func (js jwtSource) Token() (*oauth2.Token, error) {
|
||||
return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err)
|
||||
}
|
||||
@ -148,10 +147,8 @@ func (js jwtSource) Token() (*oauth2.Token, error) {
|
||||
}
|
||||
// tokenRes is the JSON response body.
|
||||
var tokenRes struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
TokenType string `json:"token_type"`
|
||||
IDToken string `json:"id_token"`
|
||||
ExpiresIn int64 `json:"expires_in"` // relative seconds from now
|
||||
oauth2.Token
|
||||
IDToken string `json:"id_token"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &tokenRes); err != nil {
|
||||
return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err)
|
||||
@ -160,7 +157,7 @@ func (js jwtSource) Token() (*oauth2.Token, error) {
|
||||
AccessToken: tokenRes.AccessToken,
|
||||
TokenType: tokenRes.TokenType,
|
||||
}
|
||||
raw := make(map[string]interface{})
|
||||
raw := make(map[string]any)
|
||||
json.Unmarshal(body, &raw) // no error checks for optional fields
|
||||
token = token.WithExtra(raw)
|
||||
|
||||
|
@ -227,7 +227,7 @@ func TestJWTFetch_AssertionPayload(t *testing.T) {
|
||||
PrivateKey: dummyPrivateKey,
|
||||
PrivateKeyID: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||
TokenURL: ts.URL,
|
||||
PrivateClaims: map[string]interface{}{
|
||||
PrivateClaims: map[string]any{
|
||||
"private0": "claim0",
|
||||
"private1": "claim1",
|
||||
},
|
||||
@ -273,11 +273,11 @@ func TestJWTFetch_AssertionPayload(t *testing.T) {
|
||||
t.Errorf("payload prn = %q; want %q", got, want)
|
||||
}
|
||||
if len(conf.PrivateClaims) > 0 {
|
||||
var got interface{}
|
||||
var got any
|
||||
if err := json.Unmarshal(gotjson, &got); err != nil {
|
||||
t.Errorf("failed to parse payload; err = %q", err)
|
||||
}
|
||||
m := got.(map[string]interface{})
|
||||
m := got.(map[string]any)
|
||||
for v, k := range conf.PrivateClaims {
|
||||
if !reflect.DeepEqual(m[v], k) {
|
||||
t.Errorf("payload private claims key = %q: got %#v; want %#v", v, m[v], k)
|
||||
|
@ -25,7 +25,8 @@ func AzureADEndpoint(tenant string) oauth2.Endpoint {
|
||||
tenant = "common"
|
||||
}
|
||||
return oauth2.Endpoint{
|
||||
AuthURL: "https://login.microsoftonline.com/" + tenant + "/oauth2/v2.0/authorize",
|
||||
TokenURL: "https://login.microsoftonline.com/" + tenant + "/oauth2/v2.0/token",
|
||||
AuthURL: "https://login.microsoftonline.com/" + tenant + "/oauth2/v2.0/authorize",
|
||||
TokenURL: "https://login.microsoftonline.com/" + tenant + "/oauth2/v2.0/token",
|
||||
DeviceAuthURL: "https://login.microsoftonline.com/" + tenant + "/oauth2/v2.0/devicecode",
|
||||
}
|
||||
}
|
||||
|
65
oauth2.go
65
oauth2.go
@ -22,9 +22,9 @@ import (
|
||||
)
|
||||
|
||||
// NoContext is the default context you should supply if not using
|
||||
// your own context.Context (see https://golang.org/x/net/context).
|
||||
// your own [context.Context].
|
||||
//
|
||||
// Deprecated: Use context.Background() or context.TODO() instead.
|
||||
// Deprecated: Use [context.Background] or [context.TODO] instead.
|
||||
var NoContext = context.TODO()
|
||||
|
||||
// RegisterBrokenAuthHeaderProvider previously did something. It is now a no-op.
|
||||
@ -37,8 +37,8 @@ func RegisterBrokenAuthHeaderProvider(tokenURL string) {}
|
||||
|
||||
// Config describes a typical 3-legged OAuth2 flow, with both the
|
||||
// client application information and the server's endpoint URLs.
|
||||
// For the client credentials 2-legged OAuth2 flow, see the clientcredentials
|
||||
// package (https://golang.org/x/oauth2/clientcredentials).
|
||||
// For the client credentials 2-legged OAuth2 flow, see the
|
||||
// [golang.org/x/oauth2/clientcredentials] package.
|
||||
type Config struct {
|
||||
// ClientID is the application's ID.
|
||||
ClientID string
|
||||
@ -46,7 +46,7 @@ type Config struct {
|
||||
// ClientSecret is the application's secret.
|
||||
ClientSecret string
|
||||
|
||||
// Endpoint contains the resource server's token endpoint
|
||||
// Endpoint contains the authorization server's token endpoint
|
||||
// URLs. These are constants specific to each server and are
|
||||
// often available via site-specific packages, such as
|
||||
// google.Endpoint or github.Endpoint.
|
||||
@ -56,7 +56,7 @@ type Config struct {
|
||||
// the OAuth flow, after the resource owner's URLs.
|
||||
RedirectURL string
|
||||
|
||||
// Scope specifies optional requested permissions.
|
||||
// Scopes specifies optional requested permissions.
|
||||
Scopes []string
|
||||
|
||||
// authStyleCache caches which auth style to use when Endpoint.AuthStyle is
|
||||
@ -135,7 +135,7 @@ type setParam struct{ k, v string }
|
||||
|
||||
func (p setParam) setValue(m url.Values) { m.Set(p.k, p.v) }
|
||||
|
||||
// SetAuthURLParam builds an AuthCodeOption which passes key/value parameters
|
||||
// SetAuthURLParam builds an [AuthCodeOption] which passes key/value parameters
|
||||
// to a provider's authorization endpoint.
|
||||
func SetAuthURLParam(key, value string) AuthCodeOption {
|
||||
return setParam{key, value}
|
||||
@ -148,8 +148,8 @@ func SetAuthURLParam(key, value string) AuthCodeOption {
|
||||
// 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.
|
||||
// Opts may include [AccessTypeOnline] or [AccessTypeOffline], as well
|
||||
// as [ApprovalForce].
|
||||
//
|
||||
// To protect against CSRF attacks, opts should include a PKCE challenge
|
||||
// (S256ChallengeOption). Not all servers support PKCE. An alternative is to
|
||||
@ -194,7 +194,7 @@ func (c *Config) AuthCodeURL(state string, opts ...AuthCodeOption) string {
|
||||
// and when other authorization grant types are not available."
|
||||
// See https://tools.ietf.org/html/rfc6749#section-4.3 for more info.
|
||||
//
|
||||
// The provided context optionally controls which HTTP client is used. See the HTTPClient variable.
|
||||
// The provided context optionally controls which HTTP client is used. See the [HTTPClient] variable.
|
||||
func (c *Config) PasswordCredentialsToken(ctx context.Context, username, password string) (*Token, error) {
|
||||
v := url.Values{
|
||||
"grant_type": {"password"},
|
||||
@ -212,10 +212,10 @@ func (c *Config) PasswordCredentialsToken(ctx context.Context, username, passwor
|
||||
// It is used after a resource provider redirects the user back
|
||||
// to the Redirect URI (the URL obtained from AuthCodeURL).
|
||||
//
|
||||
// The provided context optionally controls which HTTP client is used. See the HTTPClient variable.
|
||||
// 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") if you are
|
||||
// The code will be in the [http.Request.FormValue]("code"). Before
|
||||
// calling Exchange, be sure to validate [http.Request.FormValue]("state") if you are
|
||||
// using it to protect against CSRF attacks.
|
||||
//
|
||||
// If using PKCE to protect against CSRF attacks, opts should include a
|
||||
@ -242,10 +242,10 @@ func (c *Config) Client(ctx context.Context, t *Token) *http.Client {
|
||||
return NewClient(ctx, c.TokenSource(ctx, t))
|
||||
}
|
||||
|
||||
// TokenSource returns a TokenSource that returns t until t expires,
|
||||
// TokenSource returns a [TokenSource] that returns t until t expires,
|
||||
// automatically refreshing it as necessary using the provided context.
|
||||
//
|
||||
// Most users will use Config.Client instead.
|
||||
// Most users will use [Config.Client] instead.
|
||||
func (c *Config) TokenSource(ctx context.Context, t *Token) TokenSource {
|
||||
tkr := &tokenRefresher{
|
||||
ctx: ctx,
|
||||
@ -260,7 +260,7 @@ func (c *Config) TokenSource(ctx context.Context, t *Token) TokenSource {
|
||||
}
|
||||
}
|
||||
|
||||
// tokenRefresher is a TokenSource that makes "grant_type"=="refresh_token"
|
||||
// tokenRefresher is a TokenSource that makes "grant_type=refresh_token"
|
||||
// HTTP requests to renew a token using a RefreshToken.
|
||||
type tokenRefresher struct {
|
||||
ctx context.Context // used to get HTTP requests
|
||||
@ -288,7 +288,7 @@ func (tf *tokenRefresher) Token() (*Token, error) {
|
||||
if tf.refreshToken != tk.RefreshToken {
|
||||
tf.refreshToken = tk.RefreshToken
|
||||
}
|
||||
return tk, err
|
||||
return tk, nil
|
||||
}
|
||||
|
||||
// reuseTokenSource is a TokenSource that holds a single token in memory
|
||||
@ -305,8 +305,7 @@ type reuseTokenSource struct {
|
||||
}
|
||||
|
||||
// Token returns the current token if it's still valid, else will
|
||||
// refresh the current token (using r.Context for HTTP client
|
||||
// information) and return the new one.
|
||||
// refresh the current token and return the new one.
|
||||
func (s *reuseTokenSource) Token() (*Token, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
@ -322,7 +321,7 @@ func (s *reuseTokenSource) Token() (*Token, error) {
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// StaticTokenSource returns a TokenSource that always returns the same token.
|
||||
// StaticTokenSource returns a [TokenSource] that always returns the same token.
|
||||
// Because the provided token t is never refreshed, StaticTokenSource is only
|
||||
// useful for tokens that never expire.
|
||||
func StaticTokenSource(t *Token) TokenSource {
|
||||
@ -338,16 +337,16 @@ func (s staticTokenSource) Token() (*Token, error) {
|
||||
return s.t, nil
|
||||
}
|
||||
|
||||
// HTTPClient is the context key to use with golang.org/x/net/context's
|
||||
// WithValue function to associate an *http.Client value with a context.
|
||||
// HTTPClient is the context key to use with [context.WithValue]
|
||||
// to associate a [*http.Client] value with a context.
|
||||
var HTTPClient internal.ContextKey
|
||||
|
||||
// NewClient creates an *http.Client from a Context and TokenSource.
|
||||
// NewClient creates an [*http.Client] from a [context.Context] and [TokenSource].
|
||||
// The returned client is not valid beyond the lifetime of the context.
|
||||
//
|
||||
// Note that if a custom *http.Client is provided via the Context it
|
||||
// Note that if a custom [*http.Client] is provided via the [context.Context] it
|
||||
// is used only for token acquisition and is not used to configure the
|
||||
// *http.Client returned from NewClient.
|
||||
// [*http.Client] returned from NewClient.
|
||||
//
|
||||
// As a special case, if src is nil, a non-OAuth2 client is returned
|
||||
// using the provided context. This exists to support related OAuth2
|
||||
@ -356,15 +355,19 @@ func NewClient(ctx context.Context, src TokenSource) *http.Client {
|
||||
if src == nil {
|
||||
return internal.ContextClient(ctx)
|
||||
}
|
||||
cc := internal.ContextClient(ctx)
|
||||
return &http.Client{
|
||||
Transport: &Transport{
|
||||
Base: internal.ContextClient(ctx).Transport,
|
||||
Base: cc.Transport,
|
||||
Source: ReuseTokenSource(nil, src),
|
||||
},
|
||||
CheckRedirect: cc.CheckRedirect,
|
||||
Jar: cc.Jar,
|
||||
Timeout: cc.Timeout,
|
||||
}
|
||||
}
|
||||
|
||||
// ReuseTokenSource returns a TokenSource which repeatedly returns the
|
||||
// ReuseTokenSource returns a [TokenSource] which repeatedly returns the
|
||||
// same token as long as it's valid, starting with t.
|
||||
// When its cached token is invalid, a new token is obtained from src.
|
||||
//
|
||||
@ -372,10 +375,10 @@ func NewClient(ctx context.Context, src TokenSource) *http.Client {
|
||||
// (such as a file on disk) between runs of a program, rather than
|
||||
// obtaining new tokens unnecessarily.
|
||||
//
|
||||
// The initial token t may be nil, in which case the TokenSource is
|
||||
// The initial token t may be nil, in which case the [TokenSource] is
|
||||
// wrapped in a caching version if it isn't one already. This also
|
||||
// means it's always safe to wrap ReuseTokenSource around any other
|
||||
// TokenSource without adverse effects.
|
||||
// [TokenSource] without adverse effects.
|
||||
func ReuseTokenSource(t *Token, src TokenSource) TokenSource {
|
||||
// Don't wrap a reuseTokenSource in itself. That would work,
|
||||
// but cause an unnecessary number of mutex operations.
|
||||
@ -393,8 +396,8 @@ func ReuseTokenSource(t *Token, src TokenSource) TokenSource {
|
||||
}
|
||||
}
|
||||
|
||||
// ReuseTokenSource returns a TokenSource that acts in the same manner as the
|
||||
// TokenSource returned by ReuseTokenSource, except the expiry buffer is
|
||||
// ReuseTokenSourceWithExpiry returns a [TokenSource] that acts in the same manner as the
|
||||
// [TokenSource] returned by [ReuseTokenSource], except the expiry buffer is
|
||||
// configurable. The expiration time of a token is calculated as
|
||||
// t.Expiry.Add(-earlyExpiry).
|
||||
func ReuseTokenSourceWithExpiry(t *Token, src TokenSource, earlyExpiry time.Duration) TokenSource {
|
||||
|
@ -9,7 +9,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
@ -104,7 +103,7 @@ func TestExchangeRequest(t *testing.T) {
|
||||
if headerContentType != "application/x-www-form-urlencoded" {
|
||||
t.Errorf("Unexpected Content-Type header %q", headerContentType)
|
||||
}
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
t.Errorf("Failed reading request body: %s.", err)
|
||||
}
|
||||
@ -148,7 +147,7 @@ func TestExchangeRequest_CustomParam(t *testing.T) {
|
||||
if headerContentType != "application/x-www-form-urlencoded" {
|
||||
t.Errorf("Unexpected Content-Type header, %v is found.", headerContentType)
|
||||
}
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
t.Errorf("Failed reading request body: %s.", err)
|
||||
}
|
||||
@ -194,7 +193,7 @@ func TestExchangeRequest_JSONResponse(t *testing.T) {
|
||||
if headerContentType != "application/x-www-form-urlencoded" {
|
||||
t.Errorf("Unexpected Content-Type header, %v is found.", headerContentType)
|
||||
}
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
t.Errorf("Failed reading request body: %s.", err)
|
||||
}
|
||||
@ -301,7 +300,7 @@ func testExchangeRequest_JSONResponse_expiry(t *testing.T, exp string, want, nul
|
||||
conf := newConf(ts.URL)
|
||||
t1 := time.Now().Add(day)
|
||||
tok, err := conf.Exchange(context.Background(), "exchange-code")
|
||||
t2 := t1.Add(day)
|
||||
t2 := time.Now().Add(day)
|
||||
|
||||
if got := (err == nil); got != want {
|
||||
if want {
|
||||
@ -393,7 +392,7 @@ func TestPasswordCredentialsTokenRequest(t *testing.T) {
|
||||
if headerContentType != expected {
|
||||
t.Errorf("Content-Type header = %q; want %q", headerContentType, expected)
|
||||
}
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
t.Errorf("Failed reading request body: %s.", err)
|
||||
}
|
||||
@ -435,7 +434,7 @@ func TestTokenRefreshRequest(t *testing.T) {
|
||||
if headerContentType != "application/x-www-form-urlencoded" {
|
||||
t.Errorf("Unexpected Content-Type header %q", headerContentType)
|
||||
}
|
||||
body, _ := ioutil.ReadAll(r.Body)
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
if string(body) != "grant_type=refresh_token&refresh_token=REFRESH_TOKEN" {
|
||||
t.Errorf("Unexpected refresh token payload %q", body)
|
||||
}
|
||||
@ -460,7 +459,7 @@ func TestFetchWithNoRefreshToken(t *testing.T) {
|
||||
if headerContentType != "application/x-www-form-urlencoded" {
|
||||
t.Errorf("Unexpected Content-Type header, %v is found.", headerContentType)
|
||||
}
|
||||
body, _ := ioutil.ReadAll(r.Body)
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
if string(body) != "client_id=CLIENT_ID&grant_type=refresh_token&refresh_token=REFRESH_TOKEN" {
|
||||
t.Errorf("Unexpected refresh token payload, %v is found.", string(body))
|
||||
}
|
||||
|
15
pkce.go
15
pkce.go
@ -1,6 +1,7 @@
|
||||
// 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 (
|
||||
@ -20,9 +21,9 @@ const (
|
||||
// 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).
|
||||
// The resulting verifier should be passed to [Config.AuthCodeURL] or [Config.DeviceAuth]
|
||||
// with [S256ChallengeOption], and to [Config.Exchange] or [Config.DeviceAccessToken]
|
||||
// with [VerifierOption].
|
||||
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
|
||||
@ -36,22 +37,22 @@ func GenerateVerifier() string {
|
||||
return base64.RawURLEncoding.EncodeToString(data)
|
||||
}
|
||||
|
||||
// VerifierOption returns a PKCE code verifier AuthCodeOption. It should be
|
||||
// passed to Config.Exchange or Config.DeviceAccessToken only.
|
||||
// VerifierOption returns a PKCE code verifier [AuthCodeOption]. It should only be
|
||||
// passed to [Config.Exchange] or [Config.DeviceAccessToken].
|
||||
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.
|
||||
// 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
|
||||
// method S256. It should be passed to [Config.AuthCodeURL] or [Config.DeviceAuth]
|
||||
// only.
|
||||
func S256ChallengeOption(verifier string) AuthCodeOption {
|
||||
return challengeOption{
|
||||
|
24
token.go
24
token.go
@ -44,14 +44,21 @@ type Token struct {
|
||||
|
||||
// Expiry is the optional expiration time of the access token.
|
||||
//
|
||||
// If zero, TokenSource implementations will reuse the same
|
||||
// If zero, [TokenSource] implementations will reuse the same
|
||||
// token forever and RefreshToken or equivalent
|
||||
// mechanisms for that TokenSource will not be used.
|
||||
Expiry time.Time `json:"expiry,omitempty"`
|
||||
|
||||
// ExpiresIn is the OAuth2 wire format "expires_in" field,
|
||||
// which specifies how many seconds later the token expires,
|
||||
// relative to an unknown time base approximately around "now".
|
||||
// It is the application's responsibility to populate
|
||||
// `Expiry` from `ExpiresIn` when required.
|
||||
ExpiresIn int64 `json:"expires_in,omitempty"`
|
||||
|
||||
// raw optionally contains extra metadata from the server
|
||||
// when updating a token.
|
||||
raw interface{}
|
||||
raw any
|
||||
|
||||
// expiryDelta is used to calculate when a token is considered
|
||||
// expired, by subtracting from Expiry. If zero, defaultExpiryDelta
|
||||
@ -79,16 +86,16 @@ func (t *Token) Type() string {
|
||||
// SetAuthHeader sets the Authorization header to r using the access
|
||||
// token in t.
|
||||
//
|
||||
// This method is unnecessary when using Transport or an HTTP Client
|
||||
// This method is unnecessary when using [Transport] or an HTTP Client
|
||||
// returned by this package.
|
||||
func (t *Token) SetAuthHeader(r *http.Request) {
|
||||
r.Header.Set("Authorization", t.Type()+" "+t.AccessToken)
|
||||
}
|
||||
|
||||
// WithExtra returns a new Token that's a clone of t, but using the
|
||||
// WithExtra returns a new [Token] that's a clone of t, but using the
|
||||
// provided raw extra map. This is only intended for use by packages
|
||||
// implementing derivative OAuth2 flows.
|
||||
func (t *Token) WithExtra(extra interface{}) *Token {
|
||||
func (t *Token) WithExtra(extra any) *Token {
|
||||
t2 := new(Token)
|
||||
*t2 = *t
|
||||
t2.raw = extra
|
||||
@ -98,8 +105,8 @@ func (t *Token) WithExtra(extra interface{}) *Token {
|
||||
// Extra returns an extra field.
|
||||
// Extra fields are key-value pairs returned by the server as a
|
||||
// part of the token retrieval response.
|
||||
func (t *Token) Extra(key string) interface{} {
|
||||
if raw, ok := t.raw.(map[string]interface{}); ok {
|
||||
func (t *Token) Extra(key string) any {
|
||||
if raw, ok := t.raw.(map[string]any); ok {
|
||||
return raw[key]
|
||||
}
|
||||
|
||||
@ -156,13 +163,14 @@ func tokenFromInternal(t *internal.Token) *Token {
|
||||
TokenType: t.TokenType,
|
||||
RefreshToken: t.RefreshToken,
|
||||
Expiry: t.Expiry,
|
||||
ExpiresIn: t.ExpiresIn,
|
||||
raw: t.Raw,
|
||||
}
|
||||
}
|
||||
|
||||
// retrieveToken takes a *Config and uses that to retrieve an *internal.Token.
|
||||
// This token is then mapped from *internal.Token into an *oauth2.Token which is returned along
|
||||
// with an error..
|
||||
// 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), c.authStyleCache.Get())
|
||||
if err != nil {
|
||||
|
@ -12,8 +12,8 @@ import (
|
||||
func TestTokenExtra(t *testing.T) {
|
||||
type testCase struct {
|
||||
key string
|
||||
val interface{}
|
||||
want interface{}
|
||||
val any
|
||||
want any
|
||||
}
|
||||
const key = "extra-key"
|
||||
cases := []testCase{
|
||||
@ -23,7 +23,7 @@ func TestTokenExtra(t *testing.T) {
|
||||
{key: "other-key", val: "def", want: nil},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
extra := make(map[string]interface{})
|
||||
extra := make(map[string]any)
|
||||
extra[tc.key] = tc.val
|
||||
tok := &Token{raw: extra}
|
||||
if got, want := tok.Extra(key), tc.want; got != want {
|
||||
|
24
transport.go
24
transport.go
@ -11,12 +11,12 @@ import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Transport is an http.RoundTripper that makes OAuth 2.0 HTTP requests,
|
||||
// wrapping a base RoundTripper and adding an Authorization header
|
||||
// with a token from the supplied Sources.
|
||||
// Transport is an [http.RoundTripper] that makes OAuth 2.0 HTTP requests,
|
||||
// wrapping a base [http.RoundTripper] and adding an Authorization header
|
||||
// with a token from the supplied [TokenSource].
|
||||
//
|
||||
// Transport is a low-level mechanism. Most code will use the
|
||||
// higher-level Config.Client method instead.
|
||||
// higher-level [Config.Client] method instead.
|
||||
type Transport struct {
|
||||
// Source supplies the token to add to outgoing requests'
|
||||
// Authorization headers.
|
||||
@ -47,7 +47,7 @@ func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req2 := cloneRequest(req) // per RoundTripper contract
|
||||
req2 := req.Clone(req.Context())
|
||||
token.SetAuthHeader(req2)
|
||||
|
||||
// req.Body is assumed to be closed by the base RoundTripper.
|
||||
@ -73,17 +73,3 @@ func (t *Transport) base() http.RoundTripper {
|
||||
}
|
||||
return http.DefaultTransport
|
||||
}
|
||||
|
||||
// cloneRequest returns a clone of the provided *http.Request.
|
||||
// The clone is a shallow copy of the struct and its Header map.
|
||||
func cloneRequest(r *http.Request) *http.Request {
|
||||
// shallow copy of the struct
|
||||
r2 := new(http.Request)
|
||||
*r2 = *r
|
||||
// deep copy of the Header
|
||||
r2.Header = make(http.Header, len(r.Header))
|
||||
for k, s := range r.Header {
|
||||
r2.Header[k] = append([]string(nil), s...)
|
||||
}
|
||||
return r2
|
||||
}
|
||||
|
@ -9,12 +9,6 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type tokenSource struct{ token *Token }
|
||||
|
||||
func (t *tokenSource) Token() (*Token, error) {
|
||||
return t.token, nil
|
||||
}
|
||||
|
||||
func TestTransportNilTokenSource(t *testing.T) {
|
||||
tr := &Transport{}
|
||||
server := newMockServer(func(w http.ResponseWriter, r *http.Request) {})
|
||||
@ -88,13 +82,10 @@ func TestTransportCloseRequestBodySuccess(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTransportTokenSource(t *testing.T) {
|
||||
ts := &tokenSource{
|
||||
token: &Token{
|
||||
AccessToken: "abc",
|
||||
},
|
||||
}
|
||||
tr := &Transport{
|
||||
Source: ts,
|
||||
Source: StaticTokenSource(&Token{
|
||||
AccessToken: "abc",
|
||||
}),
|
||||
}
|
||||
server := newMockServer(func(w http.ResponseWriter, r *http.Request) {
|
||||
if got, want := r.Header.Get("Authorization"), "Bearer abc"; got != want {
|
||||
@ -123,14 +114,11 @@ func TestTransportTokenSourceTypes(t *testing.T) {
|
||||
{key: "basic", val: val, want: "Basic abc"},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
ts := &tokenSource{
|
||||
token: &Token{
|
||||
tr := &Transport{
|
||||
Source: StaticTokenSource(&Token{
|
||||
AccessToken: tc.val,
|
||||
TokenType: tc.key,
|
||||
},
|
||||
}
|
||||
tr := &Transport{
|
||||
Source: ts,
|
||||
}),
|
||||
}
|
||||
server := newMockServer(func(w http.ResponseWriter, r *http.Request) {
|
||||
if got, want := r.Header.Get("Authorization"), tc.want; got != want {
|
||||
|
Loading…
x
Reference in New Issue
Block a user