2022-02-16 10:15:36 +00:00
|
|
|
package oidc
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2023-03-15 14:03:48 +01:00
|
|
|
"crypto"
|
|
|
|
"crypto/x509"
|
|
|
|
"encoding/pem"
|
2022-02-16 10:15:36 +00:00
|
|
|
"errors"
|
|
|
|
"fmt"
|
2023-03-15 14:03:48 +01:00
|
|
|
"os"
|
2022-02-16 10:15:36 +00:00
|
|
|
|
|
|
|
"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
|
|
|
|
|
2023-03-15 14:03:48 +01:00
|
|
|
// PublicKeyFiles is a list of paths pointing to public key files in PEM format to use
|
|
|
|
// for verifying JWT tokens
|
|
|
|
PublicKeyFiles []string
|
|
|
|
|
2022-02-16 10:15:36 +00:00
|
|
|
// 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
|
2022-05-29 13:48:09 +01:00
|
|
|
|
|
|
|
// SupportedSigningAlgs is the list of signature algorithms supported by the
|
|
|
|
// provider.
|
|
|
|
SupportedSigningAlgs []string
|
2022-02-16 10:15:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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"))
|
|
|
|
}
|
|
|
|
|
2023-03-15 14:03:48 +01:00
|
|
|
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"))
|
2022-02-16 10:15:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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{
|
2022-05-29 13:48:09 +01:00
|
|
|
ClientID: p.ClientID,
|
|
|
|
SkipIssuerCheck: p.SkipIssuerVerification,
|
|
|
|
SkipClientIDCheck: true,
|
|
|
|
SupportedSigningAlgs: p.SupportedSigningAlgs,
|
2022-02-16 10:15:36 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewProviderVerifier constructs a ProviderVerifier from the options given.
|
|
|
|
func NewProviderVerifier(ctx context.Context, opts ProviderVerifierOptions) (ProviderVerifier, error) {
|
|
|
|
if err := opts.validate(); err != nil {
|
2023-03-15 14:03:48 +01:00
|
|
|
return nil, fmt.Errorf("invalid provider verifier options: %w", err)
|
2022-02-16 10:15:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
verifierBuilder, provider, err := getVerifierBuilder(ctx, opts)
|
|
|
|
if err != nil {
|
2023-03-15 14:03:48 +01:00
|
|
|
return nil, fmt.Errorf("could not get verifier builder: %w", err)
|
2022-02-16 10:15:36 +00:00
|
|
|
}
|
|
|
|
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 {
|
2023-03-15 14:03:48 +01:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
2024-06-24 13:15:24 -07:00
|
|
|
// Instead of discovering the JWKs URL, it needs to be specified in the opts already
|
2023-03-15 14:03:48 +01:00
|
|
|
return newVerifierBuilder(
|
|
|
|
opts.IssuerURL,
|
|
|
|
keySet,
|
|
|
|
opts.SupportedSigningAlgs,
|
|
|
|
), nil, nil
|
2022-02-16 10:15:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
provider, err := NewProvider(ctx, opts.IssuerURL, opts.SkipIssuerVerification)
|
|
|
|
if err != nil {
|
2023-03-15 14:03:48 +01:00
|
|
|
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)
|
2022-02-16 10:15:36 +00:00
|
|
|
}
|
2023-03-15 14:03:48 +01:00
|
|
|
|
|
|
|
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
|
2022-02-16 10:15:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// newVerifierBuilder returns a function to create a IDToken verifier from an OIDC config.
|
2023-03-15 14:03:48 +01:00
|
|
|
func newVerifierBuilder(issuerURL string, keySet oidc.KeySet, supportedSigningAlgs []string) verifierBuilder {
|
2022-02-16 10:15:36 +00:00
|
|
|
return func(oidcConfig *oidc.Config) *oidc.IDTokenVerifier {
|
2022-05-29 13:48:09 +01:00
|
|
|
if len(supportedSigningAlgs) > 0 {
|
|
|
|
oidcConfig.SupportedSigningAlgs = supportedSigningAlgs
|
|
|
|
}
|
|
|
|
|
2022-02-16 10:15:36 +00:00
|
|
|
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
|
|
|
|
}
|