Add ProviderVerifier to providers/oidc
This commit is contained in:
parent
3bb9621f5d
commit
e3678aaaff
155
pkg/providers/oidc/provider_verifier.go
Normal file
155
pkg/providers/oidc/provider_verifier.go
Normal file
@ -0,0 +1,155 @@
|
||||
package oidc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"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
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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 == "" {
|
||||
errs = append(errs, errors.New("missing required setting: jwks-url"))
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
verifierBuilder, provider, err := getVerifierBuilder(ctx, opts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get verifier builder: %v", 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 {
|
||||
// Instead of discovering the JWKs URK, it needs to be specified in the opts already
|
||||
return newVerifierBuilder(ctx, opts.IssuerURL, opts.JWKsURL), 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)
|
||||
}
|
||||
verifierBuilder := newVerifierBuilder(ctx, opts.IssuerURL, provider.Endpoints().JWKsURL)
|
||||
return verifierBuilder, provider, nil
|
||||
}
|
||||
|
||||
// newVerifierBuilder returns a function to create a IDToken verifier from an OIDC config.
|
||||
func newVerifierBuilder(ctx context.Context, issuerURL, jwksURL string) verifierBuilder {
|
||||
keySet := oidc.NewRemoteKeySet(ctx, jwksURL)
|
||||
return func(oidcConfig *oidc.Config) *oidc.IDTokenVerifier {
|
||||
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
|
||||
}
|
173
pkg/providers/oidc/provider_verifier_test.go
Normal file
173
pkg/providers/oidc/provider_verifier_test.go
Normal file
@ -0,0 +1,173 @@
|
||||
package oidc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/oauth2-proxy/mockoidc"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/ginkgo/extensions/table"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("ProviderVerifier", func() {
|
||||
var m *mockoidc.MockOIDC
|
||||
|
||||
BeforeEach(func() {
|
||||
var err error
|
||||
m, err = mockoidc.Run()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
Expect(m.Shutdown()).To(Succeed())
|
||||
})
|
||||
|
||||
type newProviderVerifierTableInput struct {
|
||||
modifyOpts func(*ProviderVerifierOptions)
|
||||
expectedError string
|
||||
}
|
||||
|
||||
DescribeTable("when constructing the provider verifier", func(in *newProviderVerifierTableInput) {
|
||||
opts := ProviderVerifierOptions{
|
||||
AudienceClaims: []string{"aud"},
|
||||
ClientID: m.Config().ClientID,
|
||||
ExtraAudiences: []string{},
|
||||
IssuerURL: m.Issuer(),
|
||||
}
|
||||
if in.modifyOpts != nil {
|
||||
in.modifyOpts(&opts)
|
||||
}
|
||||
|
||||
pv, err := NewProviderVerifier(context.Background(), opts)
|
||||
if in.expectedError != "" {
|
||||
Expect(err).To(MatchError(HavePrefix(in.expectedError)))
|
||||
return
|
||||
}
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
Expect(pv.DiscoveryEnabled()).ToNot(Equal(opts.SkipDiscovery), "DiscoveryEnabled should be the reverse of skip discovery")
|
||||
Expect(pv.Provider()).ToNot(BeNil())
|
||||
|
||||
if pv.DiscoveryEnabled() {
|
||||
endpoints := pv.Provider().Endpoints()
|
||||
Expect(endpoints.AuthURL).To(Equal(m.AuthorizationEndpoint()))
|
||||
Expect(endpoints.TokenURL).To(Equal(m.TokenEndpoint()))
|
||||
Expect(endpoints.JWKsURL).To(Equal(m.JWKSEndpoint()))
|
||||
Expect(endpoints.UserInfoURL).To(Equal(m.UserinfoEndpoint()))
|
||||
}
|
||||
},
|
||||
Entry("should be succesfful when discovering the OIDC provider", &newProviderVerifierTableInput{
|
||||
modifyOpts: func(_ *ProviderVerifierOptions) {},
|
||||
}),
|
||||
Entry("when the issuer URL is missing", &newProviderVerifierTableInput{
|
||||
modifyOpts: func(p *ProviderVerifierOptions) {
|
||||
p.IssuerURL = ""
|
||||
},
|
||||
expectedError: "invalid provider verifier options: missing required setting: issuer-url",
|
||||
}),
|
||||
Entry("when the issuer URL is invalid", &newProviderVerifierTableInput{
|
||||
modifyOpts: func(p *ProviderVerifierOptions) {
|
||||
p.IssuerURL = "invalid"
|
||||
},
|
||||
expectedError: "could not get verifier builder: error while discovery OIDC configuration: failed to discover OIDC configuration: error performing request: Get \"invalid/.well-known/openid-configuration\": unsupported protocol scheme \"\"",
|
||||
}),
|
||||
Entry("with skip discovery and the JWKs URL is missing", &newProviderVerifierTableInput{
|
||||
modifyOpts: func(p *ProviderVerifierOptions) {
|
||||
p.SkipDiscovery = true
|
||||
p.JWKsURL = ""
|
||||
},
|
||||
expectedError: "invalid provider verifier options: missing required setting: jwks-url",
|
||||
}),
|
||||
Entry("should be succesfful when skipping discovery with the JWKs URL specified", &newProviderVerifierTableInput{
|
||||
modifyOpts: func(p *ProviderVerifierOptions) {
|
||||
p.SkipDiscovery = true
|
||||
p.JWKsURL = m.JWKSEndpoint()
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
type verifierTableInput struct {
|
||||
modifyOpts func(*ProviderVerifierOptions)
|
||||
modifyClaims func(*jwt.StandardClaims)
|
||||
expectedError string
|
||||
}
|
||||
|
||||
DescribeTable("when constructing the provider verifier", func(in *verifierTableInput) {
|
||||
opts := ProviderVerifierOptions{
|
||||
AudienceClaims: []string{"aud"},
|
||||
ClientID: m.Config().ClientID,
|
||||
ExtraAudiences: []string{},
|
||||
IssuerURL: m.Issuer(),
|
||||
}
|
||||
if in.modifyOpts != nil {
|
||||
in.modifyOpts(&opts)
|
||||
}
|
||||
|
||||
pv, err := NewProviderVerifier(context.Background(), opts)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
now := time.Now()
|
||||
claims := jwt.StandardClaims{
|
||||
Audience: m.Config().ClientID,
|
||||
Issuer: m.Issuer(),
|
||||
ExpiresAt: now.Add(1 * time.Hour).Unix(),
|
||||
IssuedAt: now.Unix(),
|
||||
Subject: "user",
|
||||
}
|
||||
if in.modifyClaims != nil {
|
||||
in.modifyClaims(&claims)
|
||||
}
|
||||
|
||||
rawIDToken, err := m.Keypair.SignJWT(claims)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
idToken, err := pv.Verifier().Verify(context.Background(), rawIDToken)
|
||||
if in.expectedError != "" {
|
||||
Expect(err).To(MatchError(HavePrefix(in.expectedError)))
|
||||
return
|
||||
}
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
Expect(idToken.Issuer).To(Equal(claims.Issuer))
|
||||
Expect(idToken.Audience).To(ConsistOf(claims.Audience))
|
||||
Expect(idToken.Subject).To(Equal(claims.Subject))
|
||||
},
|
||||
Entry("with the default opts and claims", &verifierTableInput{}),
|
||||
Entry("when the audience is mismatched", &verifierTableInput{
|
||||
modifyClaims: func(j *jwt.StandardClaims) {
|
||||
j.Audience = "OtherClient"
|
||||
},
|
||||
expectedError: "audience from claim aud with value [OtherClient] does not match with any of allowed audiences",
|
||||
}),
|
||||
Entry("when the audience is an extra audience", &verifierTableInput{
|
||||
modifyOpts: func(p *ProviderVerifierOptions) {
|
||||
p.ExtraAudiences = []string{"ExtraIssuer"}
|
||||
},
|
||||
modifyClaims: func(j *jwt.StandardClaims) {
|
||||
j.Audience = "ExtraIssuer"
|
||||
},
|
||||
}),
|
||||
Entry("when the issuer is mismatched", &verifierTableInput{
|
||||
modifyClaims: func(j *jwt.StandardClaims) {
|
||||
j.Issuer = "OtherIssuer"
|
||||
},
|
||||
expectedError: "failed to verify token: oidc: id token issued by a different provider",
|
||||
}),
|
||||
Entry("when the issuer is mismatched with skip issuer verification", &verifierTableInput{
|
||||
modifyOpts: func(p *ProviderVerifierOptions) {
|
||||
p.SkipIssuerVerification = true
|
||||
},
|
||||
modifyClaims: func(j *jwt.StandardClaims) {
|
||||
j.Issuer = "OtherIssuer"
|
||||
},
|
||||
}),
|
||||
Entry("when the token has expired", &verifierTableInput{
|
||||
modifyClaims: func(j *jwt.StandardClaims) {
|
||||
j.ExpiresAt = time.Now().Add(-1 * time.Hour).Unix()
|
||||
},
|
||||
expectedError: "failed to verify token: oidc: token is expired",
|
||||
}),
|
||||
)
|
||||
})
|
Loading…
Reference in New Issue
Block a user