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)
+ })
+ }
+}