Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion lib/gcp/token_validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,10 @@ func (id *IDTokenValidator) issuerURL() string {
// Validate validates an ID token.
func (id *IDTokenValidator) Validate(ctx context.Context, token string) (*IDTokenClaims, error) {
issuer := id.issuerURL()
claims, err := oidc.ValidateToken[*IDTokenClaims](ctx, issuer, audience, token)

// GCP does not set the authorized party to one of the listed audiences, so we must skip the optional azp check.
// TODO(Joerger): Use [rp.ValidateToken] once the authorized party check is made optional upstream, e.g. with an opt.
claims, err := oidc.ValidateTokenNoAuthorizedPartyCheck[*IDTokenClaims](ctx, issuer, audience, token)
if err != nil {
return nil, trace.Wrap(err, "validating token")
}
Expand Down
5 changes: 5 additions & 0 deletions lib/gcp/token_validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,11 @@ func (f *fakeIDP) issueToken(
NotBefore: jwt.NewNumericDate(issuedAt),
Expiry: jwt.NewNumericDate(expiry),
}
// GCP ID tokens have an "azp" claim that is the same as the "sub" claim.
// This azp is not included in the "aud" claim. It's a little murky whether
// this is spec-compliant. We should explicitly reproduce this in our tests
// since zealous oidc validation implementations may reject it.
claims.AuthorizedParty = sub
token, err := jwt.Signed(f.signer).
Claims(stdClaims).
Claims(claims).
Expand Down
104 changes: 104 additions & 0 deletions lib/oidc/token_validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,107 @@ func ValidateToken[C oidc.Claims](

return claims, nil
}

// ValidateTokenNoAuthorizedPartyCheck is a copy of [ValidateToken] with the
// Authorized Party check removed.
//
// As described in https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation,
// the authorized party check is optional. GCP does not set it to a one of the listed audiences
// as expected by this check.
//
// TODO(Joerger): Remove once upstream oidc library makes this check optional.
func ValidateTokenNoAuthorizedPartyCheck[C oidc.Claims](
ctx context.Context,
issuerURL string,
audience string,
token string,
) (C, error) {
timeoutCtx, cancel := context.WithTimeout(ctx, providerTimeout)
defer cancel()

var nilClaims C

// TODO(noah): It'd be nice to cache the OIDC discovery document fairly
// aggressively across join tokens since this isn't going to change very
// regularly.
dc, err := client.Discover(timeoutCtx, issuerURL, otelhttp.DefaultClient)
if err != nil {
return nilClaims, trace.Wrap(err, "discovering oidc document")
}

// TODO(noah): Ideally we'd cache the remote keyset across joins/join tokens
// based on the issuer.
ks := rp.NewRemoteKeySet(otelhttp.DefaultClient, dc.JwksURI)
verifier := rp.NewIDTokenVerifier(issuerURL, audience, ks)
// TODO(noah): It'd be ideal if we could extend the verifier to use an
// injected "now" time.

claims, err := verifyIDToken[C](timeoutCtx, token, verifier)
if err != nil {
return nilClaims, trace.Wrap(err, "verifying token")
}

return claims, nil
}

// verifyIDToken is a copy of [rp.verifyIDToken] with the Authorized Party check removed.
// TODO(Joerger): Remove once upstream oidc library makes this check optional.
func verifyIDToken[C oidc.Claims](ctx context.Context, token string, v *rp.IDTokenVerifier) (claims C, err error) {
ctx, span := client.Tracer.Start(ctx, "VerifyIDToken")
defer span.End()

var nilClaims C

decrypted, err := oidc.DecryptToken(token)
if err != nil {
return nilClaims, err
}
payload, err := oidc.ParseToken(decrypted, &claims)
if err != nil {
return nilClaims, err
}

if err := oidc.CheckSubject(claims); err != nil {
return nilClaims, err
}

if err = oidc.CheckIssuer(claims, v.Issuer); err != nil {
return nilClaims, err
}

if err = oidc.CheckAudience(claims, v.ClientID); err != nil {
return nilClaims, err
}

// Skip Authorized party check.
// if err = oidc.CheckAuthorizedParty(claims, v.ClientID); err != nil {
// return nilClaims, err
// }

if err = oidc.CheckSignature(ctx, decrypted, payload, claims, v.SupportedSignAlgs, v.KeySet); err != nil {
return nilClaims, err
}

if err = oidc.CheckExpiration(claims, v.Offset); err != nil {
return nilClaims, err
}

if err = oidc.CheckIssuedAt(claims, v.MaxAgeIAT, v.Offset); err != nil {
return nilClaims, err
}

if v.Nonce != nil {
if err = oidc.CheckNonce(claims, v.Nonce(ctx)); err != nil {
return nilClaims, err
}
}

if err = oidc.CheckAuthorizationContextClassReference(claims, v.ACR); err != nil {
return nilClaims, err
}

if err = oidc.CheckAuthTime(claims, v.MaxAge); err != nil {
return nilClaims, err
}
return claims, nil
}
Loading