git.iko.soy/typeable/oauth2-proxy
on master help sign in
feat: add support for specifying allowed OIDC JWT signing algorithms (#2753) (#2851)
7c96234andoks
CHANGELOG.md+1
@@ -12,6 +12,7 @@
12- [#3332](https://github.com/oauth2-proxy/oauth2-proxy/pull/3332) ci: distribute windows binary with .exe extension (@igitur)
12- [#3332](https://github.com/oauth2-proxy/oauth2-proxy/pull/3332) ci: distribute windows binary with .exe extension (@igitur)
13- [#2685](https://github.com/oauth2-proxy/oauth2-proxy/pull/2685) feat: allow arbitrary claims from the IDToken and IdentityProvider UserInfo endpoint to be added to the session state (@vegetablest)
13- [#2685](https://github.com/oauth2-proxy/oauth2-proxy/pull/2685) feat: allow arbitrary claims from the IDToken and IdentityProvider UserInfo endpoint to be added to the session state (@vegetablest)
14- [#3278](https://github.com/oauth2-proxy/oauth2-proxy/pull/3278) feat: possibility to inject id_token in redirect url during sign out (@albanf)
14- [#3278](https://github.com/oauth2-proxy/oauth2-proxy/pull/3278) feat: possibility to inject id_token in redirect url during sign out (@albanf)
15+- [#2851](https://github.com/oauth2-proxy/oauth2-proxy/pull/2851) feat: add support for specifying allowed OIDC JWT signing algorithms (#2753) (@andoks / @tuunit)
15
16
16# V7.14.3
17# V7.14.3
17
18
docs/docs/configuration/alpha_config.md+1
@@ -488,6 +488,7 @@ character.
488| `userIDClaim` | _string_ | UserIDClaim indicates which claim contains the user ID<br/>default set to 'email' |
488| `userIDClaim` | _string_ | UserIDClaim indicates which claim contains the user ID<br/>default set to 'email' |
489| `audienceClaims` | _[]string_ | AudienceClaim allows to define any claim that is verified against the client id<br/>By default `aud` claim is used for verification. |
489| `audienceClaims` | _[]string_ | AudienceClaim allows to define any claim that is verified against the client id<br/>By default `aud` claim is used for verification. |
490| `extraAudiences` | _[]string_ | ExtraAudiences is a list of additional audiences that are allowed<br/>to pass verification in addition to the client id. |
490| `extraAudiences` | _[]string_ | ExtraAudiences is a list of additional audiences that are allowed<br/>to pass verification in addition to the client id. |
491+| `enabledSigningAlgs` | _[]string_ | EnabledSigningAlgs is a list of allowed JWT signing algorithms.<br/>When discovery is enabled, the effective set is the intersection<br/>between this list and the provider's discovered supported algorithms.<br/>By default `RS256` is used if nothing has been discovered or specified. |
491
492
492### Provider
493### Provider
493
494
docs/docs/configuration/overview.md+2−1
@@ -99,7 +99,8 @@ Provider specific options can be found on their respective subpages.
99| flag: `--oidc-groups-claim`<br/>toml: `oidc_groups_claim` | string | which OIDC claim contains the user groups | `"groups"` |
99| flag: `--oidc-groups-claim`<br/>toml: `oidc_groups_claim` | string | which OIDC claim contains the user groups | `"groups"` |
100| flag: `--oidc-issuer-url`<br/>toml: `oidc_issuer_url` | string | the OpenID Connect issuer URL, e.g. `"https://accounts.google.com"` | |
100| flag: `--oidc-issuer-url`<br/>toml: `oidc_issuer_url` | string | the OpenID Connect issuer URL, e.g. `"https://accounts.google.com"` | |
101| flag: `--oidc-jwks-url`<br/>toml: `oidc_jwks_url` | string | OIDC JWKS URI for token verification; required if OIDC discovery is disabled and public key files are not provided | |
101| flag: `--oidc-jwks-url`<br/>toml: `oidc_jwks_url` | string | OIDC JWKS URI for token verification; required if OIDC discovery is disabled and public key files are not provided | |
102−| flag: `--oidc-public-key-file`<br/>toml: `oidc_public_key_files` | string | Path to public key file in PEM format to use for verifying JWT tokens (may be given multiple times). Required if OIDC discovery is disabled na JWKS URL isn't provided | string \| list |
102+| flag: `--oidc-public-key-file`<br/>toml: `oidc_public_key_files` | string | Path to public key file in PEM format to use for verifying JWT tokens (may be given multiple times). Required if OIDC discovery is disabled na JWKS URL isn't provided | |
103+| flag: `--oidc-enabled-signing-alg`<br/>toml: `oidc_enabled_signing_algs` | string \| list | List of allowed JWT signing algorithms. When oidc discovery is enabled, the effective set is the intersection between this list and the provider's discovered supported algorithms. | |
103| flag: `--profile-url`<br/>toml: `profile_url` | string | Profile access endpoint | |
104| flag: `--profile-url`<br/>toml: `profile_url` | string | Profile access endpoint | |
104| flag: `--prompt`<br/>toml: `prompt` | string | [OIDC prompt](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest); if present, `approval-prompt` is ignored | `""` |
105| flag: `--prompt`<br/>toml: `prompt` | string | [OIDC prompt](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest); if present, `approval-prompt` is ignored | `""` |
105| flag: `--provider-ca-file`<br/>toml: `provider_ca_files` | string \| list | Paths to CA certificates that should be used when connecting to the provider. If not specified, the default Go trust sources are used instead. |
106| flag: `--provider-ca-file`<br/>toml: `provider_ca_files` | string \| list | Paths to CA certificates that should be used when connecting to the provider. If not specified, the default Go trust sources are used instead. |
main_test.go+1
@@ -186,6 +186,7 @@ redirect_url="http://localhost:4180/oauth2/callback"
186 InsecureAllowUnverifiedEmail: ptr.To(false),
186 InsecureAllowUnverifiedEmail: ptr.To(false),
187 InsecureSkipIssuerVerification: ptr.To(false),
187 InsecureSkipIssuerVerification: ptr.To(false),
188 SkipDiscovery: ptr.To(false),
188 SkipDiscovery: ptr.To(false),
189+ EnabledSigningAlgs: []string{},
189 },
190 },
190 MicrosoftEntraIDConfig: options.MicrosoftEntraIDOptions{
191 MicrosoftEntraIDConfig: options.MicrosoftEntraIDOptions{
191 FederatedTokenAuth: ptr.To(false),
192 FederatedTokenAuth: ptr.To(false),
pkg/apis/options/legacy_options.go+13−9
@@ -51,15 +51,16 @@ func NewLegacyOptions() *LegacyOptions {
51 },
51 },
52
52
53 LegacyProvider: LegacyProvider{
53 LegacyProvider: LegacyProvider{
54− ProviderType: "google",
54+ ProviderType: "google",
55− AzureTenant: "common",
55+ AzureTenant: "common",
56− ApprovalPrompt: "force",
56+ ApprovalPrompt: "force",
57− UserIDClaim: "email",
57+ UserIDClaim: "email",
58− OIDCEmailClaim: "email",
58+ OIDCEmailClaim: "email",
59− OIDCGroupsClaim: "groups",
59+ OIDCGroupsClaim: "groups",
60− OIDCAudienceClaims: []string{"aud"},
60+ OIDCAudienceClaims: []string{"aud"},
61− OIDCExtraAudiences: []string{},
61+ OIDCExtraAudiences: []string{},
62+ OIDCEnabledSigningAlgs: []string{},
62− InsecureOIDCSkipNonce: true,
63+ InsecureOIDCSkipNonce: true,
63 },
64 },
64
65
65 Options: *NewOptions(),
66 Options: *NewOptions(),
@@ -545,6 +546,7 @@ type LegacyProvider struct {
545 OIDCAudienceClaims []string `flag:"oidc-audience-claim" cfg:"oidc_audience_claims"`
546 OIDCAudienceClaims []string `flag:"oidc-audience-claim" cfg:"oidc_audience_claims"`
546 OIDCExtraAudiences []string `flag:"oidc-extra-audience" cfg:"oidc_extra_audiences"`
547 OIDCExtraAudiences []string `flag:"oidc-extra-audience" cfg:"oidc_extra_audiences"`
547 OIDCPublicKeyFiles []string `flag:"oidc-public-key-file" cfg:"oidc_public_key_files"`
548 OIDCPublicKeyFiles []string `flag:"oidc-public-key-file" cfg:"oidc_public_key_files"`
549+ OIDCEnabledSigningAlgs []string `flag:"oidc-enabled-signing-alg" cfg:"oidc_enabled_signing_algs"`
548 LoginURL string `flag:"login-url" cfg:"login_url"`
550 LoginURL string `flag:"login-url" cfg:"login_url"`
549 AuthRequestResponseMode string `flag:"auth-request-response-mode" cfg:"auth_request_response_mode"`
551 AuthRequestResponseMode string `flag:"auth-request-response-mode" cfg:"auth_request_response_mode"`
550 RedeemURL string `flag:"redeem-url" cfg:"redeem_url"`
552 RedeemURL string `flag:"redeem-url" cfg:"redeem_url"`
@@ -606,6 +608,7 @@ func legacyProviderFlagSet() *pflag.FlagSet {
606 flagSet.StringSlice("oidc-audience-claim", OIDCAudienceClaims, "which OIDC claims are used as audience to verify against client id")
608 flagSet.StringSlice("oidc-audience-claim", OIDCAudienceClaims, "which OIDC claims are used as audience to verify against client id")
607 flagSet.StringSlice("oidc-extra-audience", []string{}, "additional audiences allowed to pass audience verification")
609 flagSet.StringSlice("oidc-extra-audience", []string{}, "additional audiences allowed to pass audience verification")
608 flagSet.StringSlice("oidc-public-key-file", []string{}, "path to public key file in PEM format to use for verifying JWT tokens (may be given multiple times)")
610 flagSet.StringSlice("oidc-public-key-file", []string{}, "path to public key file in PEM format to use for verifying JWT tokens (may be given multiple times)")
611+ flagSet.StringSlice("oidc-enabled-signing-alg", []string{}, "accepted signing algorithms for provider to use")
609 flagSet.String("login-url", "", "Authentication endpoint")
612 flagSet.String("login-url", "", "Authentication endpoint")
610 flagSet.String("redeem-url", "", "Token redemption endpoint")
613 flagSet.String("redeem-url", "", "Token redemption endpoint")
611 flagSet.String("profile-url", "", "Profile access endpoint")
614 flagSet.String("profile-url", "", "Profile access endpoint")
@@ -727,6 +730,7 @@ func (l *LegacyProvider) convert() (Providers, error) {
727 AudienceClaims: l.OIDCAudienceClaims,
730 AudienceClaims: l.OIDCAudienceClaims,
728 ExtraAudiences: l.OIDCExtraAudiences,
731 ExtraAudiences: l.OIDCExtraAudiences,
729 PublicKeyFiles: l.OIDCPublicKeyFiles,
732 PublicKeyFiles: l.OIDCPublicKeyFiles,
733+ EnabledSigningAlgs: l.OIDCEnabledSigningAlgs,
730 }
734 }
731
735
732 // Support for legacy configuration option
736 // Support for legacy configuration option
pkg/apis/options/legacy_options_test.go+2
@@ -27,6 +27,7 @@ var _ = Describe("Legacy Options", func() {
27 legacyOpts.LegacyUpstreams.Upstreams = []string{"http://foo.bar/baz", "file:///var/lib/website#/bar", "static://204"}
27 legacyOpts.LegacyUpstreams.Upstreams = []string{"http://foo.bar/baz", "file:///var/lib/website#/bar", "static://204"}
28 legacyOpts.LegacyProvider.ClientID = "oauth-proxy"
28 legacyOpts.LegacyProvider.ClientID = "oauth-proxy"
29 legacyOpts.LegacyUpstreams.DisableKeepAlives = false
29 legacyOpts.LegacyUpstreams.DisableKeepAlives = false
30+ legacyOpts.LegacyProvider.OIDCEnabledSigningAlgs = []string{"RS256", "EdDSA"}
30
31
31 staticCode := 204
32 staticCode := 204
32 opts.UpstreamServers = UpstreamConfig{
33 opts.UpstreamServers = UpstreamConfig{
@@ -128,6 +129,7 @@ var _ = Describe("Legacy Options", func() {
128 opts.Providers[0].OIDCConfig.ExtraAudiences = []string{}
129 opts.Providers[0].OIDCConfig.ExtraAudiences = []string{}
129 opts.Providers[0].OIDCConfig.InsecureSkipNonce = ptr.To(true)
130 opts.Providers[0].OIDCConfig.InsecureSkipNonce = ptr.To(true)
130 opts.Providers[0].OIDCConfig.InsecureSkipIssuerVerification = ptr.To(false)
131 opts.Providers[0].OIDCConfig.InsecureSkipIssuerVerification = ptr.To(false)
132+ opts.Providers[0].OIDCConfig.EnabledSigningAlgs = []string{"RS256", "EdDSA"}
131 opts.Providers[0].LoginURLParameters = []LoginURLParameter{
133 opts.Providers[0].LoginURLParameters = []LoginURLParameter{
132 {Name: "approval_prompt", Default: []string{"force"}},
134 {Name: "approval_prompt", Default: []string{"force"}},
133 }
135 }
pkg/apis/options/providers.go+5
@@ -321,6 +321,11 @@ type OIDCOptions struct {
321 // ExtraAudiences is a list of additional audiences that are allowed
321 // ExtraAudiences is a list of additional audiences that are allowed
322 // to pass verification in addition to the client id.
322 // to pass verification in addition to the client id.
323 ExtraAudiences []string `yaml:"extraAudiences,omitempty"`
323 ExtraAudiences []string `yaml:"extraAudiences,omitempty"`
324+ // EnabledSigningAlgs is a list of allowed JWT signing algorithms.
325+ // When discovery is enabled, the effective set is the intersection
326+ // between this list and the provider's discovered supported algorithms.
327+ // By default `RS256` is used if nothing has been discovered or specified.
328+ EnabledSigningAlgs []string `yaml:"enabledSigningAlgs,omitempty"`
324}
329}
325
330
326type LoginGovOptions struct {
331type LoginGovOptions struct {
pkg/providers/oidc/provider_verifier.go+36−1
@@ -155,13 +155,48 @@ func getVerifierBuilder(ctx context.Context, opts ProviderVerifierOptions) (veri
155 return nil, nil, fmt.Errorf("error while discovery OIDC configuration: %w", err)
155 return nil, nil, fmt.Errorf("error while discovery OIDC configuration: %w", err)
156 }
156 }
157
157
158+ supportedSigningAlgs, err := intersectSigningAlgs(provider.SupportedSigningAlgs(), opts.SupportedSigningAlgs)
159+ if err != nil {
160+ return nil, nil, fmt.Errorf("error while determining supported signing algorithms: %w", err)
161+ }
162+
158 return newVerifierBuilder(
163 return newVerifierBuilder(
159 opts.IssuerURL,
164 opts.IssuerURL,
160 oidc.NewRemoteKeySet(ctx, provider.Endpoints().JWKsURL),
165 oidc.NewRemoteKeySet(ctx, provider.Endpoints().JWKsURL),
161− provider.SupportedSigningAlgs(),
166+ supportedSigningAlgs,
162 ), provider, nil
167 ), provider, nil
163}
168}
164
169
170+// intersectSigningAlgs returns the intersecting list of signing algorithms from the oidc discovery
171+// and the signing algorithms provided through the options.
172+func intersectSigningAlgs(discoveredSigningAlgs, configuredSigningAlgs []string) ([]string, error) {
173+ if len(configuredSigningAlgs) == 0 {
174+ return discoveredSigningAlgs, nil
175+ }
176+
177+ if len(discoveredSigningAlgs) == 0 {
178+ return configuredSigningAlgs, nil
179+ }
180+
181+ discovered := make(map[string]struct{}, len(discoveredSigningAlgs))
182+ for _, signingAlg := range discoveredSigningAlgs {
183+ discovered[signingAlg] = struct{}{}
184+ }
185+
186+ intersection := make([]string, 0, len(configuredSigningAlgs))
187+ for _, signingAlg := range configuredSigningAlgs {
188+ if _, ok := discovered[signingAlg]; ok {
189+ intersection = append(intersection, signingAlg)
190+ }
191+ }
192+
193+ if len(intersection) == 0 {
194+ return nil, fmt.Errorf("no supported signing algorithms in common between provider and configuration: discovered=%v, configured=%v", discoveredSigningAlgs, configuredSigningAlgs)
195+ }
196+
197+ return intersection, nil
198+}
199+
165// GetPublicKeyFromBytes parses a PEM-encoded public key from a byte array
200// GetPublicKeyFromBytes parses a PEM-encoded public key from a byte array
166// and returns a crypto.PublicKey object.
201// and returns a crypto.PublicKey object.
167func getPublicKeyFromBytes(bytes []byte) (crypto.PublicKey, error) {
202func getPublicKeyFromBytes(bytes []byte) (crypto.PublicKey, error) {
pkg/providers/oidc/provider_verifier_test.go+92
@@ -2,6 +2,9 @@ package oidc
2
2
3import (
3import (
4 "context"
4 "context"
5+ "encoding/json"
6+ "net"
7+ "net/http"
5 "os"
8 "os"
6 "path/filepath"
9 "path/filepath"
7 "time"
10 "time"
@@ -195,6 +198,21 @@ var _ = Describe("ProviderVerifier", func() {
195 Expect(idToken.Subject).To(Equal(claims.Subject))
198 Expect(idToken.Subject).To(Equal(claims.Subject))
196 },
199 },
197 Entry("with the default opts and claims", &verifierTableInput{}),
200 Entry("with the default opts and claims", &verifierTableInput{}),
201+ Entry("with skip discovery and an allowed signing algorithm", &verifierTableInput{
202+ modifyOpts: func(p *ProviderVerifierOptions) {
203+ p.SkipDiscovery = true
204+ p.JWKsURL = m.JWKSEndpoint()
205+ p.SupportedSigningAlgs = []string{"RS256"}
206+ },
207+ }),
208+ Entry("with skip discovery and a disallowed signing algorithm", &verifierTableInput{
209+ modifyOpts: func(p *ProviderVerifierOptions) {
210+ p.SkipDiscovery = true
211+ p.JWKsURL = m.JWKSEndpoint()
212+ p.SupportedSigningAlgs = []string{"HS256"}
213+ },
214+ expectedError: "failed to verify token: oidc: malformed jwt: unexpected signature algorithm \"RS256\"; expected [\"HS256\"]",
215+ }),
198 Entry("when the audience is mismatched", &verifierTableInput{
216 Entry("when the audience is mismatched", &verifierTableInput{
199 modifyClaims: func(j *jwt.RegisteredClaims) {
217 modifyClaims: func(j *jwt.RegisteredClaims) {
200 j.Audience = jwt.ClaimStrings{"OtherClient"}
218 j.Audience = jwt.ClaimStrings{"OtherClient"}
@@ -230,4 +248,78 @@ var _ = Describe("ProviderVerifier", func() {
230 expectedError: "failed to verify token: oidc: token is expired",
248 expectedError: "failed to verify token: oidc: token is expired",
231 }),
249 }),
232 )
250 )
251+
252+ Describe("intersectSigningAlgs", func() {
253+ DescribeTable("when determining allowed signing algorithms", func(discoveredSigningAlgs, configuredSigningAlgs, expected []string, expectedError string) {
254+ actual, err := intersectSigningAlgs(discoveredSigningAlgs, configuredSigningAlgs)
255+ Expect(actual).To(Equal(expected))
256+ if len(expectedError) > 0 {
257+ Expect(err).To(HaveOccurred())
258+ Expect(err.Error()).To(Equal(expectedError))
259+ }
260+ },
261+ Entry("returns discovered values when no configured values are provided", []string{"RS256", "HS256"}, []string(nil), []string{"RS256", "HS256"}, ""),
262+ Entry("returns configured values when no discovered values are provided", []string(nil), []string{"RS256"}, []string{"RS256"}, ""),
263+ Entry("returns the configured order of the intersection", []string{"RS256", "HS256", "EdDSA"}, []string{"EdDSA", "RS256"}, []string{"EdDSA", "RS256"}, ""),
264+ Entry("returns an error when there is no intersection", []string{"RS256", "HS256"}, []string{"EdDSA"}, nil, "no supported signing algorithms in common between provider and configuration: discovered=[RS256 HS256], configured=[EdDSA]"),
265+ )
266+ })
267+
268+ It("uses the intersection between discovered and configured signing algorithms", func() {
269+ customServer, err := mockoidc.NewServer(nil)
270+ Expect(err).ToNot(HaveOccurred())
271+ customServer.AddMiddleware(newConfiguredSigningAlgsIssuerMiddleware(customServer, []string{"RS256", "HS256"}))
272+
273+ listener, err := net.Listen("tcp", "127.0.0.1:0")
274+ Expect(err).ToNot(HaveOccurred())
275+
276+ Expect(customServer.Start(listener, nil)).To(Succeed())
277+ defer func() {
278+ Expect(customServer.Shutdown()).To(Succeed())
279+ }()
280+
281+ pv, err := NewProviderVerifier(context.Background(), ProviderVerifierOptions{
282+ AudienceClaims: []string{"aud"},
283+ ClientID: customServer.Config().ClientID,
284+ ExtraAudiences: []string{},
285+ IssuerURL: customServer.Issuer(),
286+ SupportedSigningAlgs: []string{"HS256"},
287+ })
288+ Expect(err).ToNot(HaveOccurred())
289+
290+ rawIDToken, err := customServer.Keypair.SignJWT(jwt.RegisteredClaims{
291+ Audience: jwt.ClaimStrings{customServer.Config().ClientID},
292+ Issuer: customServer.Issuer(),
293+ ExpiresAt: jwt.NewNumericDate(time.Now().Add(1 * time.Hour)),
294+ IssuedAt: jwt.NewNumericDate(time.Now()),
295+ Subject: "user",
296+ })
297+ Expect(err).ToNot(HaveOccurred())
298+
299+ _, err = pv.Verifier().Verify(context.Background(), rawIDToken)
300+ Expect(err).To(MatchError(HavePrefix("failed to verify token: oidc: malformed jwt: unexpected signature algorithm \"RS256\"; expected [\"HS256\"]")))
301+ })
233})
302})
303+
304+func newConfiguredSigningAlgsIssuerMiddleware(m *mockoidc.MockOIDC, supportedSigningAlgs []string) func(http.Handler) http.Handler {
305+ return func(next http.Handler) http.Handler {
306+ return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
307+ provider := providerJSON{
308+ Issuer: m.Issuer(),
309+ AuthURL: m.AuthorizationEndpoint(),
310+ TokenURL: m.TokenEndpoint(),
311+ JWKsURL: m.JWKSEndpoint(),
312+ UserInfoURL: m.UserinfoEndpoint(),
313+ SupportedSigningAlgs: supportedSigningAlgs,
314+ }
315+
316+ data, err := json.Marshal(provider)
317+ if err != nil {
318+ rw.WriteHeader(http.StatusInternalServerError)
319+ return
320+ }
321+
322+ _, _ = rw.Write(data)
323+ })
324+ }
325+}
pkg/validation/providers.go+33
@@ -4,10 +4,27 @@ import (
4 "fmt"
4 "fmt"
5 "os"
5 "os"
6
6
7+ jose "github.com/go-jose/go-jose/v4"
7 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
8 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
8 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util/ptr"
9 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util/ptr"
9)
10)
10
11
12+var supportedOIDCSigningAlgorithms = map[jose.SignatureAlgorithm]struct{}{
13+ jose.EdDSA: {},
14+ jose.HS256: {},
15+ jose.HS384: {},
16+ jose.HS512: {},
17+ jose.RS256: {},
18+ jose.RS384: {},
19+ jose.RS512: {},
20+ jose.ES256: {},
21+ jose.ES384: {},
22+ jose.ES512: {},
23+ jose.PS256: {},
24+ jose.PS384: {},
25+ jose.PS512: {},
26+}
27+
11// validateProviders is the initial validation migration for multiple providrers
28// validateProviders is the initial validation migration for multiple providrers
12// It currently includes only logic that can verify the providers one by one and does not break the valdation pipe
29// It currently includes only logic that can verify the providers one by one and does not break the valdation pipe
13func validateProviders(o *options.Options) []string {
30func validateProviders(o *options.Options) []string {
@@ -59,6 +76,22 @@ func validateProvider(provider options.Provider, providerIDs map[string]struct{}
59 msgs = append(msgs, validateEntraConfig(provider)...)
76 msgs = append(msgs, validateEntraConfig(provider)...)
60 }
77 }
61
78
79+ msgs = append(msgs, validateOIDCSigningAlgorithms(provider)...)
80+
81+ return msgs
82+}
83+
84+func validateOIDCSigningAlgorithms(provider options.Provider) []string {
85+ msgs := []string{}
86+
87+ for _, algorithm := range provider.OIDCConfig.EnabledSigningAlgs {
88+ if _, ok := supportedOIDCSigningAlgorithms[jose.SignatureAlgorithm(algorithm)]; ok {
89+ continue
90+ }
91+
92+ msgs = append(msgs, fmt.Sprintf("provider %s has invalid EnabledSigningAlgs entry %q", provider.ID, algorithm))
93+ }
94+
62 return msgs
95 return msgs
63}
96}
64
97
pkg/validation/providers_test.go+53
@@ -18,6 +18,33 @@ var _ = Describe("Providers", func() {
18 ClientSecret: "ClientSecret",
18 ClientSecret: "ClientSecret",
19 }
19 }
20
20
21+ validOIDCSigningAlgorithmsProvider := options.Provider{
22+ ID: "ProviderIDOIDCSigningAlgorithms",
23+ ClientID: "ClientID",
24+ ClientSecret: "ClientSecret",
25+ OIDCConfig: options.OIDCOptions{
26+ EnabledSigningAlgs: []string{"RS256", "EdDSA"},
27+ },
28+ }
29+
30+ invalidOIDCSigningAlgorithmsProvider := options.Provider{
31+ ID: "ProviderIDInvalidOIDCSigningAlgorithms",
32+ ClientID: "ClientID",
33+ ClientSecret: "ClientSecret",
34+ OIDCConfig: options.OIDCOptions{
35+ EnabledSigningAlgs: []string{"RS256", "invalid"},
36+ },
37+ }
38+
39+ invalidOIDCSigningAlgorithmCaseProvider := options.Provider{
40+ ID: "ProviderIDInvalidOIDCSigningAlgorithmCase",
41+ ClientID: "ClientID",
42+ ClientSecret: "ClientSecret",
43+ OIDCConfig: options.OIDCOptions{
44+ EnabledSigningAlgs: []string{"rs256"},
45+ },
46+ }
47+
21 validLoginGovProvider := options.Provider{
48 validLoginGovProvider := options.Provider{
22 Type: "login.gov",
49 Type: "login.gov",
23 ID: "ProviderIDLoginGov",
50 ID: "ProviderIDLoginGov",
@@ -34,6 +61,8 @@ var _ = Describe("Providers", func() {
34 emptyIDMsg := "provider has empty id: ids are required for all providers"
61 emptyIDMsg := "provider has empty id: ids are required for all providers"
35 duplicateProviderIDMsg := "multiple providers found with id ProviderID: provider ids must be unique"
62 duplicateProviderIDMsg := "multiple providers found with id ProviderID: provider ids must be unique"
36 skipButtonAndMultipleProvidersMsg := "SkipProviderButton and multiple providers are mutually exclusive"
63 skipButtonAndMultipleProvidersMsg := "SkipProviderButton and multiple providers are mutually exclusive"
64+ invalidOIDCSigningAlgorithmMsg := "provider ProviderIDInvalidOIDCSigningAlgorithms has invalid EnabledSigningAlgs entry \"invalid\""
65+ invalidOIDCSigningAlgorithmCaseMsg := "provider ProviderIDInvalidOIDCSigningAlgorithmCase has invalid EnabledSigningAlgs entry \"rs256\""
37
66
38 DescribeTable("validateProviders",
67 DescribeTable("validateProviders",
39 func(o *validateProvidersTableInput) {
68 func(o *validateProvidersTableInput) {
@@ -79,5 +108,29 @@ var _ = Describe("Providers", func() {
79 },
108 },
80 errStrings: []string{skipButtonAndMultipleProvidersMsg},
109 errStrings: []string{skipButtonAndMultipleProvidersMsg},
81 }),
110 }),
111+ Entry("with valid OIDC signing algorithms", &validateProvidersTableInput{
112+ options: &options.Options{
113+ Providers: options.Providers{
114+ validOIDCSigningAlgorithmsProvider,
115+ },
116+ },
117+ errStrings: []string{},
118+ }),
119+ Entry("with an invalid OIDC signing algorithm", &validateProvidersTableInput{
120+ options: &options.Options{
121+ Providers: options.Providers{
122+ invalidOIDCSigningAlgorithmsProvider,
123+ },
124+ },
125+ errStrings: []string{invalidOIDCSigningAlgorithmMsg},
126+ }),
127+ Entry("with an OIDC signing algorithm using invalid casing", &validateProvidersTableInput{
128+ options: &options.Options{
129+ Providers: options.Providers{
130+ invalidOIDCSigningAlgorithmCaseProvider,
131+ },
132+ },
133+ errStrings: []string{invalidOIDCSigningAlgorithmCaseMsg},
134+ }),
82 )
135 )
83})
136})
providers/providers.go+1
@@ -100,6 +100,7 @@ func newProviderDataFromConfig(providerConfig options.Provider) (*ProviderData,
100 IssuerURL: providerConfig.OIDCConfig.IssuerURL,
100 IssuerURL: providerConfig.OIDCConfig.IssuerURL,
101 JWKsURL: providerConfig.OIDCConfig.JwksURL,
101 JWKsURL: providerConfig.OIDCConfig.JwksURL,
102 PublicKeyFiles: providerConfig.OIDCConfig.PublicKeyFiles,
102 PublicKeyFiles: providerConfig.OIDCConfig.PublicKeyFiles,
103+ SupportedSigningAlgs: providerConfig.OIDCConfig.EnabledSigningAlgs,
103 SkipDiscovery: ptr.Deref(providerConfig.OIDCConfig.SkipDiscovery, options.DefaultSkipDiscovery),
104 SkipDiscovery: ptr.Deref(providerConfig.OIDCConfig.SkipDiscovery, options.DefaultSkipDiscovery),
104 SkipIssuerVerification: ptr.Deref(providerConfig.OIDCConfig.InsecureSkipIssuerVerification, options.DefaultInsecureSkipIssuerVerification),
105 SkipIssuerVerification: ptr.Deref(providerConfig.OIDCConfig.InsecureSkipIssuerVerification, options.DefaultInsecureSkipIssuerVerification),
105 })
106 })
providers/providers_test.go+90
@@ -1,9 +1,14 @@
1package providers
1package providers
2
2
3import (
3import (
4+ "context"
5+ "net"
4 "os"
6 "os"
5 "testing"
7 "testing"
8+ "time"
6
9
10+ "github.com/golang-jwt/jwt/v5"
11+ "github.com/oauth2-proxy/mockoidc"
7 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
12 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
8 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util/ptr"
13 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util/ptr"
9 . "github.com/onsi/gomega"
14 . "github.com/onsi/gomega"
@@ -121,6 +126,91 @@ func TestURLsCorrectlyParsed(t *testing.T) {
121 g.Expect(pd.RedeemURL.String()).To(Equal(msTokenURL))
126 g.Expect(pd.RedeemURL.String()).To(Equal(msTokenURL))
122}
127}
123
128
129+func TestEnabledSigningAlgsAreAppliedToProviderVerifier(t *testing.T) {
130+ g := NewWithT(t)
131+
132+ m, err := mockoidc.NewServer(nil)
133+ g.Expect(err).ToNot(HaveOccurred())
134+ m.AddMiddleware(newSigningAlgsIssuerMiddleware(m, []string{"RS256", "HS256"}))
135+
136+ listener, err := net.Listen("tcp", "127.0.0.1:0")
137+ g.Expect(err).ToNot(HaveOccurred())
138+ g.Expect(m.Start(listener, nil)).To(Succeed())
139+ defer func() {
140+ g.Expect(m.Shutdown()).To(Succeed())
141+ }()
142+
143+ providerConfig := options.Provider{
144+ ID: providerID,
145+ Type: "oidc",
146+ ClientID: m.Config().ClientID,
147+ ClientSecretFile: clientSecret,
148+ OIDCConfig: options.OIDCOptions{
149+ IssuerURL: m.Issuer(),
150+ AudienceClaims: []string{"aud"},
151+ EnabledSigningAlgs: []string{"HS256"},
152+ },
153+ }
154+
155+ pd, err := newProviderDataFromConfig(providerConfig)
156+ g.Expect(err).ToNot(HaveOccurred())
157+
158+ rawIDToken, err := m.Keypair.SignJWT(jwt.RegisteredClaims{
159+ Audience: jwt.ClaimStrings{m.Config().ClientID},
160+ Issuer: m.Issuer(),
161+ ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
162+ IssuedAt: jwt.NewNumericDate(time.Now()),
163+ Subject: "user",
164+ })
165+ g.Expect(err).ToNot(HaveOccurred())
166+
167+ _, err = pd.Verifier.Verify(context.Background(), rawIDToken)
168+ g.Expect(err).To(HaveOccurred())
169+ g.Expect(err.Error()).To(ContainSubstring("unexpected signature algorithm"))
170+}
171+
172+func TestEnabledSigningAlgsRejectUnsupportedTokens(t *testing.T) {
173+ g := NewWithT(t)
174+
175+ m, err := mockoidc.Run()
176+ g.Expect(err).ToNot(HaveOccurred())
177+ defer func() {
178+ g.Expect(m.Shutdown()).To(Succeed())
179+ }()
180+
181+ providerConfig := options.Provider{
182+ ID: providerID,
183+ Type: "oidc",
184+ ClientID: m.Config().ClientID,
185+ ClientSecretFile: clientSecret,
186+ LoginURL: m.AuthorizationEndpoint(),
187+ RedeemURL: m.TokenEndpoint(),
188+ OIDCConfig: options.OIDCOptions{
189+ IssuerURL: m.Issuer(),
190+ SkipDiscovery: ptr.To(true),
191+ JwksURL: m.JWKSEndpoint(),
192+ AudienceClaims: []string{"aud"},
193+ EnabledSigningAlgs: []string{"HS256"},
194+ },
195+ }
196+
197+ pd, err := newProviderDataFromConfig(providerConfig)
198+ g.Expect(err).ToNot(HaveOccurred())
199+
200+ rawIDToken, err := m.Keypair.SignJWT(jwt.RegisteredClaims{
201+ Audience: jwt.ClaimStrings{m.Config().ClientID},
202+ Issuer: m.Issuer(),
203+ ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
204+ IssuedAt: jwt.NewNumericDate(time.Now()),
205+ Subject: "user",
206+ })
207+ g.Expect(err).ToNot(HaveOccurred())
208+
209+ _, err = pd.Verifier.Verify(context.Background(), rawIDToken)
210+ g.Expect(err).To(HaveOccurred())
211+ g.Expect(err.Error()).To(ContainSubstring("unexpected signature algorithm"))
212+}
213+
124func TestScope(t *testing.T) {
214func TestScope(t *testing.T) {
125 g := NewWithT(t)
215 g := NewWithT(t)
126
216
providers/util_test.go+33
@@ -1,9 +1,12 @@
1package providers
1package providers
2
2
3import (
3import (
4+ "encoding/json"
4 "fmt"
5 "fmt"
6+ "net/http"
5 "testing"
7 "testing"
6
8
9+ "github.com/oauth2-proxy/mockoidc"
7 . "github.com/onsi/gomega"
10 . "github.com/onsi/gomega"
8 "golang.org/x/oauth2"
11 "golang.org/x/oauth2"
9)
12)
@@ -111,3 +114,33 @@ func Test_formatGroup(t *testing.T) {
111 })
114 })
112 }
115 }
113}
116}
117+
118+func newSigningAlgsIssuerMiddleware(m *mockoidc.MockOIDC, supportedSigningAlgs []string) func(http.Handler) http.Handler {
119+ return func(next http.Handler) http.Handler {
120+ return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
121+ provider := struct {
122+ Issuer string `json:"issuer"`
123+ AuthURL string `json:"authorization_endpoint"`
124+ TokenURL string `json:"token_endpoint"`
125+ JWKsURL string `json:"jwks_uri"`
126+ UserInfoURL string `json:"userinfo_endpoint"`
127+ SupportedSigningAlgs []string `json:"id_token_signing_alg_values_supported"`
128+ }{
129+ Issuer: m.Issuer(),
130+ AuthURL: m.AuthorizationEndpoint(),
131+ TokenURL: m.TokenEndpoint(),
132+ JWKsURL: m.JWKSEndpoint(),
133+ UserInfoURL: m.UserinfoEndpoint(),
134+ SupportedSigningAlgs: supportedSigningAlgs,
135+ }
136+
137+ data, err := json.Marshal(provider)
138+ if err != nil {
139+ rw.WriteHeader(http.StatusInternalServerError)
140+ return
141+ }
142+
143+ _, _ = rw.Write(data)
144+ })
145+ }
146+}