* 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:
parent
30853098c7
commit
7c96234233
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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. |
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,7 @@ func NewLegacyOptions() *LegacyOptions {
|
||||||
OIDCGroupsClaim: "groups",
|
OIDCGroupsClaim: "groups",
|
||||||
OIDCAudienceClaims: []string{"aud"},
|
OIDCAudienceClaims: []string{"aud"},
|
||||||
OIDCExtraAudiences: []string{},
|
OIDCExtraAudiences: []string{},
|
||||||
|
OIDCEnabledSigningAlgs: []string{},
|
||||||
InsecureOIDCSkipNonce: true,
|
InsecureOIDCSkipNonce: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"}},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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},
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue