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>
This commit is contained in:
andoks 2026-03-18 15:24:27 +01:00 committed by GitHub
parent 30853098c7
commit 7c96234233
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 363 additions and 11 deletions

View File

@ -12,6 +12,7 @@
- [#3332](https://github.com/oauth2-proxy/oauth2-proxy/pull/3332) ci: distribute windows binary with .exe extension (@igitur) - [#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) - [#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) - [#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 # V7.14.3

View File

@ -488,6 +488,7 @@ character.
| `userIDClaim` | _string_ | UserIDClaim indicates which claim contains the user ID<br/>default set to 'email' | | `userIDClaim` | _string_ | UserIDClaim indicates which claim contains the user ID<br/>default set to 'email' |
| `audienceClaims` | _[]string_ | AudienceClaim allows to define any claim that is verified against the client id<br/>By default `aud` claim is used for verification. | | `audienceClaims` | _[]string_ | AudienceClaim allows to define any claim that is verified against the client id<br/>By default `aud` claim is used for verification. |
| `extraAudiences` | _[]string_ | ExtraAudiences is a list of additional audiences that are allowed<br/>to pass verification in addition to the client id. | | `extraAudiences` | _[]string_ | ExtraAudiences is a list of additional audiences that are allowed<br/>to pass verification in addition to the client id. |
| `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. |
### Provider ### Provider

View File

@ -99,7 +99,8 @@ Provider specific options can be found on their respective subpages.
| flag: `--oidc-groups-claim`<br/>toml: `oidc_groups_claim` | string | which OIDC claim contains the user groups | `"groups"` | | flag: `--oidc-groups-claim`<br/>toml: `oidc_groups_claim` | string | which OIDC claim contains the user groups | `"groups"` |
| flag: `--oidc-issuer-url`<br/>toml: `oidc_issuer_url` | string | the OpenID Connect issuer URL, e.g. `"https://accounts.google.com"` | | | flag: `--oidc-issuer-url`<br/>toml: `oidc_issuer_url` | string | the OpenID Connect issuer URL, e.g. `"https://accounts.google.com"` | |
| 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 | | | 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 | |
| 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 | | 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 | |
| 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. | |
| flag: `--profile-url`<br/>toml: `profile_url` | string | Profile access endpoint | | | flag: `--profile-url`<br/>toml: `profile_url` | string | Profile access endpoint | |
| 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 | `""` | | 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 | `""` |
| 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. | | 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. |

View File

@ -186,6 +186,7 @@ redirect_url="http://localhost:4180/oauth2/callback"
InsecureAllowUnverifiedEmail: ptr.To(false), InsecureAllowUnverifiedEmail: ptr.To(false),
InsecureSkipIssuerVerification: ptr.To(false), InsecureSkipIssuerVerification: ptr.To(false),
SkipDiscovery: ptr.To(false), SkipDiscovery: ptr.To(false),
EnabledSigningAlgs: []string{},
}, },
MicrosoftEntraIDConfig: options.MicrosoftEntraIDOptions{ MicrosoftEntraIDConfig: options.MicrosoftEntraIDOptions{
FederatedTokenAuth: ptr.To(false), FederatedTokenAuth: ptr.To(false),

View File

@ -51,15 +51,16 @@ func NewLegacyOptions() *LegacyOptions {
}, },
LegacyProvider: LegacyProvider{ LegacyProvider: LegacyProvider{
ProviderType: "google", ProviderType: "google",
AzureTenant: "common", AzureTenant: "common",
ApprovalPrompt: "force", ApprovalPrompt: "force",
UserIDClaim: "email", UserIDClaim: "email",
OIDCEmailClaim: "email", OIDCEmailClaim: "email",
OIDCGroupsClaim: "groups", OIDCGroupsClaim: "groups",
OIDCAudienceClaims: []string{"aud"}, OIDCAudienceClaims: []string{"aud"},
OIDCExtraAudiences: []string{}, OIDCExtraAudiences: []string{},
InsecureOIDCSkipNonce: true, OIDCEnabledSigningAlgs: []string{},
InsecureOIDCSkipNonce: true,
}, },
Options: *NewOptions(), Options: *NewOptions(),
@ -545,6 +546,7 @@ type LegacyProvider struct {
OIDCAudienceClaims []string `flag:"oidc-audience-claim" cfg:"oidc_audience_claims"` OIDCAudienceClaims []string `flag:"oidc-audience-claim" cfg:"oidc_audience_claims"`
OIDCExtraAudiences []string `flag:"oidc-extra-audience" cfg:"oidc_extra_audiences"` OIDCExtraAudiences []string `flag:"oidc-extra-audience" cfg:"oidc_extra_audiences"`
OIDCPublicKeyFiles []string `flag:"oidc-public-key-file" cfg:"oidc_public_key_files"` 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"` LoginURL string `flag:"login-url" cfg:"login_url"`
AuthRequestResponseMode string `flag:"auth-request-response-mode" cfg:"auth_request_response_mode"` AuthRequestResponseMode string `flag:"auth-request-response-mode" cfg:"auth_request_response_mode"`
RedeemURL string `flag:"redeem-url" cfg:"redeem_url"` 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-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-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-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("login-url", "", "Authentication endpoint")
flagSet.String("redeem-url", "", "Token redemption endpoint") flagSet.String("redeem-url", "", "Token redemption endpoint")
flagSet.String("profile-url", "", "Profile access endpoint") flagSet.String("profile-url", "", "Profile access endpoint")
@ -727,6 +730,7 @@ func (l *LegacyProvider) convert() (Providers, error) {
AudienceClaims: l.OIDCAudienceClaims, AudienceClaims: l.OIDCAudienceClaims,
ExtraAudiences: l.OIDCExtraAudiences, ExtraAudiences: l.OIDCExtraAudiences,
PublicKeyFiles: l.OIDCPublicKeyFiles, PublicKeyFiles: l.OIDCPublicKeyFiles,
EnabledSigningAlgs: l.OIDCEnabledSigningAlgs,
} }
// Support for legacy configuration option // Support for legacy configuration option

View File

@ -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.LegacyUpstreams.Upstreams = []string{"http://foo.bar/baz", "file:///var/lib/website#/bar", "static://204"}
legacyOpts.LegacyProvider.ClientID = "oauth-proxy" legacyOpts.LegacyProvider.ClientID = "oauth-proxy"
legacyOpts.LegacyUpstreams.DisableKeepAlives = false legacyOpts.LegacyUpstreams.DisableKeepAlives = false
legacyOpts.LegacyProvider.OIDCEnabledSigningAlgs = []string{"RS256", "EdDSA"}
staticCode := 204 staticCode := 204
opts.UpstreamServers = UpstreamConfig{ opts.UpstreamServers = UpstreamConfig{
@ -128,6 +129,7 @@ var _ = Describe("Legacy Options", func() {
opts.Providers[0].OIDCConfig.ExtraAudiences = []string{} opts.Providers[0].OIDCConfig.ExtraAudiences = []string{}
opts.Providers[0].OIDCConfig.InsecureSkipNonce = ptr.To(true) opts.Providers[0].OIDCConfig.InsecureSkipNonce = ptr.To(true)
opts.Providers[0].OIDCConfig.InsecureSkipIssuerVerification = ptr.To(false) opts.Providers[0].OIDCConfig.InsecureSkipIssuerVerification = ptr.To(false)
opts.Providers[0].OIDCConfig.EnabledSigningAlgs = []string{"RS256", "EdDSA"}
opts.Providers[0].LoginURLParameters = []LoginURLParameter{ opts.Providers[0].LoginURLParameters = []LoginURLParameter{
{Name: "approval_prompt", Default: []string{"force"}}, {Name: "approval_prompt", Default: []string{"force"}},
} }

View File

@ -321,6 +321,11 @@ type OIDCOptions struct {
// ExtraAudiences is a list of additional audiences that are allowed // ExtraAudiences is a list of additional audiences that are allowed
// to pass verification in addition to the client id. // to pass verification in addition to the client id.
ExtraAudiences []string `yaml:"extraAudiences,omitempty"` 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 { type LoginGovOptions struct {

View File

@ -155,13 +155,48 @@ func getVerifierBuilder(ctx context.Context, opts ProviderVerifierOptions) (veri
return nil, nil, fmt.Errorf("error while discovery OIDC configuration: %w", err) 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( return newVerifierBuilder(
opts.IssuerURL, opts.IssuerURL,
oidc.NewRemoteKeySet(ctx, provider.Endpoints().JWKsURL), oidc.NewRemoteKeySet(ctx, provider.Endpoints().JWKsURL),
provider.SupportedSigningAlgs(), supportedSigningAlgs,
), provider, nil ), 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 // GetPublicKeyFromBytes parses a PEM-encoded public key from a byte array
// and returns a crypto.PublicKey object. // and returns a crypto.PublicKey object.
func getPublicKeyFromBytes(bytes []byte) (crypto.PublicKey, error) { func getPublicKeyFromBytes(bytes []byte) (crypto.PublicKey, error) {

View File

@ -2,6 +2,9 @@ package oidc
import ( import (
"context" "context"
"encoding/json"
"net"
"net/http"
"os" "os"
"path/filepath" "path/filepath"
"time" "time"
@ -195,6 +198,21 @@ var _ = Describe("ProviderVerifier", func() {
Expect(idToken.Subject).To(Equal(claims.Subject)) Expect(idToken.Subject).To(Equal(claims.Subject))
}, },
Entry("with the default opts and claims", &verifierTableInput{}), 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{ Entry("when the audience is mismatched", &verifierTableInput{
modifyClaims: func(j *jwt.RegisteredClaims) { modifyClaims: func(j *jwt.RegisteredClaims) {
j.Audience = jwt.ClaimStrings{"OtherClient"} j.Audience = jwt.ClaimStrings{"OtherClient"}
@ -230,4 +248,78 @@ var _ = Describe("ProviderVerifier", func() {
expectedError: "failed to verify token: oidc: token is expired", 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)
})
}
}

View File

@ -4,10 +4,27 @@ import (
"fmt" "fmt"
"os" "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/apis/options"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util/ptr" "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 // 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 // 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 { 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, 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 return msgs
} }

View File

@ -18,6 +18,33 @@ var _ = Describe("Providers", func() {
ClientSecret: "ClientSecret", 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{ validLoginGovProvider := options.Provider{
Type: "login.gov", Type: "login.gov",
ID: "ProviderIDLoginGov", ID: "ProviderIDLoginGov",
@ -34,6 +61,8 @@ var _ = Describe("Providers", func() {
emptyIDMsg := "provider has empty id: ids are required for all providers" emptyIDMsg := "provider has empty id: ids are required for all providers"
duplicateProviderIDMsg := "multiple providers found with id ProviderID: provider ids must be unique" duplicateProviderIDMsg := "multiple providers found with id ProviderID: provider ids must be unique"
skipButtonAndMultipleProvidersMsg := "SkipProviderButton and multiple providers are mutually exclusive" 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", DescribeTable("validateProviders",
func(o *validateProvidersTableInput) { func(o *validateProvidersTableInput) {
@ -79,5 +108,29 @@ var _ = Describe("Providers", func() {
}, },
errStrings: []string{skipButtonAndMultipleProvidersMsg}, 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},
}),
) )
}) })

View File

@ -100,6 +100,7 @@ func newProviderDataFromConfig(providerConfig options.Provider) (*ProviderData,
IssuerURL: providerConfig.OIDCConfig.IssuerURL, IssuerURL: providerConfig.OIDCConfig.IssuerURL,
JWKsURL: providerConfig.OIDCConfig.JwksURL, JWKsURL: providerConfig.OIDCConfig.JwksURL,
PublicKeyFiles: providerConfig.OIDCConfig.PublicKeyFiles, PublicKeyFiles: providerConfig.OIDCConfig.PublicKeyFiles,
SupportedSigningAlgs: providerConfig.OIDCConfig.EnabledSigningAlgs,
SkipDiscovery: ptr.Deref(providerConfig.OIDCConfig.SkipDiscovery, options.DefaultSkipDiscovery), SkipDiscovery: ptr.Deref(providerConfig.OIDCConfig.SkipDiscovery, options.DefaultSkipDiscovery),
SkipIssuerVerification: ptr.Deref(providerConfig.OIDCConfig.InsecureSkipIssuerVerification, options.DefaultInsecureSkipIssuerVerification), SkipIssuerVerification: ptr.Deref(providerConfig.OIDCConfig.InsecureSkipIssuerVerification, options.DefaultInsecureSkipIssuerVerification),
}) })

View File

@ -1,9 +1,14 @@
package providers package providers
import ( import (
"context"
"net"
"os" "os"
"testing" "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/apis/options"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util/ptr" "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util/ptr"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
@ -121,6 +126,91 @@ func TestURLsCorrectlyParsed(t *testing.T) {
g.Expect(pd.RedeemURL.String()).To(Equal(msTokenURL)) 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) { func TestScope(t *testing.T) {
g := NewWithT(t) g := NewWithT(t)

View File

@ -1,9 +1,12 @@
package providers package providers
import ( import (
"encoding/json"
"fmt" "fmt"
"net/http"
"testing" "testing"
"github.com/oauth2-proxy/mockoidc"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
"golang.org/x/oauth2" "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)
})
}
}