package oidc import ( "context" "crypto" "crypto/x509" "encoding/pem" "errors" "fmt" "os" "github.com/coreos/go-oidc/v3/oidc" k8serrors "k8s.io/apimachinery/pkg/util/errors" ) // ProviderVerifier represents the OIDC discovery and verification process type ProviderVerifier interface { DiscoveryEnabled() bool Provider() DiscoveryProvider Verifier() IDTokenVerifier } // ProviderVerifierOptions allows you to configure a ProviderVerifier type ProviderVerifierOptions struct { // AudienceClaim allows to define any claim that is verified against the client id // By default `aud` claim is used for verification. AudienceClaims []string // ClientID is the OAuth Client ID that is defined in the provider ClientID string // ExtraAudiences is a list of additional audiences that are allowed // to pass verification in addition to the client id. ExtraAudiences []string // IssuerURL is the OpenID Connect issuer URL // eg: https://accounts.google.com IssuerURL string // JWKsURL is the OpenID Connect JWKS URL // eg: https://www.googleapis.com/oauth2/v3/certs JWKsURL string // PublicKeyFiles is a list of paths pointing to public key files in PEM format to use // for verifying JWT tokens PublicKeyFiles []string // SkipDiscovery allows to skip OIDC discovery and use manually supplied Endpoints SkipDiscovery bool // SkipIssuerVerification skips verification of ID token issuers. // When false, ID Token Issuers must match the OIDC discovery URL. SkipIssuerVerification bool // SupportedSigningAlgs is the list of signature algorithms supported by the // provider. SupportedSigningAlgs []string } // validate checks that the required options are present before attempting to create // the ProviderVerifier. func (p ProviderVerifierOptions) validate() error { var errs []error if p.IssuerURL == "" { errs = append(errs, errors.New("missing required setting: issuer-url")) } if p.SkipDiscovery && p.JWKsURL == "" && len(p.PublicKeyFiles) == 0 { errs = append(errs, errors.New("missing required setting: jwks-url or public-key-files")) } if p.JWKsURL != "" && len(p.PublicKeyFiles) > 0 { errs = append(errs, errors.New("mutually exclusive settings: jwks-url and public-key-files")) } if len(errs) > 0 { return k8serrors.NewAggregate(errs) } return nil } // toVerificationOptions returns an IDTokenVerificationOptions based on the configured options. func (p ProviderVerifierOptions) toVerificationOptions() IDTokenVerificationOptions { return IDTokenVerificationOptions{ AudienceClaims: p.AudienceClaims, ClientID: p.ClientID, ExtraAudiences: p.ExtraAudiences, } } // toOIDCConfig returns an oidc.Config based on the configured options. func (p ProviderVerifierOptions) toOIDCConfig() *oidc.Config { return &oidc.Config{ ClientID: p.ClientID, SkipIssuerCheck: p.SkipIssuerVerification, SkipClientIDCheck: true, SupportedSigningAlgs: p.SupportedSigningAlgs, } } // NewProviderVerifier constructs a ProviderVerifier from the options given. func NewProviderVerifier(ctx context.Context, opts ProviderVerifierOptions) (ProviderVerifier, error) { if err := opts.validate(); err != nil { return nil, fmt.Errorf("invalid provider verifier options: %w", err) } verifierBuilder, provider, err := getVerifierBuilder(ctx, opts) if err != nil { return nil, fmt.Errorf("could not get verifier builder: %w", err) } verifier := NewVerifier(verifierBuilder(opts.toOIDCConfig()), opts.toVerificationOptions()) if provider == nil { // To avoid the possibility of nil pointers, always return an empty provider if discovery didn't occur. // Users are expected to check whether discovery was enabled before using the provider. provider = &discoveryProvider{} } return &providerVerifier{ discoveryEnabled: !opts.SkipDiscovery, provider: provider, verifier: verifier, }, nil } type verifierBuilder func(*oidc.Config) *oidc.IDTokenVerifier func getVerifierBuilder(ctx context.Context, opts ProviderVerifierOptions) (verifierBuilder, DiscoveryProvider, error) { if opts.SkipDiscovery { var keySet oidc.KeySet var err error if opts.JWKsURL != "" { keySet = oidc.NewRemoteKeySet(ctx, opts.JWKsURL) } else { keySet, err = newKeySetFromStatic(opts.PublicKeyFiles) if err != nil { return nil, nil, fmt.Errorf("error while parsing public keys: %w", err) } } // Instead of discovering the JWKs URL, it needs to be specified in the opts already return newVerifierBuilder( opts.IssuerURL, keySet, opts.SupportedSigningAlgs, ), nil, nil } provider, err := NewProvider(ctx, opts.IssuerURL, opts.SkipIssuerVerification) if err != nil { return nil, nil, fmt.Errorf("error while discovery OIDC configuration: %w", err) } return newVerifierBuilder( opts.IssuerURL, oidc.NewRemoteKeySet(ctx, provider.Endpoints().JWKsURL), provider.SupportedSigningAlgs(), ), provider, nil } // GetPublicKeyFromBytes parses a PEM-encoded public key from a byte array // and returns a crypto.PublicKey object. func getPublicKeyFromBytes(bytes []byte) (crypto.PublicKey, error) { block, _ := pem.Decode(bytes) if block == nil { return nil, errors.New("failed to parse PEM block containing the public key") } publicKey, err := x509.ParsePKIXPublicKey(block.Bytes) if err != nil { return nil, fmt.Errorf("failed to parse public key: %w", err) } cryptoPublicKey, ok := publicKey.(crypto.PublicKey) if !ok { return nil, errors.New("failed to cast public key to crypto.PublicKey") } return cryptoPublicKey, nil } // newKeySetFromStatic create a StaticKeySet from a set of files func newKeySetFromStatic(keys []string) (*oidc.StaticKeySet, error) { keySet := []crypto.PublicKey{} for _, keyFile := range keys { bytes, err := os.ReadFile(keyFile) if err != nil { return nil, fmt.Errorf("failed to read file: %w", err) } publicKey, err := getPublicKeyFromBytes(bytes) if err != nil { return nil, fmt.Errorf("failed to create keys: %w", err) } keySet = append(keySet, publicKey) } return &oidc.StaticKeySet{PublicKeys: keySet}, nil } // newVerifierBuilder returns a function to create a IDToken verifier from an OIDC config. func newVerifierBuilder(issuerURL string, keySet oidc.KeySet, supportedSigningAlgs []string) verifierBuilder { return func(oidcConfig *oidc.Config) *oidc.IDTokenVerifier { if len(supportedSigningAlgs) > 0 { oidcConfig.SupportedSigningAlgs = supportedSigningAlgs } return oidc.NewVerifier(issuerURL, keySet, oidcConfig) } } // providerVerifier is an implementation of the ProviderVerifier interface type providerVerifier struct { discoveryEnabled bool provider DiscoveryProvider verifier IDTokenVerifier } // DiscoveryEnabled returns whether the provider verifier was constructed // using the OIDC discovery process or whether it was manually discovered. func (p *providerVerifier) DiscoveryEnabled() bool { return p.discoveryEnabled } // Provider returns the OIDC discovery provider func (p *providerVerifier) Provider() DiscoveryProvider { return p.provider } // Verifier returns the ID token verifier func (p *providerVerifier) Verifier() IDTokenVerifier { return p.verifier }