git.iko.soy/typeable/oauth2-proxy
on master help sign in
Add ProviderVerifier to providers/oidc
commite3678aaaff5ccf99518d5ca91291949662794845 authorJoel Speed <joel.speed@hotmail.co.uk> dateWed 16 Feb 2022, 10:15 (5 years ago) parent3bb9621
2 files changed+328
pkg/providers/oidc/provider_verifier.go+155 pkg/providers/oidc/provider_verifier_test.go+173
pkg/providers/oidc/provider_verifier.gonew file+155
@@ -0,0 +1,155 @@
1+package oidc
2+
3+import (
4+ "context"
5+ "errors"
6+ "fmt"
7+
8+ "github.com/coreos/go-oidc/v3/oidc"
9+ k8serrors "k8s.io/apimachinery/pkg/util/errors"
10+)
11+
12+// ProviderVerifier represents the OIDC discovery and verification process
13+type ProviderVerifier interface {
14+ DiscoveryEnabled() bool
15+ Provider() DiscoveryProvider
16+ Verifier() IDTokenVerifier
17+}
18+
19+// ProviderVerifierOptions allows you to configure a ProviderVerifier
20+type ProviderVerifierOptions struct {
21+ // AudienceClaim allows to define any claim that is verified against the client id
22+ // By default `aud` claim is used for verification.
23+ AudienceClaims []string
24+
25+ // ClientID is the OAuth Client ID that is defined in the provider
26+ ClientID string
27+
28+ // ExtraAudiences is a list of additional audiences that are allowed
29+ // to pass verification in addition to the client id.
30+ ExtraAudiences []string
31+
32+ // IssuerURL is the OpenID Connect issuer URL
33+ // eg: https://accounts.google.com
34+ IssuerURL string
35+
36+ // JWKsURL is the OpenID Connect JWKS URL
37+ // eg: https://www.googleapis.com/oauth2/v3/certs
38+ JWKsURL string
39+
40+ // SkipDiscovery allows to skip OIDC discovery and use manually supplied Endpoints
41+ SkipDiscovery bool
42+
43+ // SkipIssuerVerification skips verification of ID token issuers.
44+ // When false, ID Token Issuers must match the OIDC discovery URL.
45+ SkipIssuerVerification bool
46+}
47+
48+// validate checks that the required options are present before attempting to create
49+// the ProviderVerifier.
50+func (p ProviderVerifierOptions) validate() error {
51+ var errs []error
52+
53+ if p.IssuerURL == "" {
54+ errs = append(errs, errors.New("missing required setting: issuer-url"))
55+ }
56+
57+ if p.SkipDiscovery && p.JWKsURL == "" {
58+ errs = append(errs, errors.New("missing required setting: jwks-url"))
59+ }
60+
61+ if len(errs) > 0 {
62+ return k8serrors.NewAggregate(errs)
63+ }
64+ return nil
65+}
66+
67+// toVerificationOptions returns an IDTokenVerificationOptions based on the configured options.
68+func (p ProviderVerifierOptions) toVerificationOptions() IDTokenVerificationOptions {
69+ return IDTokenVerificationOptions{
70+ AudienceClaims: p.AudienceClaims,
71+ ClientID: p.ClientID,
72+ ExtraAudiences: p.ExtraAudiences,
73+ }
74+}
75+
76+// toOIDCConfig returns an oidc.Config based on the configured options.
77+func (p ProviderVerifierOptions) toOIDCConfig() *oidc.Config {
78+ return &oidc.Config{
79+ ClientID: p.ClientID,
80+ SkipIssuerCheck: p.SkipIssuerVerification,
81+ SkipClientIDCheck: true,
82+ }
83+}
84+
85+// NewProviderVerifier constructs a ProviderVerifier from the options given.
86+func NewProviderVerifier(ctx context.Context, opts ProviderVerifierOptions) (ProviderVerifier, error) {
87+ if err := opts.validate(); err != nil {
88+ return nil, fmt.Errorf("invalid provider verifier options: %v", err)
89+ }
90+
91+ verifierBuilder, provider, err := getVerifierBuilder(ctx, opts)
92+ if err != nil {
93+ return nil, fmt.Errorf("could not get verifier builder: %v", err)
94+ }
95+ verifier := NewVerifier(verifierBuilder(opts.toOIDCConfig()), opts.toVerificationOptions())
96+
97+ if provider == nil {
98+ // To avoid the possibility of nil pointers, always return an empty provider if discovery didn't occur.
99+ // Users are expected to check whether discovery was enabled before using the provider.
100+ provider = &discoveryProvider{}
101+ }
102+
103+ return &providerVerifier{
104+ discoveryEnabled: !opts.SkipDiscovery,
105+ provider: provider,
106+ verifier: verifier,
107+ }, nil
108+}
109+
110+type verifierBuilder func(*oidc.Config) *oidc.IDTokenVerifier
111+
112+func getVerifierBuilder(ctx context.Context, opts ProviderVerifierOptions) (verifierBuilder, DiscoveryProvider, error) {
113+ if opts.SkipDiscovery {
114+ // Instead of discovering the JWKs URK, it needs to be specified in the opts already
115+ return newVerifierBuilder(ctx, opts.IssuerURL, opts.JWKsURL), nil, nil
116+ }
117+
118+ provider, err := NewProvider(ctx, opts.IssuerURL, opts.SkipIssuerVerification)
119+ if err != nil {
120+ return nil, nil, fmt.Errorf("error while discovery OIDC configuration: %v", err)
121+ }
122+ verifierBuilder := newVerifierBuilder(ctx, opts.IssuerURL, provider.Endpoints().JWKsURL)
123+ return verifierBuilder, provider, nil
124+}
125+
126+// newVerifierBuilder returns a function to create a IDToken verifier from an OIDC config.
127+func newVerifierBuilder(ctx context.Context, issuerURL, jwksURL string) verifierBuilder {
128+ keySet := oidc.NewRemoteKeySet(ctx, jwksURL)
129+ return func(oidcConfig *oidc.Config) *oidc.IDTokenVerifier {
130+ return oidc.NewVerifier(issuerURL, keySet, oidcConfig)
131+ }
132+}
133+
134+// providerVerifier is an implementation of the ProviderVerifier interface
135+type providerVerifier struct {
136+ discoveryEnabled bool
137+ provider DiscoveryProvider
138+ verifier IDTokenVerifier
139+}
140+
141+// DiscoveryEnabled returns whether the provider verifier was constructed
142+// using the OIDC discovery process or whether it was manually discovered.
143+func (p *providerVerifier) DiscoveryEnabled() bool {
144+ return p.discoveryEnabled
145+}
146+
147+// Provider returns the OIDC discovery provider
148+func (p *providerVerifier) Provider() DiscoveryProvider {
149+ return p.provider
150+}
151+
152+// Verifier returns the ID token verifier
153+func (p *providerVerifier) Verifier() IDTokenVerifier {
154+ return p.verifier
155+}
pkg/providers/oidc/provider_verifier_test.gonew file+173
@@ -0,0 +1,173 @@
1+package oidc
2+
3+import (
4+ "context"
5+ "time"
6+
7+ "github.com/dgrijalva/jwt-go"
8+ "github.com/oauth2-proxy/mockoidc"
9+ . "github.com/onsi/ginkgo"
10+ . "github.com/onsi/ginkgo/extensions/table"
11+ . "github.com/onsi/gomega"
12+)
13+
14+var _ = Describe("ProviderVerifier", func() {
15+ var m *mockoidc.MockOIDC
16+
17+ BeforeEach(func() {
18+ var err error
19+ m, err = mockoidc.Run()
20+ Expect(err).ToNot(HaveOccurred())
21+ })
22+
23+ AfterEach(func() {
24+ Expect(m.Shutdown()).To(Succeed())
25+ })
26+
27+ type newProviderVerifierTableInput struct {
28+ modifyOpts func(*ProviderVerifierOptions)
29+ expectedError string
30+ }
31+
32+ DescribeTable("when constructing the provider verifier", func(in *newProviderVerifierTableInput) {
33+ opts := ProviderVerifierOptions{
34+ AudienceClaims: []string{"aud"},
35+ ClientID: m.Config().ClientID,
36+ ExtraAudiences: []string{},
37+ IssuerURL: m.Issuer(),
38+ }
39+ if in.modifyOpts != nil {
40+ in.modifyOpts(&opts)
41+ }
42+
43+ pv, err := NewProviderVerifier(context.Background(), opts)
44+ if in.expectedError != "" {
45+ Expect(err).To(MatchError(HavePrefix(in.expectedError)))
46+ return
47+ }
48+ Expect(err).ToNot(HaveOccurred())
49+
50+ Expect(pv.DiscoveryEnabled()).ToNot(Equal(opts.SkipDiscovery), "DiscoveryEnabled should be the reverse of skip discovery")
51+ Expect(pv.Provider()).ToNot(BeNil())
52+
53+ if pv.DiscoveryEnabled() {
54+ endpoints := pv.Provider().Endpoints()
55+ Expect(endpoints.AuthURL).To(Equal(m.AuthorizationEndpoint()))
56+ Expect(endpoints.TokenURL).To(Equal(m.TokenEndpoint()))
57+ Expect(endpoints.JWKsURL).To(Equal(m.JWKSEndpoint()))
58+ Expect(endpoints.UserInfoURL).To(Equal(m.UserinfoEndpoint()))
59+ }
60+ },
61+ Entry("should be succesfful when discovering the OIDC provider", &newProviderVerifierTableInput{
62+ modifyOpts: func(_ *ProviderVerifierOptions) {},
63+ }),
64+ Entry("when the issuer URL is missing", &newProviderVerifierTableInput{
65+ modifyOpts: func(p *ProviderVerifierOptions) {
66+ p.IssuerURL = ""
67+ },
68+ expectedError: "invalid provider verifier options: missing required setting: issuer-url",
69+ }),
70+ Entry("when the issuer URL is invalid", &newProviderVerifierTableInput{
71+ modifyOpts: func(p *ProviderVerifierOptions) {
72+ p.IssuerURL = "invalid"
73+ },
74+ 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 \"\"",
75+ }),
76+ Entry("with skip discovery and the JWKs URL is missing", &newProviderVerifierTableInput{
77+ modifyOpts: func(p *ProviderVerifierOptions) {
78+ p.SkipDiscovery = true
79+ p.JWKsURL = ""
80+ },
81+ expectedError: "invalid provider verifier options: missing required setting: jwks-url",
82+ }),
83+ Entry("should be succesfful when skipping discovery with the JWKs URL specified", &newProviderVerifierTableInput{
84+ modifyOpts: func(p *ProviderVerifierOptions) {
85+ p.SkipDiscovery = true
86+ p.JWKsURL = m.JWKSEndpoint()
87+ },
88+ }),
89+ )
90+
91+ type verifierTableInput struct {
92+ modifyOpts func(*ProviderVerifierOptions)
93+ modifyClaims func(*jwt.StandardClaims)
94+ expectedError string
95+ }
96+
97+ DescribeTable("when constructing the provider verifier", func(in *verifierTableInput) {
98+ opts := ProviderVerifierOptions{
99+ AudienceClaims: []string{"aud"},
100+ ClientID: m.Config().ClientID,
101+ ExtraAudiences: []string{},
102+ IssuerURL: m.Issuer(),
103+ }
104+ if in.modifyOpts != nil {
105+ in.modifyOpts(&opts)
106+ }
107+
108+ pv, err := NewProviderVerifier(context.Background(), opts)
109+ Expect(err).ToNot(HaveOccurred())
110+
111+ now := time.Now()
112+ claims := jwt.StandardClaims{
113+ Audience: m.Config().ClientID,
114+ Issuer: m.Issuer(),
115+ ExpiresAt: now.Add(1 * time.Hour).Unix(),
116+ IssuedAt: now.Unix(),
117+ Subject: "user",
118+ }
119+ if in.modifyClaims != nil {
120+ in.modifyClaims(&claims)
121+ }
122+
123+ rawIDToken, err := m.Keypair.SignJWT(claims)
124+ Expect(err).ToNot(HaveOccurred())
125+
126+ idToken, err := pv.Verifier().Verify(context.Background(), rawIDToken)
127+ if in.expectedError != "" {
128+ Expect(err).To(MatchError(HavePrefix(in.expectedError)))
129+ return
130+ }
131+ Expect(err).ToNot(HaveOccurred())
132+
133+ Expect(idToken.Issuer).To(Equal(claims.Issuer))
134+ Expect(idToken.Audience).To(ConsistOf(claims.Audience))
135+ Expect(idToken.Subject).To(Equal(claims.Subject))
136+ },
137+ Entry("with the default opts and claims", &verifierTableInput{}),
138+ Entry("when the audience is mismatched", &verifierTableInput{
139+ modifyClaims: func(j *jwt.StandardClaims) {
140+ j.Audience = "OtherClient"
141+ },
142+ expectedError: "audience from claim aud with value [OtherClient] does not match with any of allowed audiences",
143+ }),
144+ Entry("when the audience is an extra audience", &verifierTableInput{
145+ modifyOpts: func(p *ProviderVerifierOptions) {
146+ p.ExtraAudiences = []string{"ExtraIssuer"}
147+ },
148+ modifyClaims: func(j *jwt.StandardClaims) {
149+ j.Audience = "ExtraIssuer"
150+ },
151+ }),
152+ Entry("when the issuer is mismatched", &verifierTableInput{
153+ modifyClaims: func(j *jwt.StandardClaims) {
154+ j.Issuer = "OtherIssuer"
155+ },
156+ expectedError: "failed to verify token: oidc: id token issued by a different provider",
157+ }),
158+ Entry("when the issuer is mismatched with skip issuer verification", &verifierTableInput{
159+ modifyOpts: func(p *ProviderVerifierOptions) {
160+ p.SkipIssuerVerification = true
161+ },
162+ modifyClaims: func(j *jwt.StandardClaims) {
163+ j.Issuer = "OtherIssuer"
164+ },
165+ }),
166+ Entry("when the token has expired", &verifierTableInput{
167+ modifyClaims: func(j *jwt.StandardClaims) {
168+ j.ExpiresAt = time.Now().Add(-1 * time.Hour).Unix()
169+ },
170+ expectedError: "failed to verify token: oidc: token is expired",
171+ }),
172+ )
173+})