You've already forked oauth2-proxy
mirror of
https://github.com/oauth2-proxy/oauth2-proxy.git
synced 2025-07-13 01:40:48 +02:00
feature: static public keys file support for oidc provider
Co-authored-by: Jan Larwig <jan@larwig.com> Co-authored-by: JJ Łakis <jacek.lakis@checkatrade.com>
This commit is contained in:
@ -2,11 +2,14 @@ package oidc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests"
|
||||
k8serrors "k8s.io/apimachinery/pkg/util/errors"
|
||||
)
|
||||
|
||||
@ -38,6 +41,10 @@ type ProviderVerifierOptions struct {
|
||||
// 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
|
||||
|
||||
@ -59,8 +66,12 @@ func (p ProviderVerifierOptions) validate() error {
|
||||
errs = append(errs, errors.New("missing required setting: issuer-url"))
|
||||
}
|
||||
|
||||
if p.SkipDiscovery && p.JWKsURL == "" {
|
||||
errs = append(errs, errors.New("missing required setting: jwks-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 {
|
||||
@ -91,12 +102,12 @@ func (p ProviderVerifierOptions) toOIDCConfig() *oidc.Config {
|
||||
// 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: %v", err)
|
||||
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: %v", err)
|
||||
return nil, fmt.Errorf("could not get verifier builder: %w", err)
|
||||
}
|
||||
verifier := NewVerifier(verifierBuilder(opts.toOIDCConfig()), opts.toVerificationOptions())
|
||||
|
||||
@ -117,22 +128,78 @@ 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(ctx, opts.IssuerURL, opts.JWKsURL, opts.SupportedSigningAlgs), nil, nil
|
||||
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: %v", err)
|
||||
return nil, nil, fmt.Errorf("error while discovery OIDC configuration: %w", err)
|
||||
}
|
||||
verifierBuilder := newVerifierBuilder(ctx, opts.IssuerURL, provider.Endpoints().JWKsURL, provider.SupportedSigningAlgs())
|
||||
return verifierBuilder, provider, nil
|
||||
|
||||
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(ctx context.Context, issuerURL, jwksURL string, supportedSigningAlgs []string) verifierBuilder {
|
||||
ctx = oidc.ClientContext(ctx, requests.DefaultHTTPClient)
|
||||
keySet := oidc.NewRemoteKeySet(ctx, jwksURL)
|
||||
func newVerifierBuilder(issuerURL string, keySet oidc.KeySet, supportedSigningAlgs []string) verifierBuilder {
|
||||
return func(oidcConfig *oidc.Config) *oidc.IDTokenVerifier {
|
||||
if len(supportedSigningAlgs) > 0 {
|
||||
oidcConfig.SupportedSigningAlgs = supportedSigningAlgs
|
||||
|
@ -2,6 +2,8 @@ package oidc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
@ -10,6 +12,37 @@ import (
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var tempDir string
|
||||
var invalidPublicKeyFilePath string
|
||||
var validPublicKeyFilePath string
|
||||
|
||||
var _ = BeforeSuite(func() {
|
||||
var err error
|
||||
|
||||
// Create a temporary directory and public key file
|
||||
tempDir, err = os.MkdirTemp("/tmp", "provider-verifier-test")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
invalidPublicKeyFilePath = filepath.Join(tempDir, "invalid.key")
|
||||
validPublicKeyFilePath = filepath.Join(tempDir, "valid.key")
|
||||
|
||||
invalidKeyContents := []byte(`-----BEGIN INVALID KEY-----
|
||||
ThisIsNotAValidKey
|
||||
-----END INVALID KEY-----`)
|
||||
Expect(os.WriteFile(invalidPublicKeyFilePath, invalidKeyContents, 0644)).To(Succeed())
|
||||
|
||||
validKeyContents := []byte(`-----BEGIN PUBLIC KEY-----
|
||||
MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALBJK+8qU+aQu2bHxJ8E95AIu2NINztM
|
||||
NmX9R2zI9xlXN8wGQG8kWLYoRLbyiZwY9kdzOBGvYci64wHIjtFswHcCAwEAAQ==
|
||||
-----END PUBLIC KEY-----`)
|
||||
Expect(os.WriteFile(validPublicKeyFilePath, validKeyContents, 0644)).To(Succeed())
|
||||
})
|
||||
|
||||
var _ = AfterSuite(func() {
|
||||
// Clean up temporary directory
|
||||
Expect(os.RemoveAll(tempDir)).To(Succeed())
|
||||
})
|
||||
|
||||
var _ = Describe("ProviderVerifier", func() {
|
||||
var m *mockoidc.MockOIDC
|
||||
|
||||
@ -77,14 +110,42 @@ var _ = Describe("ProviderVerifier", func() {
|
||||
p.SkipDiscovery = true
|
||||
p.JWKsURL = ""
|
||||
},
|
||||
expectedError: "invalid provider verifier options: missing required setting: jwks-url",
|
||||
expectedError: "invalid provider verifier options: missing required setting: jwks-url or public-key-files",
|
||||
}),
|
||||
Entry("should be succesfful when skipping discovery with the JWKs URL specified", &newProviderVerifierTableInput{
|
||||
Entry("with skip discovery, the JWKs URL not empty and len(PublicKeyFiles) is greater than 0", &newProviderVerifierTableInput{
|
||||
modifyOpts: func(p *ProviderVerifierOptions) {
|
||||
p.SkipDiscovery = true
|
||||
p.JWKsURL = "notEmpty"
|
||||
p.PublicKeyFiles = []string{"notEmpty"}
|
||||
},
|
||||
expectedError: "invalid provider verifier options: mutually exclusive settings: jwks-url and public-key-files",
|
||||
}),
|
||||
Entry("should be successful when skipping discovery with the JWKs URL specified", &newProviderVerifierTableInput{
|
||||
modifyOpts: func(p *ProviderVerifierOptions) {
|
||||
p.SkipDiscovery = true
|
||||
p.JWKsURL = m.JWKSEndpoint()
|
||||
},
|
||||
}),
|
||||
Entry("should pass when the key is valid", &newProviderVerifierTableInput{
|
||||
modifyOpts: func(p *ProviderVerifierOptions) {
|
||||
p.SkipDiscovery = true
|
||||
p.PublicKeyFiles = []string{validPublicKeyFilePath}
|
||||
},
|
||||
}),
|
||||
Entry("should fail when the key is invalid", &newProviderVerifierTableInput{
|
||||
modifyOpts: func(p *ProviderVerifierOptions) {
|
||||
p.SkipDiscovery = true
|
||||
p.PublicKeyFiles = []string{invalidPublicKeyFilePath}
|
||||
},
|
||||
expectedError: "could not get verifier builder: error while parsing public keys",
|
||||
}),
|
||||
Entry("should fail when the key file is not found", &newProviderVerifierTableInput{
|
||||
modifyOpts: func(p *ProviderVerifierOptions) {
|
||||
p.SkipDiscovery = true
|
||||
p.PublicKeyFiles = []string{"non-existing"}
|
||||
},
|
||||
expectedError: "could not get verifier builder: error while parsing public keys: failed to read file",
|
||||
}),
|
||||
)
|
||||
|
||||
type verifierTableInput struct {
|
||||
|
Reference in New Issue
Block a user