git.iko.soy/typeable/oauth2-proxy
on master help sign in
feat: add support for specifying allowed OIDC JWT signing algorithms (#2753) (#2851)

* feat: add support for specifying allowed OIDC JWT signing algorithms (#2753) TODO: - [X] update docs - [X] add support in yaml (modern) config - [X] add more test(s)? Add (legacy for now) configuration flag "oidc-enabled-signing-alg" (cfg: oidc_enabled_signing_algs) that allows setting what signing algorithms are specified by provider in JWT header ("alg" header claim). In particular useful when skip_oidc_discovery = true, as verifier defaults to only accept "RS256" in alg field in such circumstances. Signed-off-by: Jan Larwig <jan@larwig.com> * doc: update changelog and alpha config Signed-off-by: Jan Larwig <jan@larwig.com> * feat: add signing algorithm intersection handling with oidc discovery and additional tests Signed-off-by: Jan Larwig <jan@larwig.com> --------- Signed-off-by: Jan Larwig <jan@larwig.com> Co-authored-by: Jan Larwig <jan@larwig.com>

commit7c96234233d7aa192939e90700313cc9c82e7516 authorandoks <andoks@users.noreply.github.com> dateWed 18 Mar 2026, 15:24 (6 months ago) parent3085309
14 files changed+363−11
CHANGELOG.md+1 docs/docs/configuration/alpha_config.md+1 docs/docs/configuration/overview.md+2−1 main_test.go+1 pkg/apis/options/legacy_options.go+13−9 pkg/apis/options/legacy_options_test.go+2 pkg/apis/options/providers.go+5 pkg/providers/oidc/provider_verifier.go+36−1 pkg/providers/oidc/provider_verifier_test.go+92 pkg/validation/providers.go+33 pkg/validation/providers_test.go+53 providers/providers.go+1 providers/providers_test.go+90 providers/util_test.go+33
CHANGELOG.md+1
@@ -12,6 +12,7 @@
1212- [#3332](https://github.com/oauth2-proxy/oauth2-proxy/pull/3332) ci: distribute windows binary with .exe extension (@igitur)
1313- [#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)
1414- [#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)
1516
1617# V7.14.3
1718
docs/docs/configuration/alpha_config.md+1
@@ -488,6 +488,7 @@ character.
488488| `userIDClaim` | _string_ | UserIDClaim indicates which claim contains the user ID<br/>default set to 'email' |
489489| `audienceClaims` | _[]string_ | AudienceClaim allows to define any claim that is verified against the client id<br/>By default `aud` claim is used for verification. |
490490| `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. |
491492
492493### Provider
493494
docs/docs/configuration/overview.md+2−1
@@ -99,7 +99,8 @@ Provider specific options can be found on their respective subpages.
9999| flag: `--oidc-groups-claim`<br/>toml: `oidc_groups_claim` | string | which OIDC claim contains the user groups | `"groups"` |
100100| flag: `--oidc-issuer-url`<br/>toml: `oidc_issuer_url` | string | the OpenID Connect issuer URL, e.g. `"https://accounts.google.com"` | |
101101| 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. | |
103104| flag: `--profile-url`<br/>toml: `profile_url` | string | Profile access endpoint | |
104105| 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 | `""` |
105106| 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"
186186 InsecureAllowUnverifiedEmail: ptr.To(false),
187187 InsecureSkipIssuerVerification: ptr.To(false),
188188 SkipDiscovery: ptr.To(false),
189+ EnabledSigningAlgs: []string{},
189190 },
190191 MicrosoftEntraIDConfig: options.MicrosoftEntraIDOptions{
191192 FederatedTokenAuth: ptr.To(false),
pkg/apis/options/legacy_options.go+13−9
@@ -51,15 +51,16 @@ func NewLegacyOptions() *LegacyOptions {
5151 },
5252
5353 LegacyProvider: LegacyProvider{
54− ProviderType: "google",
55− AzureTenant: "common",
56− ApprovalPrompt: "force",
57− UserIDClaim: "email",
58− OIDCEmailClaim: "email",
59− OIDCGroupsClaim: "groups",
60− OIDCAudienceClaims: []string{"aud"},
61− OIDCExtraAudiences: []string{},
62− InsecureOIDCSkipNonce: true,
54+ ProviderType: "google",
55+ AzureTenant: "common",
56+ ApprovalPrompt: "force",
57+ UserIDClaim: "email",
58+ OIDCEmailClaim: "email",
59+ OIDCGroupsClaim: "groups",
60+ OIDCAudienceClaims: []string{"aud"},
61+ OIDCExtraAudiences: []string{},
62+ OIDCEnabledSigningAlgs: []string{},
63+ InsecureOIDCSkipNonce: true,
6364 },
6465
6566 Options: *NewOptions(),
@@ -545,6 +546,7 @@ type LegacyProvider struct {
545546 OIDCAudienceClaims []string `flag:"oidc-audience-claim" cfg:"oidc_audience_claims"`
546547 OIDCExtraAudiences []string `flag:"oidc-extra-audience" cfg:"oidc_extra_audiences"`
547548 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"`
548550 LoginURL string `flag:"login-url" cfg:"login_url"`
549551 AuthRequestResponseMode string `flag:"auth-request-response-mode" cfg:"auth_request_response_mode"`
550552 RedeemURL string `flag:"redeem-url" cfg:"redeem_url"`
@@ -606,6 +608,7 @@ func legacyProviderFlagSet() *pflag.FlagSet {
606608 flagSet.StringSlice("oidc-audience-claim", OIDCAudienceClaims, "which OIDC claims are used as audience to verify against client id")
607609 flagSet.StringSlice("oidc-extra-audience", []string{}, "additional audiences allowed to pass audience verification")
608610 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")
609612 flagSet.String("login-url", "", "Authentication endpoint")
610613 flagSet.String("redeem-url", "", "Token redemption endpoint")
611614 flagSet.String("profile-url", "", "Profile access endpoint")
@@ -727,6 +730,7 @@ func (l *LegacyProvider) convert() (Providers, error) {
727730 AudienceClaims: l.OIDCAudienceClaims,
728731 ExtraAudiences: l.OIDCExtraAudiences,
729732 PublicKeyFiles: l.OIDCPublicKeyFiles,
733+ EnabledSigningAlgs: l.OIDCEnabledSigningAlgs,
730734 }
731735
732736 // Support for legacy configuration option
pkg/apis/options/legacy_options_test.go+2
@@ -27,6 +27,7 @@ var _ = Describe("Legacy Options", func() {
2727 legacyOpts.LegacyUpstreams.Upstreams = []string{"http://foo.bar/baz", "file:///var/lib/website#/bar", "static://204"}
2828 legacyOpts.LegacyProvider.ClientID = "oauth-proxy"
2929 legacyOpts.LegacyUpstreams.DisableKeepAlives = false
30+ legacyOpts.LegacyProvider.OIDCEnabledSigningAlgs = []string{"RS256", "EdDSA"}
3031
3132 staticCode := 204
3233 opts.UpstreamServers = UpstreamConfig{
@@ -128,6 +129,7 @@ var _ = Describe("Legacy Options", func() {
128129 opts.Providers[0].OIDCConfig.ExtraAudiences = []string{}
129130 opts.Providers[0].OIDCConfig.InsecureSkipNonce = ptr.To(true)
130131 opts.Providers[0].OIDCConfig.InsecureSkipIssuerVerification = ptr.To(false)
132+ opts.Providers[0].OIDCConfig.EnabledSigningAlgs = []string{"RS256", "EdDSA"}
131133 opts.Providers[0].LoginURLParameters = []LoginURLParameter{
132134 {Name: "approval_prompt", Default: []string{"force"}},
133135 }
pkg/apis/options/providers.go+5
@@ -321,6 +321,11 @@ type OIDCOptions struct {
321321 // ExtraAudiences is a list of additional audiences that are allowed
322322 // to pass verification in addition to the client id.
323323 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"`
324329}
325330
326331type LoginGovOptions struct {
pkg/providers/oidc/provider_verifier.go+36−1
@@ -155,13 +155,48 @@ func getVerifierBuilder(ctx context.Context, opts ProviderVerifierOptions) (veri
155155 return nil, nil, fmt.Errorf("error while discovery OIDC configuration: %w", err)
156156 }
157157
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+
158163 return newVerifierBuilder(
159164 opts.IssuerURL,
160165 oidc.NewRemoteKeySet(ctx, provider.Endpoints().JWKsURL),
161− provider.SupportedSigningAlgs(),
166+ supportedSigningAlgs,
162167 ), provider, nil
163168}
164169
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+
165200// GetPublicKeyFromBytes parses a PEM-encoded public key from a byte array
166201// and returns a crypto.PublicKey object.
167202func getPublicKeyFromBytes(bytes []byte) (crypto.PublicKey, error) {
pkg/providers/oidc/provider_verifier_test.go+92
@@ -2,6 +2,9 @@ package oidc
22
33import (
44 "context"
5+ "encoding/json"
6+ "net"
7+ "net/http"
58 "os"
69 "path/filepath"
710 "time"
@@ -195,6 +198,21 @@ var _ = Describe("ProviderVerifier", func() {
195198 Expect(idToken.Subject).To(Equal(claims.Subject))
196199 },
197200 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+ }),
198216 Entry("when the audience is mismatched", &verifierTableInput{
199217 modifyClaims: func(j *jwt.RegisteredClaims) {
200218 j.Audience = jwt.ClaimStrings{"OtherClient"}
@@ -230,4 +248,78 @@ var _ = Describe("ProviderVerifier", func() {
230248 expectedError: "failed to verify token: oidc: token is expired",
231249 }),
232250 )
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+ })
233302})
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 (
44 "fmt"
55 "os"
66
7+ jose "github.com/go-jose/go-jose/v4"
78 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
89 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util/ptr"
910)
1011
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+
1128// validateProviders is the initial validation migration for multiple providrers
1229// It currently includes only logic that can verify the providers one by one and does not break the valdation pipe
1330func validateProviders(o *options.Options) []string {
@@ -59,6 +76,22 @@ func validateProvider(provider options.Provider, providerIDs map[string]struct{}
5976 msgs = append(msgs, validateEntraConfig(provider)...)
6077 }
6178
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+
6295 return msgs
6396}
6497
pkg/validation/providers_test.go+53
@@ -18,6 +18,33 @@ var _ = Describe("Providers", func() {
1818 ClientSecret: "ClientSecret",
1919 }
2020
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+
2148 validLoginGovProvider := options.Provider{
2249 Type: "login.gov",
2350 ID: "ProviderIDLoginGov",
@@ -34,6 +61,8 @@ var _ = Describe("Providers", func() {
3461 emptyIDMsg := "provider has empty id: ids are required for all providers"
3562 duplicateProviderIDMsg := "multiple providers found with id ProviderID: provider ids must be unique"
3663 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\""
3766
3867 DescribeTable("validateProviders",
3968 func(o *validateProvidersTableInput) {
@@ -79,5 +108,29 @@ var _ = Describe("Providers", func() {
79108 },
80109 errStrings: []string{skipButtonAndMultipleProvidersMsg},
81110 }),
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+ }),
82135 )
83136})
providers/providers.go+1
@@ -100,6 +100,7 @@ func newProviderDataFromConfig(providerConfig options.Provider) (*ProviderData,
100100 IssuerURL: providerConfig.OIDCConfig.IssuerURL,
101101 JWKsURL: providerConfig.OIDCConfig.JwksURL,
102102 PublicKeyFiles: providerConfig.OIDCConfig.PublicKeyFiles,
103+ SupportedSigningAlgs: providerConfig.OIDCConfig.EnabledSigningAlgs,
103104 SkipDiscovery: ptr.Deref(providerConfig.OIDCConfig.SkipDiscovery, options.DefaultSkipDiscovery),
104105 SkipIssuerVerification: ptr.Deref(providerConfig.OIDCConfig.InsecureSkipIssuerVerification, options.DefaultInsecureSkipIssuerVerification),
105106 })
providers/providers_test.go+90
@@ -1,9 +1,14 @@
11package providers
22
33import (
4+ "context"
5+ "net"
46 "os"
57 "testing"
8+ "time"
69
10+ "github.com/golang-jwt/jwt/v5"
11+ "github.com/oauth2-proxy/mockoidc"
712 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
813 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util/ptr"
914 . "github.com/onsi/gomega"
@@ -121,6 +126,91 @@ func TestURLsCorrectlyParsed(t *testing.T) {
121126 g.Expect(pd.RedeemURL.String()).To(Equal(msTokenURL))
122127}
123128
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+
124214func TestScope(t *testing.T) {
125215 g := NewWithT(t)
126216
providers/util_test.go+33
@@ -1,9 +1,12 @@
11package providers
22
33import (
4+ "encoding/json"
45 "fmt"
6+ "net/http"
57 "testing"
68
9+ "github.com/oauth2-proxy/mockoidc"
710 . "github.com/onsi/gomega"
811 "golang.org/x/oauth2"
912)
@@ -111,3 +114,33 @@ func Test_formatGroup(t *testing.T) {
111114 })
112115 }
113116}
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+}