From 7c96234233d7aa192939e90700313cc9c82e7516 Mon Sep 17 00:00:00 2001 From: andoks Date: Wed, 18 Mar 2026 15:24:27 +0100 Subject: [PATCH] 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 * doc: update changelog and alpha config Signed-off-by: Jan Larwig * feat: add signing algorithm intersection handling with oidc discovery and additional tests Signed-off-by: Jan Larwig --------- Signed-off-by: Jan Larwig Co-authored-by: Jan Larwig --- CHANGELOG.md | 1 + docs/docs/configuration/alpha_config.md | 1 + docs/docs/configuration/overview.md | 3 +- main_test.go | 1 + pkg/apis/options/legacy_options.go | 22 +++-- pkg/apis/options/legacy_options_test.go | 2 + pkg/apis/options/providers.go | 5 ++ pkg/providers/oidc/provider_verifier.go | 37 +++++++- 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 +++++++ 14 files changed, 363 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3225c3f..9d964685 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ - [#3332](https://github.com/oauth2-proxy/oauth2-proxy/pull/3332) ci: distribute windows binary with .exe extension (@igitur) - [#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) - [#3278](https://github.com/oauth2-proxy/oauth2-proxy/pull/3278) feat: possibility to inject id_token in redirect url during sign out (@albanf) +- [#2851](https://github.com/oauth2-proxy/oauth2-proxy/pull/2851) feat: add support for specifying allowed OIDC JWT signing algorithms (#2753) (@andoks / @tuunit) # V7.14.3 diff --git a/docs/docs/configuration/alpha_config.md b/docs/docs/configuration/alpha_config.md index b92b42f1..385a9f85 100644 --- a/docs/docs/configuration/alpha_config.md +++ b/docs/docs/configuration/alpha_config.md @@ -488,6 +488,7 @@ character. | `userIDClaim` | _string_ | UserIDClaim indicates which claim contains the user ID
default set to 'email' | | `audienceClaims` | _[]string_ | AudienceClaim allows to define any claim that is verified against the client id
By default `aud` claim is used for verification. | | `extraAudiences` | _[]string_ | ExtraAudiences is a list of additional audiences that are allowed
to pass verification in addition to the client id. | +| `enabledSigningAlgs` | _[]string_ | EnabledSigningAlgs is a list of allowed JWT signing algorithms.
When discovery is enabled, the effective set is the intersection
between this list and the provider's discovered supported algorithms.
By default `RS256` is used if nothing has been discovered or specified. | ### Provider diff --git a/docs/docs/configuration/overview.md b/docs/docs/configuration/overview.md index 7bd7bf07..54ca3776 100644 --- a/docs/docs/configuration/overview.md +++ b/docs/docs/configuration/overview.md @@ -99,7 +99,8 @@ Provider specific options can be found on their respective subpages. | flag: `--oidc-groups-claim`
toml: `oidc_groups_claim` | string | which OIDC claim contains the user groups | `"groups"` | | flag: `--oidc-issuer-url`
toml: `oidc_issuer_url` | string | the OpenID Connect issuer URL, e.g. `"https://accounts.google.com"` | | | flag: `--oidc-jwks-url`
toml: `oidc_jwks_url` | string | OIDC JWKS URI for token verification; required if OIDC discovery is disabled and public key files are not provided | | -| flag: `--oidc-public-key-file`
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 | +| flag: `--oidc-public-key-file`
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 | | +| flag: `--oidc-enabled-signing-alg`
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. | | | flag: `--profile-url`
toml: `profile_url` | string | Profile access endpoint | | | flag: `--prompt`
toml: `prompt` | string | [OIDC prompt](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest); if present, `approval-prompt` is ignored | `""` | | flag: `--provider-ca-file`
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. | diff --git a/main_test.go b/main_test.go index cbe79683..a90f1a38 100644 --- a/main_test.go +++ b/main_test.go @@ -186,6 +186,7 @@ redirect_url="http://localhost:4180/oauth2/callback" InsecureAllowUnverifiedEmail: ptr.To(false), InsecureSkipIssuerVerification: ptr.To(false), SkipDiscovery: ptr.To(false), + EnabledSigningAlgs: []string{}, }, MicrosoftEntraIDConfig: options.MicrosoftEntraIDOptions{ FederatedTokenAuth: ptr.To(false), diff --git a/pkg/apis/options/legacy_options.go b/pkg/apis/options/legacy_options.go index b4f37aaf..99e3679f 100644 --- a/pkg/apis/options/legacy_options.go +++ b/pkg/apis/options/legacy_options.go @@ -51,15 +51,16 @@ func NewLegacyOptions() *LegacyOptions { }, LegacyProvider: LegacyProvider{ - ProviderType: "google", - AzureTenant: "common", - ApprovalPrompt: "force", - UserIDClaim: "email", - OIDCEmailClaim: "email", - OIDCGroupsClaim: "groups", - OIDCAudienceClaims: []string{"aud"}, - OIDCExtraAudiences: []string{}, - InsecureOIDCSkipNonce: true, + ProviderType: "google", + AzureTenant: "common", + ApprovalPrompt: "force", + UserIDClaim: "email", + OIDCEmailClaim: "email", + OIDCGroupsClaim: "groups", + OIDCAudienceClaims: []string{"aud"}, + OIDCExtraAudiences: []string{}, + OIDCEnabledSigningAlgs: []string{}, + InsecureOIDCSkipNonce: true, }, Options: *NewOptions(), @@ -545,6 +546,7 @@ type LegacyProvider struct { OIDCAudienceClaims []string `flag:"oidc-audience-claim" cfg:"oidc_audience_claims"` OIDCExtraAudiences []string `flag:"oidc-extra-audience" cfg:"oidc_extra_audiences"` OIDCPublicKeyFiles []string `flag:"oidc-public-key-file" cfg:"oidc_public_key_files"` + OIDCEnabledSigningAlgs []string `flag:"oidc-enabled-signing-alg" cfg:"oidc_enabled_signing_algs"` LoginURL string `flag:"login-url" cfg:"login_url"` AuthRequestResponseMode string `flag:"auth-request-response-mode" cfg:"auth_request_response_mode"` RedeemURL string `flag:"redeem-url" cfg:"redeem_url"` @@ -606,6 +608,7 @@ func legacyProviderFlagSet() *pflag.FlagSet { flagSet.StringSlice("oidc-audience-claim", OIDCAudienceClaims, "which OIDC claims are used as audience to verify against client id") flagSet.StringSlice("oidc-extra-audience", []string{}, "additional audiences allowed to pass audience verification") 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)") + flagSet.StringSlice("oidc-enabled-signing-alg", []string{}, "accepted signing algorithms for provider to use") flagSet.String("login-url", "", "Authentication endpoint") flagSet.String("redeem-url", "", "Token redemption endpoint") flagSet.String("profile-url", "", "Profile access endpoint") @@ -727,6 +730,7 @@ func (l *LegacyProvider) convert() (Providers, error) { AudienceClaims: l.OIDCAudienceClaims, ExtraAudiences: l.OIDCExtraAudiences, PublicKeyFiles: l.OIDCPublicKeyFiles, + EnabledSigningAlgs: l.OIDCEnabledSigningAlgs, } // Support for legacy configuration option diff --git a/pkg/apis/options/legacy_options_test.go b/pkg/apis/options/legacy_options_test.go index d8d14bb0..f6cbfb7c 100644 --- a/pkg/apis/options/legacy_options_test.go +++ b/pkg/apis/options/legacy_options_test.go @@ -27,6 +27,7 @@ var _ = Describe("Legacy Options", func() { legacyOpts.LegacyUpstreams.Upstreams = []string{"http://foo.bar/baz", "file:///var/lib/website#/bar", "static://204"} legacyOpts.LegacyProvider.ClientID = "oauth-proxy" legacyOpts.LegacyUpstreams.DisableKeepAlives = false + legacyOpts.LegacyProvider.OIDCEnabledSigningAlgs = []string{"RS256", "EdDSA"} staticCode := 204 opts.UpstreamServers = UpstreamConfig{ @@ -128,6 +129,7 @@ var _ = Describe("Legacy Options", func() { opts.Providers[0].OIDCConfig.ExtraAudiences = []string{} opts.Providers[0].OIDCConfig.InsecureSkipNonce = ptr.To(true) opts.Providers[0].OIDCConfig.InsecureSkipIssuerVerification = ptr.To(false) + opts.Providers[0].OIDCConfig.EnabledSigningAlgs = []string{"RS256", "EdDSA"} opts.Providers[0].LoginURLParameters = []LoginURLParameter{ {Name: "approval_prompt", Default: []string{"force"}}, } diff --git a/pkg/apis/options/providers.go b/pkg/apis/options/providers.go index 55965ed9..6f115f8a 100644 --- a/pkg/apis/options/providers.go +++ b/pkg/apis/options/providers.go @@ -321,6 +321,11 @@ type OIDCOptions struct { // ExtraAudiences is a list of additional audiences that are allowed // to pass verification in addition to the client id. ExtraAudiences []string `yaml:"extraAudiences,omitempty"` + // EnabledSigningAlgs is a list of allowed JWT signing algorithms. + // When discovery is enabled, the effective set is the intersection + // between this list and the provider's discovered supported algorithms. + // By default `RS256` is used if nothing has been discovered or specified. + EnabledSigningAlgs []string `yaml:"enabledSigningAlgs,omitempty"` } type LoginGovOptions struct { diff --git a/pkg/providers/oidc/provider_verifier.go b/pkg/providers/oidc/provider_verifier.go index eac80a8c..0457a9dc 100644 --- a/pkg/providers/oidc/provider_verifier.go +++ b/pkg/providers/oidc/provider_verifier.go @@ -155,13 +155,48 @@ func getVerifierBuilder(ctx context.Context, opts ProviderVerifierOptions) (veri return nil, nil, fmt.Errorf("error while discovery OIDC configuration: %w", err) } + supportedSigningAlgs, err := intersectSigningAlgs(provider.SupportedSigningAlgs(), opts.SupportedSigningAlgs) + if err != nil { + return nil, nil, fmt.Errorf("error while determining supported signing algorithms: %w", err) + } + return newVerifierBuilder( opts.IssuerURL, oidc.NewRemoteKeySet(ctx, provider.Endpoints().JWKsURL), - provider.SupportedSigningAlgs(), + supportedSigningAlgs, ), provider, nil } +// intersectSigningAlgs returns the intersecting list of signing algorithms from the oidc discovery +// and the signing algorithms provided through the options. +func intersectSigningAlgs(discoveredSigningAlgs, configuredSigningAlgs []string) ([]string, error) { + if len(configuredSigningAlgs) == 0 { + return discoveredSigningAlgs, nil + } + + if len(discoveredSigningAlgs) == 0 { + return configuredSigningAlgs, nil + } + + discovered := make(map[string]struct{}, len(discoveredSigningAlgs)) + for _, signingAlg := range discoveredSigningAlgs { + discovered[signingAlg] = struct{}{} + } + + intersection := make([]string, 0, len(configuredSigningAlgs)) + for _, signingAlg := range configuredSigningAlgs { + if _, ok := discovered[signingAlg]; ok { + intersection = append(intersection, signingAlg) + } + } + + if len(intersection) == 0 { + return nil, fmt.Errorf("no supported signing algorithms in common between provider and configuration: discovered=%v, configured=%v", discoveredSigningAlgs, configuredSigningAlgs) + } + + return intersection, 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) { diff --git a/pkg/providers/oidc/provider_verifier_test.go b/pkg/providers/oidc/provider_verifier_test.go index ff91e016..b575a64a 100644 --- a/pkg/providers/oidc/provider_verifier_test.go +++ b/pkg/providers/oidc/provider_verifier_test.go @@ -2,6 +2,9 @@ package oidc import ( "context" + "encoding/json" + "net" + "net/http" "os" "path/filepath" "time" @@ -195,6 +198,21 @@ var _ = Describe("ProviderVerifier", func() { Expect(idToken.Subject).To(Equal(claims.Subject)) }, Entry("with the default opts and claims", &verifierTableInput{}), + Entry("with skip discovery and an allowed signing algorithm", &verifierTableInput{ + modifyOpts: func(p *ProviderVerifierOptions) { + p.SkipDiscovery = true + p.JWKsURL = m.JWKSEndpoint() + p.SupportedSigningAlgs = []string{"RS256"} + }, + }), + Entry("with skip discovery and a disallowed signing algorithm", &verifierTableInput{ + modifyOpts: func(p *ProviderVerifierOptions) { + p.SkipDiscovery = true + p.JWKsURL = m.JWKSEndpoint() + p.SupportedSigningAlgs = []string{"HS256"} + }, + expectedError: "failed to verify token: oidc: malformed jwt: unexpected signature algorithm \"RS256\"; expected [\"HS256\"]", + }), Entry("when the audience is mismatched", &verifierTableInput{ modifyClaims: func(j *jwt.RegisteredClaims) { j.Audience = jwt.ClaimStrings{"OtherClient"} @@ -230,4 +248,78 @@ var _ = Describe("ProviderVerifier", func() { expectedError: "failed to verify token: oidc: token is expired", }), ) + + Describe("intersectSigningAlgs", func() { + DescribeTable("when determining allowed signing algorithms", func(discoveredSigningAlgs, configuredSigningAlgs, expected []string, expectedError string) { + actual, err := intersectSigningAlgs(discoveredSigningAlgs, configuredSigningAlgs) + Expect(actual).To(Equal(expected)) + if len(expectedError) > 0 { + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal(expectedError)) + } + }, + Entry("returns discovered values when no configured values are provided", []string{"RS256", "HS256"}, []string(nil), []string{"RS256", "HS256"}, ""), + Entry("returns configured values when no discovered values are provided", []string(nil), []string{"RS256"}, []string{"RS256"}, ""), + Entry("returns the configured order of the intersection", []string{"RS256", "HS256", "EdDSA"}, []string{"EdDSA", "RS256"}, []string{"EdDSA", "RS256"}, ""), + 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]"), + ) + }) + + It("uses the intersection between discovered and configured signing algorithms", func() { + customServer, err := mockoidc.NewServer(nil) + Expect(err).ToNot(HaveOccurred()) + customServer.AddMiddleware(newConfiguredSigningAlgsIssuerMiddleware(customServer, []string{"RS256", "HS256"})) + + listener, err := net.Listen("tcp", "127.0.0.1:0") + Expect(err).ToNot(HaveOccurred()) + + Expect(customServer.Start(listener, nil)).To(Succeed()) + defer func() { + Expect(customServer.Shutdown()).To(Succeed()) + }() + + pv, err := NewProviderVerifier(context.Background(), ProviderVerifierOptions{ + AudienceClaims: []string{"aud"}, + ClientID: customServer.Config().ClientID, + ExtraAudiences: []string{}, + IssuerURL: customServer.Issuer(), + SupportedSigningAlgs: []string{"HS256"}, + }) + Expect(err).ToNot(HaveOccurred()) + + rawIDToken, err := customServer.Keypair.SignJWT(jwt.RegisteredClaims{ + Audience: jwt.ClaimStrings{customServer.Config().ClientID}, + Issuer: customServer.Issuer(), + ExpiresAt: jwt.NewNumericDate(time.Now().Add(1 * time.Hour)), + IssuedAt: jwt.NewNumericDate(time.Now()), + Subject: "user", + }) + Expect(err).ToNot(HaveOccurred()) + + _, err = pv.Verifier().Verify(context.Background(), rawIDToken) + Expect(err).To(MatchError(HavePrefix("failed to verify token: oidc: malformed jwt: unexpected signature algorithm \"RS256\"; expected [\"HS256\"]"))) + }) }) + +func newConfiguredSigningAlgsIssuerMiddleware(m *mockoidc.MockOIDC, supportedSigningAlgs []string) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + provider := providerJSON{ + Issuer: m.Issuer(), + AuthURL: m.AuthorizationEndpoint(), + TokenURL: m.TokenEndpoint(), + JWKsURL: m.JWKSEndpoint(), + UserInfoURL: m.UserinfoEndpoint(), + SupportedSigningAlgs: supportedSigningAlgs, + } + + data, err := json.Marshal(provider) + if err != nil { + rw.WriteHeader(http.StatusInternalServerError) + return + } + + _, _ = rw.Write(data) + }) + } +} diff --git a/pkg/validation/providers.go b/pkg/validation/providers.go index 9e62e98a..ecc3277a 100644 --- a/pkg/validation/providers.go +++ b/pkg/validation/providers.go @@ -4,10 +4,27 @@ import ( "fmt" "os" + jose "github.com/go-jose/go-jose/v4" "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options" "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util/ptr" ) +var supportedOIDCSigningAlgorithms = map[jose.SignatureAlgorithm]struct{}{ + jose.EdDSA: {}, + jose.HS256: {}, + jose.HS384: {}, + jose.HS512: {}, + jose.RS256: {}, + jose.RS384: {}, + jose.RS512: {}, + jose.ES256: {}, + jose.ES384: {}, + jose.ES512: {}, + jose.PS256: {}, + jose.PS384: {}, + jose.PS512: {}, +} + // validateProviders is the initial validation migration for multiple providrers // It currently includes only logic that can verify the providers one by one and does not break the valdation pipe func validateProviders(o *options.Options) []string { @@ -59,6 +76,22 @@ func validateProvider(provider options.Provider, providerIDs map[string]struct{} msgs = append(msgs, validateEntraConfig(provider)...) } + msgs = append(msgs, validateOIDCSigningAlgorithms(provider)...) + + return msgs +} + +func validateOIDCSigningAlgorithms(provider options.Provider) []string { + msgs := []string{} + + for _, algorithm := range provider.OIDCConfig.EnabledSigningAlgs { + if _, ok := supportedOIDCSigningAlgorithms[jose.SignatureAlgorithm(algorithm)]; ok { + continue + } + + msgs = append(msgs, fmt.Sprintf("provider %s has invalid EnabledSigningAlgs entry %q", provider.ID, algorithm)) + } + return msgs } diff --git a/pkg/validation/providers_test.go b/pkg/validation/providers_test.go index 065eb305..3c3531d7 100644 --- a/pkg/validation/providers_test.go +++ b/pkg/validation/providers_test.go @@ -18,6 +18,33 @@ var _ = Describe("Providers", func() { ClientSecret: "ClientSecret", } + validOIDCSigningAlgorithmsProvider := options.Provider{ + ID: "ProviderIDOIDCSigningAlgorithms", + ClientID: "ClientID", + ClientSecret: "ClientSecret", + OIDCConfig: options.OIDCOptions{ + EnabledSigningAlgs: []string{"RS256", "EdDSA"}, + }, + } + + invalidOIDCSigningAlgorithmsProvider := options.Provider{ + ID: "ProviderIDInvalidOIDCSigningAlgorithms", + ClientID: "ClientID", + ClientSecret: "ClientSecret", + OIDCConfig: options.OIDCOptions{ + EnabledSigningAlgs: []string{"RS256", "invalid"}, + }, + } + + invalidOIDCSigningAlgorithmCaseProvider := options.Provider{ + ID: "ProviderIDInvalidOIDCSigningAlgorithmCase", + ClientID: "ClientID", + ClientSecret: "ClientSecret", + OIDCConfig: options.OIDCOptions{ + EnabledSigningAlgs: []string{"rs256"}, + }, + } + validLoginGovProvider := options.Provider{ Type: "login.gov", ID: "ProviderIDLoginGov", @@ -34,6 +61,8 @@ var _ = Describe("Providers", func() { emptyIDMsg := "provider has empty id: ids are required for all providers" duplicateProviderIDMsg := "multiple providers found with id ProviderID: provider ids must be unique" skipButtonAndMultipleProvidersMsg := "SkipProviderButton and multiple providers are mutually exclusive" + invalidOIDCSigningAlgorithmMsg := "provider ProviderIDInvalidOIDCSigningAlgorithms has invalid EnabledSigningAlgs entry \"invalid\"" + invalidOIDCSigningAlgorithmCaseMsg := "provider ProviderIDInvalidOIDCSigningAlgorithmCase has invalid EnabledSigningAlgs entry \"rs256\"" DescribeTable("validateProviders", func(o *validateProvidersTableInput) { @@ -79,5 +108,29 @@ var _ = Describe("Providers", func() { }, errStrings: []string{skipButtonAndMultipleProvidersMsg}, }), + Entry("with valid OIDC signing algorithms", &validateProvidersTableInput{ + options: &options.Options{ + Providers: options.Providers{ + validOIDCSigningAlgorithmsProvider, + }, + }, + errStrings: []string{}, + }), + Entry("with an invalid OIDC signing algorithm", &validateProvidersTableInput{ + options: &options.Options{ + Providers: options.Providers{ + invalidOIDCSigningAlgorithmsProvider, + }, + }, + errStrings: []string{invalidOIDCSigningAlgorithmMsg}, + }), + Entry("with an OIDC signing algorithm using invalid casing", &validateProvidersTableInput{ + options: &options.Options{ + Providers: options.Providers{ + invalidOIDCSigningAlgorithmCaseProvider, + }, + }, + errStrings: []string{invalidOIDCSigningAlgorithmCaseMsg}, + }), ) }) diff --git a/providers/providers.go b/providers/providers.go index 85c45ac5..f87d26a2 100644 --- a/providers/providers.go +++ b/providers/providers.go @@ -100,6 +100,7 @@ func newProviderDataFromConfig(providerConfig options.Provider) (*ProviderData, IssuerURL: providerConfig.OIDCConfig.IssuerURL, JWKsURL: providerConfig.OIDCConfig.JwksURL, PublicKeyFiles: providerConfig.OIDCConfig.PublicKeyFiles, + SupportedSigningAlgs: providerConfig.OIDCConfig.EnabledSigningAlgs, SkipDiscovery: ptr.Deref(providerConfig.OIDCConfig.SkipDiscovery, options.DefaultSkipDiscovery), SkipIssuerVerification: ptr.Deref(providerConfig.OIDCConfig.InsecureSkipIssuerVerification, options.DefaultInsecureSkipIssuerVerification), }) diff --git a/providers/providers_test.go b/providers/providers_test.go index 8e3b8d77..a0e7fd7e 100644 --- a/providers/providers_test.go +++ b/providers/providers_test.go @@ -1,9 +1,14 @@ package providers import ( + "context" + "net" "os" "testing" + "time" + "github.com/golang-jwt/jwt/v5" + "github.com/oauth2-proxy/mockoidc" "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options" "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util/ptr" . "github.com/onsi/gomega" @@ -121,6 +126,91 @@ func TestURLsCorrectlyParsed(t *testing.T) { g.Expect(pd.RedeemURL.String()).To(Equal(msTokenURL)) } +func TestEnabledSigningAlgsAreAppliedToProviderVerifier(t *testing.T) { + g := NewWithT(t) + + m, err := mockoidc.NewServer(nil) + g.Expect(err).ToNot(HaveOccurred()) + m.AddMiddleware(newSigningAlgsIssuerMiddleware(m, []string{"RS256", "HS256"})) + + listener, err := net.Listen("tcp", "127.0.0.1:0") + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(m.Start(listener, nil)).To(Succeed()) + defer func() { + g.Expect(m.Shutdown()).To(Succeed()) + }() + + providerConfig := options.Provider{ + ID: providerID, + Type: "oidc", + ClientID: m.Config().ClientID, + ClientSecretFile: clientSecret, + OIDCConfig: options.OIDCOptions{ + IssuerURL: m.Issuer(), + AudienceClaims: []string{"aud"}, + EnabledSigningAlgs: []string{"HS256"}, + }, + } + + pd, err := newProviderDataFromConfig(providerConfig) + g.Expect(err).ToNot(HaveOccurred()) + + rawIDToken, err := m.Keypair.SignJWT(jwt.RegisteredClaims{ + Audience: jwt.ClaimStrings{m.Config().ClientID}, + Issuer: m.Issuer(), + ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)), + IssuedAt: jwt.NewNumericDate(time.Now()), + Subject: "user", + }) + g.Expect(err).ToNot(HaveOccurred()) + + _, err = pd.Verifier.Verify(context.Background(), rawIDToken) + g.Expect(err).To(HaveOccurred()) + g.Expect(err.Error()).To(ContainSubstring("unexpected signature algorithm")) +} + +func TestEnabledSigningAlgsRejectUnsupportedTokens(t *testing.T) { + g := NewWithT(t) + + m, err := mockoidc.Run() + g.Expect(err).ToNot(HaveOccurred()) + defer func() { + g.Expect(m.Shutdown()).To(Succeed()) + }() + + providerConfig := options.Provider{ + ID: providerID, + Type: "oidc", + ClientID: m.Config().ClientID, + ClientSecretFile: clientSecret, + LoginURL: m.AuthorizationEndpoint(), + RedeemURL: m.TokenEndpoint(), + OIDCConfig: options.OIDCOptions{ + IssuerURL: m.Issuer(), + SkipDiscovery: ptr.To(true), + JwksURL: m.JWKSEndpoint(), + AudienceClaims: []string{"aud"}, + EnabledSigningAlgs: []string{"HS256"}, + }, + } + + pd, err := newProviderDataFromConfig(providerConfig) + g.Expect(err).ToNot(HaveOccurred()) + + rawIDToken, err := m.Keypair.SignJWT(jwt.RegisteredClaims{ + Audience: jwt.ClaimStrings{m.Config().ClientID}, + Issuer: m.Issuer(), + ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)), + IssuedAt: jwt.NewNumericDate(time.Now()), + Subject: "user", + }) + g.Expect(err).ToNot(HaveOccurred()) + + _, err = pd.Verifier.Verify(context.Background(), rawIDToken) + g.Expect(err).To(HaveOccurred()) + g.Expect(err.Error()).To(ContainSubstring("unexpected signature algorithm")) +} + func TestScope(t *testing.T) { g := NewWithT(t) diff --git a/providers/util_test.go b/providers/util_test.go index e14ff061..5e5ccbf2 100644 --- a/providers/util_test.go +++ b/providers/util_test.go @@ -1,9 +1,12 @@ package providers import ( + "encoding/json" "fmt" + "net/http" "testing" + "github.com/oauth2-proxy/mockoidc" . "github.com/onsi/gomega" "golang.org/x/oauth2" ) @@ -111,3 +114,33 @@ func Test_formatGroup(t *testing.T) { }) } } + +func newSigningAlgsIssuerMiddleware(m *mockoidc.MockOIDC, supportedSigningAlgs []string) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + provider := struct { + Issuer string `json:"issuer"` + AuthURL string `json:"authorization_endpoint"` + TokenURL string `json:"token_endpoint"` + JWKsURL string `json:"jwks_uri"` + UserInfoURL string `json:"userinfo_endpoint"` + SupportedSigningAlgs []string `json:"id_token_signing_alg_values_supported"` + }{ + Issuer: m.Issuer(), + AuthURL: m.AuthorizationEndpoint(), + TokenURL: m.TokenEndpoint(), + JWKsURL: m.JWKSEndpoint(), + UserInfoURL: m.UserinfoEndpoint(), + SupportedSigningAlgs: supportedSigningAlgs, + } + + data, err := json.Marshal(provider) + if err != nil { + rw.WriteHeader(http.StatusInternalServerError) + return + } + + _, _ = rw.Write(data) + }) + } +}