chore: migrate to gitea
This commit is contained in:
618
vendor/cloud.google.com/go/auth/auth.go
generated
vendored
Normal file
618
vendor/cloud.google.com/go/auth/auth.go
generated
vendored
Normal file
@@ -0,0 +1,618 @@
|
||||
// Copyright 2023 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package auth provides utilities for managing Google Cloud credentials,
|
||||
// including functionality for creating, caching, and refreshing OAuth2 tokens.
|
||||
// It offers customizable options for different OAuth2 flows, such as 2-legged
|
||||
// (2LO) and 3-legged (3LO) OAuth, along with support for PKCE and automatic
|
||||
// token management.
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"cloud.google.com/go/auth/internal"
|
||||
"cloud.google.com/go/auth/internal/jwt"
|
||||
"github.com/googleapis/gax-go/v2/internallog"
|
||||
)
|
||||
|
||||
const (
|
||||
// Parameter keys for AuthCodeURL method to support PKCE.
|
||||
codeChallengeKey = "code_challenge"
|
||||
codeChallengeMethodKey = "code_challenge_method"
|
||||
|
||||
// Parameter key for Exchange method to support PKCE.
|
||||
codeVerifierKey = "code_verifier"
|
||||
|
||||
// 3 minutes and 45 seconds before expiration. The shortest MDS cache is 4 minutes,
|
||||
// so we give it 15 seconds to refresh it's cache before attempting to refresh a token.
|
||||
defaultExpiryDelta = 225 * time.Second
|
||||
|
||||
universeDomainDefault = "googleapis.com"
|
||||
)
|
||||
|
||||
// tokenState represents different states for a [Token].
|
||||
type tokenState int
|
||||
|
||||
const (
|
||||
// fresh indicates that the [Token] is valid. It is not expired or close to
|
||||
// expired, or the token has no expiry.
|
||||
fresh tokenState = iota
|
||||
// stale indicates that the [Token] is close to expired, and should be
|
||||
// refreshed. The token can be used normally.
|
||||
stale
|
||||
// invalid indicates that the [Token] is expired or invalid. The token
|
||||
// cannot be used for a normal operation.
|
||||
invalid
|
||||
)
|
||||
|
||||
var (
|
||||
defaultGrantType = "urn:ietf:params:oauth:grant-type:jwt-bearer"
|
||||
defaultHeader = &jwt.Header{Algorithm: jwt.HeaderAlgRSA256, Type: jwt.HeaderType}
|
||||
|
||||
// for testing
|
||||
timeNow = time.Now
|
||||
)
|
||||
|
||||
// TokenProvider specifies an interface for anything that can return a token.
|
||||
type TokenProvider interface {
|
||||
// Token returns a Token or an error.
|
||||
// The Token returned must be safe to use
|
||||
// concurrently.
|
||||
// The returned Token must not be modified.
|
||||
// The context provided must be sent along to any requests that are made in
|
||||
// the implementing code.
|
||||
Token(context.Context) (*Token, error)
|
||||
}
|
||||
|
||||
// Token holds the credential token used to authorized requests. All fields are
|
||||
// considered read-only.
|
||||
type Token struct {
|
||||
// Value is the token used to authorize requests. It is usually an access
|
||||
// token but may be other types of tokens such as ID tokens in some flows.
|
||||
Value string
|
||||
// Type is the type of token Value is. If uninitialized, it should be
|
||||
// assumed to be a "Bearer" token.
|
||||
Type string
|
||||
// Expiry is the time the token is set to expire.
|
||||
Expiry time.Time
|
||||
// Metadata may include, but is not limited to, the body of the token
|
||||
// response returned by the server.
|
||||
Metadata map[string]interface{} // TODO(codyoss): maybe make a method to flatten metadata to avoid []string for url.Values
|
||||
}
|
||||
|
||||
// IsValid reports that a [Token] is non-nil, has a [Token.Value], and has not
|
||||
// expired. A token is considered expired if [Token.Expiry] has passed or will
|
||||
// pass in the next 225 seconds.
|
||||
func (t *Token) IsValid() bool {
|
||||
return t.isValidWithEarlyExpiry(defaultExpiryDelta)
|
||||
}
|
||||
|
||||
// MetadataString is a convenience method for accessing string values in the
|
||||
// token's metadata. Returns an empty string if the metadata is nil or the value
|
||||
// for the given key cannot be cast to a string.
|
||||
func (t *Token) MetadataString(k string) string {
|
||||
if t.Metadata == nil {
|
||||
return ""
|
||||
}
|
||||
s, ok := t.Metadata[k].(string)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (t *Token) isValidWithEarlyExpiry(earlyExpiry time.Duration) bool {
|
||||
if t.isEmpty() {
|
||||
return false
|
||||
}
|
||||
if t.Expiry.IsZero() {
|
||||
return true
|
||||
}
|
||||
return !t.Expiry.Round(0).Add(-earlyExpiry).Before(timeNow())
|
||||
}
|
||||
|
||||
func (t *Token) isEmpty() bool {
|
||||
return t == nil || t.Value == ""
|
||||
}
|
||||
|
||||
// Credentials holds Google credentials, including
|
||||
// [Application Default Credentials].
|
||||
//
|
||||
// [Application Default Credentials]: https://developers.google.com/accounts/docs/application-default-credentials
|
||||
type Credentials struct {
|
||||
json []byte
|
||||
projectID CredentialsPropertyProvider
|
||||
quotaProjectID CredentialsPropertyProvider
|
||||
// universeDomain is the default service domain for a given Cloud universe.
|
||||
universeDomain CredentialsPropertyProvider
|
||||
|
||||
TokenProvider
|
||||
}
|
||||
|
||||
// JSON returns the bytes associated with the the file used to source
|
||||
// credentials if one was used.
|
||||
func (c *Credentials) JSON() []byte {
|
||||
return c.json
|
||||
}
|
||||
|
||||
// ProjectID returns the associated project ID from the underlying file or
|
||||
// environment.
|
||||
func (c *Credentials) ProjectID(ctx context.Context) (string, error) {
|
||||
if c.projectID == nil {
|
||||
return internal.GetProjectID(c.json, ""), nil
|
||||
}
|
||||
v, err := c.projectID.GetProperty(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return internal.GetProjectID(c.json, v), nil
|
||||
}
|
||||
|
||||
// QuotaProjectID returns the associated quota project ID from the underlying
|
||||
// file or environment.
|
||||
func (c *Credentials) QuotaProjectID(ctx context.Context) (string, error) {
|
||||
if c.quotaProjectID == nil {
|
||||
return internal.GetQuotaProject(c.json, ""), nil
|
||||
}
|
||||
v, err := c.quotaProjectID.GetProperty(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return internal.GetQuotaProject(c.json, v), nil
|
||||
}
|
||||
|
||||
// UniverseDomain returns the default service domain for a given Cloud universe.
|
||||
// The default value is "googleapis.com".
|
||||
func (c *Credentials) UniverseDomain(ctx context.Context) (string, error) {
|
||||
if c.universeDomain == nil {
|
||||
return universeDomainDefault, nil
|
||||
}
|
||||
v, err := c.universeDomain.GetProperty(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if v == "" {
|
||||
return universeDomainDefault, nil
|
||||
}
|
||||
return v, err
|
||||
}
|
||||
|
||||
// CredentialsPropertyProvider provides an implementation to fetch a property
|
||||
// value for [Credentials].
|
||||
type CredentialsPropertyProvider interface {
|
||||
GetProperty(context.Context) (string, error)
|
||||
}
|
||||
|
||||
// CredentialsPropertyFunc is a type adapter to allow the use of ordinary
|
||||
// functions as a [CredentialsPropertyProvider].
|
||||
type CredentialsPropertyFunc func(context.Context) (string, error)
|
||||
|
||||
// GetProperty loads the properly value provided the given context.
|
||||
func (p CredentialsPropertyFunc) GetProperty(ctx context.Context) (string, error) {
|
||||
return p(ctx)
|
||||
}
|
||||
|
||||
// CredentialsOptions are used to configure [Credentials].
|
||||
type CredentialsOptions struct {
|
||||
// TokenProvider is a means of sourcing a token for the credentials. Required.
|
||||
TokenProvider TokenProvider
|
||||
// JSON is the raw contents of the credentials file if sourced from a file.
|
||||
JSON []byte
|
||||
// ProjectIDProvider resolves the project ID associated with the
|
||||
// credentials.
|
||||
ProjectIDProvider CredentialsPropertyProvider
|
||||
// QuotaProjectIDProvider resolves the quota project ID associated with the
|
||||
// credentials.
|
||||
QuotaProjectIDProvider CredentialsPropertyProvider
|
||||
// UniverseDomainProvider resolves the universe domain with the credentials.
|
||||
UniverseDomainProvider CredentialsPropertyProvider
|
||||
}
|
||||
|
||||
// NewCredentials returns new [Credentials] from the provided options.
|
||||
func NewCredentials(opts *CredentialsOptions) *Credentials {
|
||||
creds := &Credentials{
|
||||
TokenProvider: opts.TokenProvider,
|
||||
json: opts.JSON,
|
||||
projectID: opts.ProjectIDProvider,
|
||||
quotaProjectID: opts.QuotaProjectIDProvider,
|
||||
universeDomain: opts.UniverseDomainProvider,
|
||||
}
|
||||
|
||||
return creds
|
||||
}
|
||||
|
||||
// CachedTokenProviderOptions provides options for configuring a cached
|
||||
// [TokenProvider].
|
||||
type CachedTokenProviderOptions struct {
|
||||
// DisableAutoRefresh makes the TokenProvider always return the same token,
|
||||
// even if it is expired. The default is false. Optional.
|
||||
DisableAutoRefresh bool
|
||||
// ExpireEarly configures the amount of time before a token expires, that it
|
||||
// should be refreshed. If unset, the default value is 3 minutes and 45
|
||||
// seconds. Optional.
|
||||
ExpireEarly time.Duration
|
||||
// DisableAsyncRefresh configures a synchronous workflow that refreshes
|
||||
// tokens in a blocking manner. The default is false. Optional.
|
||||
DisableAsyncRefresh bool
|
||||
}
|
||||
|
||||
func (ctpo *CachedTokenProviderOptions) autoRefresh() bool {
|
||||
if ctpo == nil {
|
||||
return true
|
||||
}
|
||||
return !ctpo.DisableAutoRefresh
|
||||
}
|
||||
|
||||
func (ctpo *CachedTokenProviderOptions) expireEarly() time.Duration {
|
||||
if ctpo == nil || ctpo.ExpireEarly == 0 {
|
||||
return defaultExpiryDelta
|
||||
}
|
||||
return ctpo.ExpireEarly
|
||||
}
|
||||
|
||||
func (ctpo *CachedTokenProviderOptions) blockingRefresh() bool {
|
||||
if ctpo == nil {
|
||||
return false
|
||||
}
|
||||
return ctpo.DisableAsyncRefresh
|
||||
}
|
||||
|
||||
// NewCachedTokenProvider wraps a [TokenProvider] to cache the tokens returned
|
||||
// by the underlying provider. By default it will refresh tokens asynchronously
|
||||
// a few minutes before they expire.
|
||||
func NewCachedTokenProvider(tp TokenProvider, opts *CachedTokenProviderOptions) TokenProvider {
|
||||
if ctp, ok := tp.(*cachedTokenProvider); ok {
|
||||
return ctp
|
||||
}
|
||||
return &cachedTokenProvider{
|
||||
tp: tp,
|
||||
autoRefresh: opts.autoRefresh(),
|
||||
expireEarly: opts.expireEarly(),
|
||||
blockingRefresh: opts.blockingRefresh(),
|
||||
}
|
||||
}
|
||||
|
||||
type cachedTokenProvider struct {
|
||||
tp TokenProvider
|
||||
autoRefresh bool
|
||||
expireEarly time.Duration
|
||||
blockingRefresh bool
|
||||
|
||||
mu sync.Mutex
|
||||
cachedToken *Token
|
||||
// isRefreshRunning ensures that the non-blocking refresh will only be
|
||||
// attempted once, even if multiple callers enter the Token method.
|
||||
isRefreshRunning bool
|
||||
// isRefreshErr ensures that the non-blocking refresh will only be attempted
|
||||
// once per refresh window if an error is encountered.
|
||||
isRefreshErr bool
|
||||
}
|
||||
|
||||
func (c *cachedTokenProvider) Token(ctx context.Context) (*Token, error) {
|
||||
if c.blockingRefresh {
|
||||
return c.tokenBlocking(ctx)
|
||||
}
|
||||
return c.tokenNonBlocking(ctx)
|
||||
}
|
||||
|
||||
func (c *cachedTokenProvider) tokenNonBlocking(ctx context.Context) (*Token, error) {
|
||||
switch c.tokenState() {
|
||||
case fresh:
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.cachedToken, nil
|
||||
case stale:
|
||||
// Call tokenAsync with a new Context because the user-provided context
|
||||
// may have a short timeout incompatible with async token refresh.
|
||||
c.tokenAsync(context.Background())
|
||||
// Return the stale token immediately to not block customer requests to Cloud services.
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.cachedToken, nil
|
||||
default: // invalid
|
||||
return c.tokenBlocking(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// tokenState reports the token's validity.
|
||||
func (c *cachedTokenProvider) tokenState() tokenState {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
t := c.cachedToken
|
||||
now := timeNow()
|
||||
if t == nil || t.Value == "" {
|
||||
return invalid
|
||||
} else if t.Expiry.IsZero() {
|
||||
return fresh
|
||||
} else if now.After(t.Expiry.Round(0)) {
|
||||
return invalid
|
||||
} else if now.After(t.Expiry.Round(0).Add(-c.expireEarly)) {
|
||||
return stale
|
||||
}
|
||||
return fresh
|
||||
}
|
||||
|
||||
// tokenAsync uses a bool to ensure that only one non-blocking token refresh
|
||||
// happens at a time, even if multiple callers have entered this function
|
||||
// concurrently. This avoids creating an arbitrary number of concurrent
|
||||
// goroutines. Retries should be attempted and managed within the Token method.
|
||||
// If the refresh attempt fails, no further attempts are made until the refresh
|
||||
// window expires and the token enters the invalid state, at which point the
|
||||
// blocking call to Token should likely return the same error on the main goroutine.
|
||||
func (c *cachedTokenProvider) tokenAsync(ctx context.Context) {
|
||||
fn := func() {
|
||||
t, err := c.tp.Token(ctx)
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.isRefreshRunning = false
|
||||
if err != nil {
|
||||
// Discard errors from the non-blocking refresh, but prevent further
|
||||
// attempts.
|
||||
c.isRefreshErr = true
|
||||
return
|
||||
}
|
||||
c.cachedToken = t
|
||||
}
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if !c.isRefreshRunning && !c.isRefreshErr {
|
||||
c.isRefreshRunning = true
|
||||
go fn()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *cachedTokenProvider) tokenBlocking(ctx context.Context) (*Token, error) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.isRefreshErr = false
|
||||
if c.cachedToken.IsValid() || (!c.autoRefresh && !c.cachedToken.isEmpty()) {
|
||||
return c.cachedToken, nil
|
||||
}
|
||||
t, err := c.tp.Token(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.cachedToken = t
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// Error is a error associated with retrieving a [Token]. It can hold useful
|
||||
// additional details for debugging.
|
||||
type Error struct {
|
||||
// Response is the HTTP response associated with error. The body will always
|
||||
// be already closed and consumed.
|
||||
Response *http.Response
|
||||
// Body is the HTTP response body.
|
||||
Body []byte
|
||||
// Err is the underlying wrapped error.
|
||||
Err error
|
||||
|
||||
// code returned in the token response
|
||||
code string
|
||||
// description returned in the token response
|
||||
description string
|
||||
// uri returned in the token response
|
||||
uri string
|
||||
}
|
||||
|
||||
func (e *Error) Error() string {
|
||||
if e.code != "" {
|
||||
s := fmt.Sprintf("auth: %q", e.code)
|
||||
if e.description != "" {
|
||||
s += fmt.Sprintf(" %q", e.description)
|
||||
}
|
||||
if e.uri != "" {
|
||||
s += fmt.Sprintf(" %q", e.uri)
|
||||
}
|
||||
return s
|
||||
}
|
||||
return fmt.Sprintf("auth: cannot fetch token: %v\nResponse: %s", e.Response.StatusCode, e.Body)
|
||||
}
|
||||
|
||||
// Temporary returns true if the error is considered temporary and may be able
|
||||
// to be retried.
|
||||
func (e *Error) Temporary() bool {
|
||||
if e.Response == nil {
|
||||
return false
|
||||
}
|
||||
sc := e.Response.StatusCode
|
||||
return sc == http.StatusInternalServerError || sc == http.StatusServiceUnavailable || sc == http.StatusRequestTimeout || sc == http.StatusTooManyRequests
|
||||
}
|
||||
|
||||
func (e *Error) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
||||
|
||||
// Style describes how the token endpoint wants to receive the ClientID and
|
||||
// ClientSecret.
|
||||
type Style int
|
||||
|
||||
const (
|
||||
// StyleUnknown means the value has not been initiated. Sending this in
|
||||
// a request will cause the token exchange to fail.
|
||||
StyleUnknown Style = iota
|
||||
// StyleInParams sends client info in the body of a POST request.
|
||||
StyleInParams
|
||||
// StyleInHeader sends client info using Basic Authorization header.
|
||||
StyleInHeader
|
||||
)
|
||||
|
||||
// Options2LO is the configuration settings for doing a 2-legged JWT OAuth2 flow.
|
||||
type Options2LO struct {
|
||||
// Email is the OAuth2 client ID. This value is set as the "iss" in the
|
||||
// JWT.
|
||||
Email string
|
||||
// PrivateKey contains the contents of an RSA private key or the
|
||||
// contents of a PEM file that contains a private key. It is used to sign
|
||||
// the JWT created.
|
||||
PrivateKey []byte
|
||||
// TokenURL is th URL the JWT is sent to. Required.
|
||||
TokenURL string
|
||||
// PrivateKeyID is the ID of the key used to sign the JWT. It is used as the
|
||||
// "kid" in the JWT header. Optional.
|
||||
PrivateKeyID string
|
||||
// Subject is the used for to impersonate a user. It is used as the "sub" in
|
||||
// the JWT.m Optional.
|
||||
Subject string
|
||||
// Scopes specifies requested permissions for the token. Optional.
|
||||
Scopes []string
|
||||
// Expires specifies the lifetime of the token. Optional.
|
||||
Expires time.Duration
|
||||
// Audience specifies the "aud" in the JWT. Optional.
|
||||
Audience string
|
||||
// PrivateClaims allows specifying any custom claims for the JWT. Optional.
|
||||
PrivateClaims map[string]interface{}
|
||||
// UniverseDomain is the default service domain for a given Cloud universe.
|
||||
UniverseDomain string
|
||||
|
||||
// Client is the client to be used to make the underlying token requests.
|
||||
// Optional.
|
||||
Client *http.Client
|
||||
// UseIDToken requests that the token returned be an ID token if one is
|
||||
// returned from the server. Optional.
|
||||
UseIDToken bool
|
||||
// Logger is used for debug logging. If provided, logging will be enabled
|
||||
// at the loggers configured level. By default logging is disabled unless
|
||||
// enabled by setting GOOGLE_SDK_GO_LOGGING_LEVEL in which case a default
|
||||
// logger will be used. Optional.
|
||||
Logger *slog.Logger
|
||||
}
|
||||
|
||||
func (o *Options2LO) client() *http.Client {
|
||||
if o.Client != nil {
|
||||
return o.Client
|
||||
}
|
||||
return internal.DefaultClient()
|
||||
}
|
||||
|
||||
func (o *Options2LO) validate() error {
|
||||
if o == nil {
|
||||
return errors.New("auth: options must be provided")
|
||||
}
|
||||
if o.Email == "" {
|
||||
return errors.New("auth: email must be provided")
|
||||
}
|
||||
if len(o.PrivateKey) == 0 {
|
||||
return errors.New("auth: private key must be provided")
|
||||
}
|
||||
if o.TokenURL == "" {
|
||||
return errors.New("auth: token URL must be provided")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// New2LOTokenProvider returns a [TokenProvider] from the provided options.
|
||||
func New2LOTokenProvider(opts *Options2LO) (TokenProvider, error) {
|
||||
if err := opts.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tokenProvider2LO{opts: opts, Client: opts.client(), logger: internallog.New(opts.Logger)}, nil
|
||||
}
|
||||
|
||||
type tokenProvider2LO struct {
|
||||
opts *Options2LO
|
||||
Client *http.Client
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
func (tp tokenProvider2LO) Token(ctx context.Context) (*Token, error) {
|
||||
pk, err := internal.ParseKey(tp.opts.PrivateKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
claimSet := &jwt.Claims{
|
||||
Iss: tp.opts.Email,
|
||||
Scope: strings.Join(tp.opts.Scopes, " "),
|
||||
Aud: tp.opts.TokenURL,
|
||||
AdditionalClaims: tp.opts.PrivateClaims,
|
||||
Sub: tp.opts.Subject,
|
||||
}
|
||||
if t := tp.opts.Expires; t > 0 {
|
||||
claimSet.Exp = time.Now().Add(t).Unix()
|
||||
}
|
||||
if aud := tp.opts.Audience; aud != "" {
|
||||
claimSet.Aud = aud
|
||||
}
|
||||
h := *defaultHeader
|
||||
h.KeyID = tp.opts.PrivateKeyID
|
||||
payload, err := jwt.EncodeJWS(&h, claimSet, pk)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v := url.Values{}
|
||||
v.Set("grant_type", defaultGrantType)
|
||||
v.Set("assertion", payload)
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", tp.opts.TokenURL, strings.NewReader(v.Encode()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
tp.logger.DebugContext(ctx, "2LO token request", "request", internallog.HTTPRequest(req, []byte(v.Encode())))
|
||||
resp, body, err := internal.DoRequest(tp.Client, req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("auth: cannot fetch token: %w", err)
|
||||
}
|
||||
tp.logger.DebugContext(ctx, "2LO token response", "response", internallog.HTTPResponse(resp, body))
|
||||
if c := resp.StatusCode; c < http.StatusOK || c >= http.StatusMultipleChoices {
|
||||
return nil, &Error{
|
||||
Response: resp,
|
||||
Body: body,
|
||||
}
|
||||
}
|
||||
// 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"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &tokenRes); err != nil {
|
||||
return nil, fmt.Errorf("auth: cannot fetch token: %w", err)
|
||||
}
|
||||
token := &Token{
|
||||
Value: tokenRes.AccessToken,
|
||||
Type: tokenRes.TokenType,
|
||||
}
|
||||
token.Metadata = make(map[string]interface{})
|
||||
json.Unmarshal(body, &token.Metadata) // no error checks for optional fields
|
||||
|
||||
if secs := tokenRes.ExpiresIn; secs > 0 {
|
||||
token.Expiry = time.Now().Add(time.Duration(secs) * time.Second)
|
||||
}
|
||||
if v := tokenRes.IDToken; v != "" {
|
||||
// decode returned id token to get expiry
|
||||
claimSet, err := jwt.DecodeJWS(v)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("auth: error decoding JWT token: %w", err)
|
||||
}
|
||||
token.Expiry = time.Unix(claimSet.Exp, 0)
|
||||
}
|
||||
if tp.opts.UseIDToken {
|
||||
if tokenRes.IDToken == "" {
|
||||
return nil, fmt.Errorf("auth: response doesn't have JWT token")
|
||||
}
|
||||
token.Value = tokenRes.IDToken
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
Reference in New Issue
Block a user