diff --git a/go.mod b/go.mod index d1554c6f1..cba1839ee 100644 --- a/go.mod +++ b/go.mod @@ -11,9 +11,9 @@ require ( github.com/aws/aws-sdk-go-v2/service/lambda v1.88.5 github.com/cespare/xxhash/v2 v2.3.0 github.com/cockroachdb/pebble/v2 v2.1.5 - github.com/conductorone/dpop v0.2.6 - github.com/conductorone/dpop/integrations/dpop_grpc v0.2.4 - github.com/conductorone/dpop/integrations/dpop_oauth2 v0.2.5 + github.com/conductorone/dpop v0.3.0 + github.com/conductorone/dpop/integrations/dpop_grpc v0.3.0 + github.com/conductorone/dpop/integrations/dpop_oauth2 v0.3.0 github.com/deckarep/golang-set/v2 v2.9.0 github.com/doug-martin/goqu/v9 v9.19.0 github.com/envoyproxy/protoc-gen-validate v1.3.3 diff --git a/go.sum b/go.sum index 85e66f246..2acde0ef3 100644 --- a/go.sum +++ b/go.sum @@ -116,12 +116,12 @@ github.com/cockroachdb/swiss v0.0.0-20251224182025-b0f6560f979b h1:VXvSNzmr8hMj8 github.com/cockroachdb/swiss v0.0.0-20251224182025-b0f6560f979b/go.mod h1:yBRu/cnL4ks9bgy4vAASdjIW+/xMlFwuHKqtmh3GZQg= github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo= github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= -github.com/conductorone/dpop v0.2.6 h1:fakwai/Xm2b/fcDUwJN41WtcSI/2UhQOyRIVvnnrrNA= -github.com/conductorone/dpop v0.2.6/go.mod h1:gyo8TtzB9SCFCsjsICH4IaLZ7y64CcrDXMOPBwfq/3s= -github.com/conductorone/dpop/integrations/dpop_grpc v0.2.4 h1:lYxYi9/WTSL9sE96CO0QF2BY3kehs8dTTApI134TGCA= -github.com/conductorone/dpop/integrations/dpop_grpc v0.2.4/go.mod h1:LYNoUc1lkvozk9HBio+xI2w8YyfYy0v2cAJtIgrkj8o= -github.com/conductorone/dpop/integrations/dpop_oauth2 v0.2.5 h1:x/ZtD0YLNwlmoSv9SE4OBPJB9Hj2cpwyE5BAfia7aY8= -github.com/conductorone/dpop/integrations/dpop_oauth2 v0.2.5/go.mod h1:2eI0qv+XaEhoCw0GKFF1yH4X8Mp4KLVEVnQKRFEy4zs= +github.com/conductorone/dpop v0.3.0 h1:j5fZk0VqepGKYo+/NDikCOMsZcgs4HO4i0k56wRel5g= +github.com/conductorone/dpop v0.3.0/go.mod h1:gyo8TtzB9SCFCsjsICH4IaLZ7y64CcrDXMOPBwfq/3s= +github.com/conductorone/dpop/integrations/dpop_grpc v0.3.0 h1:R2uxHBtStgUn7cxbAnT3mAj6/e1akfP2Pj/hEPFB0D8= +github.com/conductorone/dpop/integrations/dpop_grpc v0.3.0/go.mod h1:f30gFNZHGkbPlufIDqzg5cr7llQQS0r5VQ+bJCnrDik= +github.com/conductorone/dpop/integrations/dpop_oauth2 v0.3.0 h1:g9OX0PW9DQyrGy/Tp2lesky/V1VH55cTY8z0Iq0ksOo= +github.com/conductorone/dpop/integrations/dpop_oauth2 v0.3.0/go.mod h1:RZqiSQdi4NXnoZtB3qG3XYN/6L4dHZy6jctEr5K5oPk= github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc= github.com/containerd/console v1.0.5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= diff --git a/vendor/github.com/conductorone/dpop/integrations/dpop_grpc/client_credential.go b/vendor/github.com/conductorone/dpop/integrations/dpop_grpc/client_credential.go index 4a8b4ce98..b0e229b95 100644 --- a/vendor/github.com/conductorone/dpop/integrations/dpop_grpc/client_credential.go +++ b/vendor/github.com/conductorone/dpop/integrations/dpop_grpc/client_credential.go @@ -5,9 +5,12 @@ import ( "errors" "net/url" + "github.com/conductorone/dpop/integrations/dpop_oauth2" "github.com/conductorone/dpop/pkg/dpop" "golang.org/x/oauth2" + "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials" + "google.golang.org/grpc/status" ) // DPoPCredentials implements the credentials.PerRPCCredentials interface @@ -54,7 +57,7 @@ func (d *DPoPCredentials) GetRequestMetadata(ctx context.Context, uri ...string) // Get the OAuth2 token token, err := d.tokenSource.Token() if err != nil { - return nil, err + return nil, tokenStatusError(err) } // Add access token to proof options @@ -78,3 +81,19 @@ func (d *DPoPCredentials) GetRequestMetadata(ctx context.Context, uri ...string) func (d *DPoPCredentials) RequireTransportSecurity() bool { return d.requireTLS } + +// tokenStatusError maps a token source failure onto a gRPC status so the +// transient/definitive classification survives the per-RPC credentials +// boundary — grpc-go flattens any non-status credentials error to +// codes.Unauthenticated. Transient failures (5xx responses, transport errors, +// timeouts; see dpop_oauth2.IsTransient) become codes.Unavailable so callers' +// retry policies treat them as retryable. Definitive failures (e.g. +// invalid_client, a disabled credential) become codes.Unauthenticated and +// fail fast. +func tokenStatusError(err error) error { + code := codes.Unauthenticated + if dpop_oauth2.IsTransient(err) { + code = codes.Unavailable + } + return status.Errorf(code, "dpop_grpc: failed to fetch token: %v", err) +} diff --git a/vendor/github.com/conductorone/dpop/integrations/dpop_oauth2/retry.go b/vendor/github.com/conductorone/dpop/integrations/dpop_oauth2/retry.go new file mode 100644 index 000000000..888be6623 --- /dev/null +++ b/vendor/github.com/conductorone/dpop/integrations/dpop_oauth2/retry.go @@ -0,0 +1,115 @@ +package dpop_oauth2 + +import ( + "context" + "errors" + "math/rand/v2" + "net/http" + "time" +) + +// Defaults chosen so a full retry cycle (attempts plus backoff) fits well +// within the 30 second budget Token() imposes on each call. +const ( + defaultRetryMaxAttempts = 3 + defaultRetryInitialDelay = 500 * time.Millisecond + defaultRetryMaxDelay = 2 * time.Second +) + +// RetryConfig controls how Token() retries transient token request failures: +// 5xx or 429 responses, transport-level errors, and timeouts. Every attempt +// re-runs the full token request with a freshly signed DPoP proof and client +// assertion; a proof's jti may be single-use, so an identical request is never +// replayed. Definitive OAuth protocol errors (e.g. invalid_client) are never +// retried. +type RetryConfig struct { + // MaxAttempts is the total number of attempts, including the first. + // Values below 1 are treated as 1 (retries disabled). + MaxAttempts int + // InitialDelay is the backoff before the first retry. It doubles on each + // subsequent retry, capped at MaxDelay, with jitter applied. + InitialDelay time.Duration + // MaxDelay caps the backoff between attempts. + MaxDelay time.Duration +} + +// DefaultRetryConfig returns the retry behavior used when no WithRetryConfig +// option is supplied. +func DefaultRetryConfig() RetryConfig { + return RetryConfig{ + MaxAttempts: defaultRetryMaxAttempts, + InitialDelay: defaultRetryInitialDelay, + MaxDelay: defaultRetryMaxDelay, + } +} + +func (c RetryConfig) normalized() RetryConfig { + if c.MaxAttempts < 1 { + c.MaxAttempts = 1 + } + if c.InitialDelay <= 0 { + c.InitialDelay = defaultRetryInitialDelay + } + if c.MaxDelay < c.InitialDelay { + c.MaxDelay = c.InitialDelay + } + return c +} + +// retryDelay computes the backoff preceding retry number `retry` (1-based): +// exponential doubling capped at MaxDelay, with equal jitter (half the delay +// is fixed, the other half randomized) so concurrent clients hitting the same +// outage don't retry in lockstep. +func (c RetryConfig) retryDelay(retry int) time.Duration { + delay := c.InitialDelay + for i := 1; i < retry; i++ { + delay *= 2 + if delay >= c.MaxDelay { + delay = c.MaxDelay + break + } + } + half := delay / 2 + return half + rand.N(half+1) +} + +// sleepBeforeRetry blocks for the backoff delay preceding the given retry. +// It returns false if ctx expires first. +func sleepBeforeRetry(ctx context.Context, cfg RetryConfig, retry int) bool { + timer := time.NewTimer(cfg.retryDelay(retry)) + defer timer.Stop() + select { + case <-ctx.Done(): + return false + case <-timer.C: + return true + } +} + +// IsTransient reports whether err is a token request failure that was +// classified as transient: the failure mode gives no indication the +// credential itself is bad, so retrying (with a fresh proof and assertion) +// may succeed. +func IsTransient(err error) bool { + return errors.Is(err, ErrTokenRequestTransient) +} + +// markTransient tags err as a transient token request failure. The result +// matches ErrTokenRequestTransient in addition to everything err already +// matched, and its message is unchanged. +func markTransient(err error) error { + return &transientError{error: err} +} + +type transientError struct{ error } + +func (e *transientError) Unwrap() []error { + return []error{e.error, ErrTokenRequestTransient} +} + +// isRetryableStatus reports whether an HTTP response status is worth +// retrying: any 5xx (upstream failure) or 429 (throttling). 4xx OAuth +// protocol rejections are definitive and must not be retried. +func isRetryableStatus(code int) bool { + return code >= 500 || code == http.StatusTooManyRequests +} diff --git a/vendor/github.com/conductorone/dpop/integrations/dpop_oauth2/token_client_assertion.go b/vendor/github.com/conductorone/dpop/integrations/dpop_oauth2/token_client_assertion.go index edb8588c4..2ebaaace5 100644 --- a/vendor/github.com/conductorone/dpop/integrations/dpop_oauth2/token_client_assertion.go +++ b/vendor/github.com/conductorone/dpop/integrations/dpop_oauth2/token_client_assertion.go @@ -12,6 +12,7 @@ import ( "github.com/go-jose/go-jose/v4" "github.com/go-jose/go-jose/v4/jwt" + "github.com/google/uuid" "golang.org/x/oauth2" "github.com/conductorone/dpop/pkg/dpop" @@ -30,6 +31,14 @@ var ( // ErrTokenRequestFailed indicates the token request failed ErrTokenRequestFailed = errors.New("dpop_oauth2: token request failed") + // ErrTokenRequestTransient classifies a token request failure as likely + // transient: a 5xx or 429 response, a transport-level error, or a + // timeout. Errors matching this sentinel always also match + // ErrTokenRequestFailed; definitive OAuth protocol rejections (e.g. + // invalid_client) match only ErrTokenRequestFailed. Use IsTransient to + // test for it. + ErrTokenRequestTransient = errors.New("dpop_oauth2: transient token request failure") + // ErrProofCreationFailed indicates failure to create or sign DPoP proof ErrProofCreationFailed = errors.New("dpop_oauth2: failed to create or sign DPoP proof") ) @@ -88,6 +97,7 @@ type tokenSourceOptions struct { proofOptions []dpop.ProofOption nonceStore *NonceStore requestOptions []TokenRequestOption + retry RetryConfig } // WithBaseContext sets a custom base context for the token source @@ -125,6 +135,15 @@ func WithRequestOption(opt TokenRequestOption) TokenSourceOption { } } +// WithRetryConfig overrides how transient token request failures are retried. +// See RetryConfig for field semantics; set MaxAttempts to 1 to disable +// retries entirely. +func WithRetryConfig(cfg RetryConfig) TokenSourceOption { + return func(opts *tokenSourceOptions) { + opts.retry = cfg + } +} + func NewTokenSource(proofer *dpop.Proofer, tokenURL *url.URL, clientID string, clientSecret *jose.JSONWebKey, opts ...TokenSourceOption) (*tokenSource, error) { if proofer == nil { return nil, fmt.Errorf("%w: dpop-proofer", ErrMissingRequiredField) @@ -145,6 +164,7 @@ func NewTokenSource(proofer *dpop.Proofer, tokenURL *url.URL, clientID string, c options := &tokenSourceOptions{ baseCtx: context.Background(), httpClient: http.DefaultClient, + retry: DefaultRetryConfig(), } for _, opt := range opts { @@ -161,6 +181,7 @@ func NewTokenSource(proofer *dpop.Proofer, tokenURL *url.URL, clientID string, c requestOptions: options.requestOptions, proofOptions: options.proofOptions, nonceStore: options.nonceStore, + retry: options.retry.normalized(), }, nil } @@ -174,15 +195,66 @@ type tokenSource struct { requestOptions []TokenRequestOption proofOptions []dpop.ProofOption nonceStore *NonceStore + retry RetryConfig } func (c *tokenSource) Token() (*oauth2.Token, error) { ctx, done := context.WithTimeout(c.baseCtx, time.Second*30) defer done() - return c.tryToken(ctx, true) + + // Transient failures (5xx/429, transport errors, timeouts) are retried + // with capped exponential backoff + jitter. The retry re-enters tryToken, + // so every attempt signs a fresh DPoP proof and client assertion — both + // carry unique jtis, so an identical request is never replayed. + // Definitive failures (OAuth protocol rejections) return immediately. + // + // A nonce learned from a use_dpop_nonce challenge is carried across + // attempts so a bare consumer (no NonceStore) isn't re-challenged on + // every retry. + var lastErr error + retryNonce := "" + for attempt := 0; attempt < c.retry.MaxAttempts; attempt++ { + if attempt > 0 { + if !sleepBeforeRetry(ctx, c.retry, attempt) { + // The context died mid-backoff. A deadline expiry (the 30s + // Token() budget) is a timeout: surface the last transient + // failure so callers can still classify it. A caller cancel + // is not a timeout — strip the transient classification so + // nothing retries abandoned work. + if errors.Is(ctx.Err(), context.Canceled) { + // context.Cause preserves a WithCancelCause cause in the + // chain; for a plain cancel it is context.Canceled. + return nil, fmt.Errorf("%w: %w during retry backoff (last error: %v)", ErrTokenRequestFailed, context.Cause(ctx), lastErr) + } + break + } + } + + token, nonce, err := c.tryToken(ctx, true, retryNonce) + if err == nil { + return token, nil + } + if nonce != "" { + retryNonce = nonce + } + lastErr = err + if !IsTransient(err) { + return nil, err + } + } + return nil, lastErr } -func (c *tokenSource) tryToken(ctx context.Context, firstAttempt bool) (*oauth2.Token, error) { +// tryToken performs a single token request. retryNonce, when non-empty, is the +// nonce returned by a prior use_dpop_nonce challenge and is attached to this +// attempt's proof regardless of whether a NonceStore is configured. This is +// what makes a bare consumer (no NonceStore) nonce-aware: the challenge/retry +// is self-contained within a single Token() call. +// +// The second return value is the nonce in effect for this attempt (the +// carried retryNonce, a cached store nonce, or a newly challenged one), so +// the transient retry loop in Token() can carry it into the next attempt. +func (c *tokenSource) tryToken(ctx context.Context, firstAttempt bool, retryNonce string) (*oauth2.Token, string, error) { jsigner, err := jose.NewSigner( jose.SigningKey{ Algorithm: jose.EdDSA, @@ -190,7 +262,7 @@ func (c *tokenSource) tryToken(ctx context.Context, firstAttempt bool) (*oauth2. }, nil) if err != nil { - return nil, fmt.Errorf("%w: failed to create signer: %v", ErrProofCreationFailed, err) + return nil, retryNonce, fmt.Errorf("%w: failed to create signer: %v", ErrProofCreationFailed, err) } // Our token host may include a port, but the audience never expects a port @@ -198,6 +270,11 @@ func (c *tokenSource) tryToken(ctx context.Context, firstAttempt bool) (*oauth2. now := time.Now() claims := &jwt.Claims{ + // A unique jti makes every signed assertion distinct. Without it, + // second-precision timestamps plus deterministic Ed25519 signatures + // would make fast retries re-send a byte-identical assertion, which a + // server enforcing RFC 7523 single-use may reject. + ID: uuid.New().String(), Issuer: c.clientID, Subject: c.clientID, Audience: jwt.Audience{aud}, @@ -220,13 +297,13 @@ func (c *tokenSource) tryToken(ctx context.Context, firstAttempt bool) (*oauth2. for _, opt := range c.requestOptions { err = opt(tr) if err != nil { - return nil, fmt.Errorf("%w: failed to modify request: %v", ErrTokenRequestFailed, err) + return nil, retryNonce, fmt.Errorf("%w: failed to modify request: %v", ErrTokenRequestFailed, err) } } marshalledClaims, err := tr.Marshaler(claims) if err != nil { - return nil, fmt.Errorf("%w: failed to marshal claims: %v", ErrTokenRequestFailed, err) + return nil, retryNonce, fmt.Errorf("%w: failed to marshal claims: %v", ErrTokenRequestFailed, err) } method := http.MethodPost @@ -234,34 +311,37 @@ func (c *tokenSource) tryToken(ctx context.Context, firstAttempt bool) (*oauth2. proofOpts := make([]dpop.ProofOption, 0, len(c.proofOptions)+2) proofOpts = append(proofOpts, c.proofOptions...) - // Add nonce if available from store - if c.nonceStore != nil { - nonce := c.nonceStore.GetNonce() - if nonce != "" { - proofOpts = append(proofOpts, dpop.WithStaticNonce(nonce)) - } + // Attach a nonce when available. Prefer the nonce from a use_dpop_nonce + // challenge on this same Token() call (retryNonce); otherwise fall back to + // a cached nonce from the configured store for cross-call reuse. + nonce := retryNonce + if nonce == "" && c.nonceStore != nil { + nonce = c.nonceStore.GetNonce() + } + if nonce != "" { + proofOpts = append(proofOpts, dpop.WithStaticNonce(nonce)) } dpopProof, err := c.proofer.CreateProof(ctx, method, c.tokenURL.String(), proofOpts...) if err != nil { - return nil, fmt.Errorf("%w: failed to create proof: %v", ErrProofCreationFailed, err) + return nil, nonce, fmt.Errorf("%w: failed to create proof: %v", ErrProofCreationFailed, err) } rv, err := jsigner.Sign(marshalledClaims) if err != nil { - return nil, fmt.Errorf("%w: failed to sign proof: %v", ErrProofCreationFailed, err) + return nil, nonce, fmt.Errorf("%w: failed to sign proof: %v", ErrProofCreationFailed, err) } s, err := rv.CompactSerialize() if err != nil { - return nil, fmt.Errorf("%w: failed to serialize proof: %v", ErrProofCreationFailed, err) + return nil, nonce, fmt.Errorf("%w: failed to serialize proof: %v", ErrProofCreationFailed, err) } tr.Body["client_assertion"] = []string{s} req, err := http.NewRequestWithContext(ctx, method, c.tokenURL.String(), strings.NewReader(tr.Body.Encode())) if err != nil { - return nil, fmt.Errorf("%w: failed to create request: %v", ErrTokenRequestFailed, err) + return nil, nonce, fmt.Errorf("%w: failed to create request: %v", ErrTokenRequestFailed, err) } req.Header.Set(dpop.HeaderName, dpopProof) @@ -271,7 +351,23 @@ func (c *tokenSource) tryToken(ctx context.Context, firstAttempt bool) (*oauth2. resp, err := c.httpClient.Do(req) if err != nil { - return nil, fmt.Errorf("%w: failed to execute request: %v", ErrTokenRequestFailed, err) + // The transport error stays in the chain (%w) so callers can inspect + // the underlying cause (context.Canceled, net errors, ...). + reqErr := fmt.Errorf("%w: failed to execute request: %w", ErrTokenRequestFailed, err) + // A canceled context means the caller abandoned the call — that is + // not a transport failure, so don't classify it as retryable. Check + // the context as well as the returned error: when the context was + // canceled via context.WithCancelCause, Do returns the cause, which + // need not match context.Canceled. + if errors.Is(err, context.Canceled) || errors.Is(ctx.Err(), context.Canceled) { + return nil, nonce, reqErr + } + // Everything else that fails before an HTTP response (connection + // resets, proxy errors, timeouts — including a deadline expiry, which + // is exactly the timed-out token POST class) never reached the + // authorization server's OAuth logic: it carries no verdict about the + // credential, so it is safe to classify as retryable. + return nil, nonce, markTransient(reqErr) } defer resp.Body.Close() @@ -282,58 +378,67 @@ func (c *tokenSource) tryToken(ctx context.Context, firstAttempt bool) (*oauth2. ErrorDescription string `json:"error_description"` } if err := json.NewDecoder(resp.Body).Decode(&errorResp); err != nil { - return nil, fmt.Errorf("%w: failed to decode error response: %v", ErrTokenRequestFailed, err) + return nil, nonce, fmt.Errorf("%w: failed to decode error response: %v", ErrTokenRequestFailed, err) } if errorResp.Error == "use_dpop_nonce" { // Get the new nonce from header - nonce := resp.Header.Get(dpop.NonceHeaderName) - if nonce == "" { - return nil, ErrNonceMissing + challengeNonce := resp.Header.Get(dpop.NonceHeaderName) + if challengeNonce == "" { + return nil, nonce, ErrNonceMissing } - // Store the nonce if we have a store + // Store the nonce for cross-call reuse if we have a store if c.nonceStore != nil { - c.nonceStore.SetNonce(nonce) + c.nonceStore.SetNonce(challengeNonce) } // Only retry once on first attempt if !firstAttempt { - return nil, fmt.Errorf("%w: token request failed after retry: %s - %s", ErrTokenRequestFailed, errorResp.Error, errorResp.ErrorDescription) + return nil, challengeNonce, fmt.Errorf("%w: token request failed after retry: %s - %s", ErrTokenRequestFailed, errorResp.Error, errorResp.ErrorDescription) } - // Try again with the new nonce - return c.tryToken(ctx, false) + // Retry with the challenged nonce. Passing it explicitly means the + // retry is nonce-aware even with no NonceStore configured. + return c.tryToken(ctx, false, challengeNonce) } - return nil, fmt.Errorf("%w: %s - %s", ErrTokenRequestFailed, errorResp.Error, errorResp.ErrorDescription) + return nil, nonce, fmt.Errorf("%w: %s - %s", ErrTokenRequestFailed, errorResp.Error, errorResp.ErrorDescription) + } + + if isRetryableStatus(resp.StatusCode) { + return nil, nonce, markTransient(fmt.Errorf("%w: unexpected status code: %s", ErrTokenRequestFailed, resp.Status)) } if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("%w: unexpected status code: %s", ErrTokenRequestFailed, resp.Status) + return nil, nonce, fmt.Errorf("%w: unexpected status code: %s", ErrTokenRequestFailed, resp.Status) } token := &oauth2.Token{} err = json.NewDecoder(resp.Body).Decode(token) if err != nil { - return nil, fmt.Errorf("%w: failed to decode token response: %v", ErrInvalidToken, err) + return nil, nonce, fmt.Errorf("%w: failed to decode token response: %v", ErrInvalidToken, err) } if token.AccessToken == "" { - return nil, fmt.Errorf("%w: empty access token", ErrInvalidToken) + return nil, nonce, fmt.Errorf("%w: empty access token", ErrInvalidToken) } if token.Expiry.IsZero() { token.Expiry = time.Now() if token.ExpiresIn > 0 { - token.Expiry = time.Now().Add(time.Duration(token.ExpiresIn-10) * time.Second) // 10 seconds before the token expires + expiresIn := token.ExpiresIn - 10 // 10 seconds before the token expires + if expiresIn < 0 { + expiresIn = 0 + } + token.Expiry = time.Now().Add(time.Duration(expiresIn) * time.Second) } } // Accept both DPoP and Bearer tokens // If we sent a DPoP proof but got a Bearer token, that means the AS doesn't support DPoP if !strings.EqualFold(token.TokenType, "DPoP") && !strings.EqualFold(token.TokenType, "Bearer") { - return nil, fmt.Errorf("%w: invalid token type: %s", ErrInvalidToken, token.TokenType) + return nil, nonce, fmt.Errorf("%w: invalid token type: %s", ErrInvalidToken, token.TokenType) } - return token, nil + return token, nonce, nil } diff --git a/vendor/modules.txt b/vendor/modules.txt index ac3ac554d..419a40d4d 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -347,13 +347,13 @@ github.com/cockroachdb/swiss # github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 ## explicit; go 1.19 github.com/cockroachdb/tokenbucket -# github.com/conductorone/dpop v0.2.6 +# github.com/conductorone/dpop v0.3.0 ## explicit; go 1.23.4 github.com/conductorone/dpop/pkg/dpop -# github.com/conductorone/dpop/integrations/dpop_grpc v0.2.4 +# github.com/conductorone/dpop/integrations/dpop_grpc v0.3.0 ## explicit; go 1.23.4 github.com/conductorone/dpop/integrations/dpop_grpc -# github.com/conductorone/dpop/integrations/dpop_oauth2 v0.2.5 +# github.com/conductorone/dpop/integrations/dpop_oauth2 v0.3.0 ## explicit; go 1.23.4 github.com/conductorone/dpop/integrations/dpop_oauth2 # github.com/containerd/console v1.0.5