lib/verifier/verifier.go (148 lines of code) (raw):

// Copyright 2019 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 verifier provides a token verifier. package verifier import ( "context" "strings" "time" "github.com/go-jose/go-jose/v3/jwt" /* copybara-comment */ "google.golang.org/grpc/codes" /* copybara-comment */ "google.golang.org/grpc/status" /* copybara-comment */ "github.com/GoogleCloudPlatform/healthcare-federated-access-services/lib/errutil" /* copybara-comment: errutil */ "github.com/GoogleCloudPlatform/healthcare-federated-access-services/lib/ga4gh" /* copybara-comment: ga4gh */ ) // PassportVerifier verifies passport tokens. type PassportVerifier struct { tok *oidcJwtSigVerifier aud *passportAudienceVerifier } // Verify verifies signature, timestamp, issuer and audiences in passport token. func (s *PassportVerifier) Verify(ctx context.Context, token string) error { return verify(ctx, s.tok, s.aud, token, nil) } // VisaVerifier verifies visa tokens. type VisaVerifier struct { tok extractClaimsAndVerifyToken aud *visaAudienceVerifier } // Verify signature, timestamp, issuer, jku and audiences in visa token. func (s *VisaVerifier) Verify(ctx context.Context, token, jku string) error { if len(jku) > 0 { if _, ok := s.tok.(*jkuVisaSigVerifier); !ok { return errutil.WithErrorReason(errVerifierInvalidType, status.Errorf(codes.Internal, "extractClaimsAndVerifyToken type must be an oidc verifier")) } } else { if _, ok := s.tok.(*oidcJwtSigVerifier); !ok { return errutil.WithErrorReason(errVerifierInvalidType, status.Errorf(codes.Internal, "extractClaimsAndVerifyToken type must be an oidc verifier")) } } return verify(ctx, s.tok, s.aud, token, nil) } // JWTAccessTokenVerifier verifies jwt access tokens, used in lib/auth. type JWTAccessTokenVerifier struct { tok *oidcJwtSigVerifier aud *accessTokenAudienceVerifier } // Verify verifies signature, timestamp, issuer and audiences in access tok. func (s *JWTAccessTokenVerifier) Verify(ctx context.Context, token string, claims any, opt Option) error { return verify(ctx, s.tok, s.aud, token, claims, opt) } // UserinfoAccesssTokenVerifier verifies access tokens with userinfo endpoint, used in lib/auth. type UserinfoAccesssTokenVerifier struct { tok *oidcOpaqueUserinfoVerifier aud *accessTokenAudienceVerifier } // Verify verifies signature, timestamp, issuer and audiences of access token with userinfo. func (s *UserinfoAccesssTokenVerifier) Verify(ctx context.Context, token string, claims any, opt Option) error { return verify(ctx, s.tok, s.aud, token, claims, opt) } // NewVisaVerifier creates a visa token verifier. func NewVisaVerifier(ctx context.Context, issuer, jku, prefix string) (*VisaVerifier, error) { v := &VisaVerifier{ aud: &visaAudienceVerifier{prefix: prefix}, } if len(jku) > 0 { v.tok = newJkuVisaSigVerifier(ctx, issuer, jku) return v, nil } var err error v.tok, err = newOIDCSigVerifier(ctx, issuer) if err != nil { return nil, err } return v, nil } // NewPassportVerifier creates a passport token verifier. func NewPassportVerifier(ctx context.Context, issuer, clientID string) (*PassportVerifier, error) { tok, err := newOIDCSigVerifier(ctx, issuer) if err != nil { return nil, err } return &PassportVerifier{ tok: tok, aud: &passportAudienceVerifier{ clientID: clientID, }, }, nil } // AccessTokenVerifier verifies jwt access tokens or access token to userinfo, used in lib/auth. type AccessTokenVerifier interface { Verify(ctx context.Context, token string, claims any, opt Option) error } // NewAccessTokenVerifier creates a access tok verifier. func NewAccessTokenVerifier(ctx context.Context, issuer string, useUserinfoVerifier bool) (AccessTokenVerifier, error) { if useUserinfoVerifier { tok, err := newOIDCUserinfoVerifier(ctx, issuer) if err != nil { return nil, err } return &UserinfoAccesssTokenVerifier{ tok: tok, aud: &accessTokenAudienceVerifier{}, }, nil } tok, err := newOIDCSigVerifier(ctx, issuer) if err != nil { return nil, err } return &JWTAccessTokenVerifier{ tok: tok, aud: &accessTokenAudienceVerifier{}, }, nil } // extractClaimsAndVerifyToken is used to verify tokens. type extractClaimsAndVerifyToken interface { // PreviewClaimsBeforeVerification from the given tok, will also extracts to custom claim object if claims passed in. // Claims will be unsafe for jwt token, and claims will be safe if fetched from the userinfo endpoint. // This function need to be called before VerifySig(). PreviewClaimsBeforeVerification(ctx context.Context, token string, claims any) (*ga4gh.StdClaims, error) // VerifySig of the access tok, it will be empty if not jwt tok. VerifySig(ctx context.Context, token string) error // Issuer the wanted issuer of the tok. Issuer() string } // verify verifies the provided token. func verify(ctx context.Context, tokenVerifier extractClaimsAndVerifyToken, aud audienceVerifier, token string, claims any, opts ...Option) error { d, err := tokenVerifier.PreviewClaimsBeforeVerification(ctx, token, claims) if err != nil { return err } if len(d.Subject) == 0 { return errutil.WithErrorReason(errSubMissing, status.Errorf(codes.Unauthenticated, "Issuer in tok does not match issuer in tokenVerifier")) } if normalizeIssuer(d.Issuer) != normalizeIssuer(tokenVerifier.Issuer()) { return errutil.WithErrorReason(errIssuerNotMatch, status.Errorf(codes.Unauthenticated, "Issuer in tok does not match issuer in tokenVerifier")) } if err := aud.Verify(d, opts...); err != nil { return errutil.WithErrorReason(errInvalidAudience, status.Errorf(codes.Unauthenticated, "invalid aud claim: %v", err)) } now := time.Now().Unix() if now > d.ExpiresAt { return errutil.WithErrorReason(errExpired, status.Errorf(codes.Unauthenticated, "tok expired")) } if now < d.NotBefore { return errutil.WithErrorReason(errFutureToken, status.Errorf(codes.Unauthenticated, "future tok: tok is not valid yet")) } if now < d.IssuedAt { return errutil.WithErrorReason(errFutureToken, status.Errorf(codes.Unauthenticated, "future tok: tok used before issued")) } if err := tokenVerifier.VerifySig(ctx, token); err != nil { return errutil.WithErrorReason(errInvalidSignature, status.Errorf(codes.Unauthenticated, "%v", err)) } return nil } // normalizeIssuer ensure the issuer string does not have trailing slash. func normalizeIssuer(issuer string) string { return strings.TrimSuffix(issuer, "/") } // Option for verifies tokens. type Option interface { isOption() } // unsafeClaimsFromJWTToken extracts custom claims from jwt body. func unsafeClaimsFromJWTToken(token string, obj any) error { tok, err := jwt.ParseSigned(token) if err != nil { return errutil.WithErrorReason(errParseFailed, status.Errorf(codes.Unauthenticated, "ParseSigned() failed: %v", err)) } if err := tok.UnsafeClaimsWithoutVerification(obj); err != nil { return errutil.WithErrorReason(errParseFailed, status.Errorf(codes.Unauthenticated, "UnsafeClaimsWithoutVerification() failed: %v", err)) } return nil }