This commit is contained in:
Dimitri John Ledkov 2026-04-08 15:34:40 -04:00 committed by GitHub
commit f5bc9cfba7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 115 additions and 13 deletions

View File

@ -8,6 +8,8 @@
## Changes since v7.15.1
- [#3396](https://github.com/oauth2-proxy/oauth2-proxy/pull/3396) feat: add support for specifying custom OIDC JWT Issuer Headers (@xnox)
# V7.15.1
## Release Highlights

View File

@ -24,7 +24,7 @@ _oauth2_proxy() {
COMPREPLY=( $(compgen -W 'X-Real-IP X-Forwarded-For X-ProxyUser-IP' -- ${cur}) )
return 0
;;
--@(http-address|https-address|redirect-url|upstream|basic-auth-password|skip-auth-regex|flush-interval|extra-jwt-issuers|email-domain|whitelist-domain|trusted-ip|keycloak-group|azure-tenant|bitbucket-team|bitbucket-repository|github-org|github-team|github-repo|github-token|gitlab-group|github-user|google-group|google-admin-email|google-service-account-json|client-id|client_secret|banner|footer|proxy-prefix|ping-path|ready-path|cookie-name|cookie-secret|cookie-domain|cookie-path|cookie-expire|cookie-refresh|cookie-samesite|redist-sentinel-master-name|redist-sentinel-connection-urls|redist-cluster-connection-urls|logging-max-size|logging-max-age|logging-max-backups|standard-logging-format|request-logging-format|exclude-logging-paths|auth-logging-format|oidc-issuer-url|oidc-jwks-url|login-url|redeem-url|profile-url|resource|validate-url|scope|approval-prompt|signature-key|acr-values|jwt-key|pubjwk-url|force-json-errors))
--@(http-address|https-address|redirect-url|upstream|basic-auth-password|skip-auth-regex|flush-interval|extra-jwt-issuers|extra-jwt-issuers-headers|email-domain|whitelist-domain|trusted-ip|keycloak-group|azure-tenant|bitbucket-team|bitbucket-repository|github-org|github-team|github-repo|github-token|gitlab-group|github-user|google-group|google-admin-email|google-service-account-json|client-id|client_secret|banner|footer|proxy-prefix|ping-path|ready-path|cookie-name|cookie-secret|cookie-domain|cookie-path|cookie-expire|cookie-refresh|cookie-samesite|redist-sentinel-master-name|redist-sentinel-connection-urls|redist-cluster-connection-urls|logging-max-size|logging-max-age|logging-max-backups|standard-logging-format|request-logging-format|exclude-logging-paths|auth-logging-format|oidc-issuer-url|oidc-jwks-url|login-url|redeem-url|profile-url|resource|validate-url|scope|approval-prompt|signature-key|acr-values|jwt-key|pubjwk-url|force-json-errors))
return 0
;;
esac

View File

@ -58,6 +58,7 @@ type Options struct {
SkipJwtBearerTokens bool `flag:"skip-jwt-bearer-tokens" cfg:"skip_jwt_bearer_tokens"`
BearerTokenLoginFallback bool `flag:"bearer-token-login-fallback" cfg:"bearer_token_login_fallback"`
ExtraJwtIssuers []string `flag:"extra-jwt-issuers" cfg:"extra_jwt_issuers"`
ExtraJwtIssuersHeaders []string `flag:"extra-jwt-issuers-headers" cfg:"extra_jwt_issuers_headers"`
SkipProviderButton bool `flag:"skip-provider-button" cfg:"skip_provider_button"`
SSLInsecureSkipVerify bool `flag:"ssl-insecure-skip-verify" cfg:"ssl_insecure_skip_verify"`
SkipAuthPreflight bool `flag:"skip-auth-preflight" cfg:"skip_auth_preflight"`
@ -135,6 +136,7 @@ func NewFlagSet() *pflag.FlagSet {
flagSet.Bool("encode-state", false, "will encode oauth state with base64")
flagSet.Bool("allow-query-semicolons", false, "allow the use of semicolons in query args")
flagSet.StringSlice("extra-jwt-issuers", []string{}, "if skip-jwt-bearer-tokens is set, a list of extra JWT issuer=audience pairs (where the issuer URL has a .well-known/openid-configuration or a .well-known/jwks.json)")
flagSet.StringSlice("extra-jwt-issuers-headers", []string{}, "Allows setting a header when .well-known/openid-configuration is called. specified as key=value")
flagSet.StringSlice("email-domain", []string{}, "authenticate emails with the specified domain (may be given multiple times). Use * to authenticate any email")
flagSet.StringSlice("whitelist-domain", []string{}, "allowed domains for redirection after authentication. Prefix domain with a . or a *. to allow subdomains (eg .example.com, *.example.com)")

View File

@ -47,7 +47,13 @@ type DiscoveryProvider interface {
// We implement this here as opposed to using oidc.Provider so that we can override the Issuer verification check.
// As we have our own verifier and fetch the userinfo separately, the rest of the oidc.Provider implementation is not
// useful to us.
func NewProvider(ctx context.Context, issuerURL string, skipIssuerVerification bool) (DiscoveryProvider, error) {
//
// Parameters:
// ctx: The context for the function execution.
// issuerURL: The URL of the OIDC issuer to perform discovery against.
// skipIssuerVerification: A boolean flag indicating whether to skip issuer verification.
// IssuerCustomHeaders: A map of custom headers to be used when calling the issuer for discovery.
func NewProvider(ctx context.Context, issuerURL string, skipIssuerVerification bool, issuerCustomHeaders map[string]string) (DiscoveryProvider, error) {
// go-oidc doesn't let us pass bypass the issuer check this in the oidc.NewProvider call
// (which uses discovery to get the URLs), so we'll do a quick check ourselves and if
// we get the URLs, we'll just use the non-discovery path.
@ -56,10 +62,17 @@ func NewProvider(ctx context.Context, issuerURL string, skipIssuerVerification b
var p providerJSON
requestURL := strings.TrimSuffix(issuerURL, "/") + "/.well-known/openid-configuration"
if err := requests.New(requestURL).WithContext(ctx).Do().UnmarshalInto(&p); err != nil {
return nil, fmt.Errorf("failed to discover OIDC configuration: %v", err)
request := requests.New(requestURL).
WithContext(ctx)
for key, value := range issuerCustomHeaders {
request = request.SetHeader(key, value)
}
if err := request.Do().UnmarshalInto(&p); err != nil {
return nil, fmt.Errorf("failed to discover OIDC configuration: %v", err)
}
if !skipIssuerVerification && p.Issuer != issuerURL {
return nil, fmt.Errorf("oidc: issuer did not match the issuer returned by provider, expected %q got %q", issuerURL, p.Issuer)
}

View File

@ -15,6 +15,7 @@ var _ = Describe("Provider", func() {
type newProviderTableInput struct {
skipIssuerVerification bool
expectedError string
customHeaders map[string]string
middlewares func(*mockoidc.MockOIDC) []func(http.Handler) http.Handler
}
@ -37,7 +38,11 @@ var _ = Describe("Provider", func() {
Expect(m.Shutdown()).To(Succeed())
}()
provider, err := NewProvider(context.Background(), m.Issuer(), in.skipIssuerVerification)
customHeaders := in.customHeaders
if customHeaders == nil {
customHeaders = make(map[string]string)
}
provider, err := NewProvider(context.Background(), m.Issuer(), in.skipIssuerVerification, customHeaders)
if in.expectedError != "" {
Expect(err).To(MatchError(HavePrefix(in.expectedError)))
return
@ -82,6 +87,15 @@ var _ = Describe("Provider", func() {
},
expectedError: "failed to discover OIDC configuration: unexpected status \"400\"",
}),
Entry("with custom headers, sends them in the discovery request", &newProviderTableInput{
skipIssuerVerification: false,
customHeaders: map[string]string{"X-Custom-Header": "custom-value"},
middlewares: func(m *mockoidc.MockOIDC) []func(http.Handler) http.Handler {
return []func(http.Handler) http.Handler{
newRequiredHeaderMiddleware("X-Custom-Header", "custom-value"),
}
},
}),
)
It("with code challenges supported on the provider, shold populate PKCE information", func() {
@ -97,7 +111,7 @@ var _ = Describe("Provider", func() {
Expect(m.Shutdown()).To(Succeed())
}()
provider, err := NewProvider(context.Background(), m.Issuer(), false)
provider, err := NewProvider(context.Background(), m.Issuer(), false, make(map[string]string))
Expect(err).ToNot(HaveOccurred())
Expect(provider.PKCE().CodeChallengeAlgs).To(ConsistOf("S256", "plain"))
@ -116,7 +130,7 @@ var _ = Describe("Provider", func() {
Expect(m.Shutdown()).To(Succeed())
}()
provider, err := NewProvider(context.Background(), m.Issuer(), false)
provider, err := NewProvider(context.Background(), m.Issuer(), false, make(map[string]string))
Expect(err).ToNot(HaveOccurred())
Expect(provider.SupportedSigningAlgs()).To(ConsistOf("RS256", "HS256"))
@ -189,3 +203,15 @@ func newBadRequestMiddleware() func(http.Handler) http.Handler {
})
}
}
func newRequiredHeaderMiddleware(key, value string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
if req.Header.Get(key) != value {
rw.WriteHeader(http.StatusUnauthorized)
return
}
next.ServeHTTP(rw, req)
})
}
}

View File

@ -38,6 +38,9 @@ type ProviderVerifierOptions struct {
// eg: https://accounts.google.com
IssuerURL string
// IssuerCustomHeaders defines optional header values to be set when calling the given provider.
IssuerCustomHeaders map[string]string
// JWKsURL is the OpenID Connect JWKS URL
// eg: https://www.googleapis.com/oauth2/v3/certs
JWKsURL string
@ -150,7 +153,7 @@ func getVerifierBuilder(ctx context.Context, opts ProviderVerifierOptions) (veri
), nil, nil
}
provider, err := NewProvider(ctx, opts.IssuerURL, opts.SkipIssuerVerification)
provider, err := NewProvider(ctx, opts.IssuerURL, opts.SkipIssuerVerification, opts.IssuerCustomHeaders)
if err != nil {
return nil, nil, fmt.Errorf("error while discovery OIDC configuration: %w", err)
}

View File

@ -57,11 +57,20 @@ func Validate(o *options.Options) error {
if len(o.ExtraJwtIssuers) > 0 {
var jwtIssuers []jwtIssuer
jwtIssuers, msgs = parseJwtIssuers(o.ExtraJwtIssuers, msgs)
var jwtIssuersHeaders map[string]string
if len(o.ExtraJwtIssuersHeaders) > 0 {
jwtIssuersHeaders, msgs = parseJwtIssuerHeader(o.ExtraJwtIssuersHeaders, msgs)
} else {
jwtIssuersHeaders = make(map[string]string) // Initialize an empty map if headers are not provided
}
for _, jwtIssuer := range jwtIssuers {
verifier, err := newVerifierFromJwtIssuer(
o.Providers[0].OIDCConfig.AudienceClaims,
o.Providers[0].OIDCConfig.ExtraAudiences,
jwtIssuer,
jwtIssuersHeaders,
)
if err != nil {
msgs = append(msgs, fmt.Sprintf("error building verifiers: %s", err))
@ -141,14 +150,61 @@ func parseJwtIssuers(issuers []string, msgs []string) ([]jwtIssuer, []string) {
return parsedIssuers, msgs
}
// parseJwtIssuerHeader takes in an array of header strings in the form of "headerKey=headerValue"
// and parses them to return a map of key-value pairs and any error messages.
//
// Parameters:
//
// headers: A slice of strings representing headerKey=headerValue entries.
// msgs: A slice of strings to collect error messages, if any.
//
// Returns:
//
// map[string]string: A map of key-value pairs extracted from the headers.
// []string: A slice of strings containing any error messages encountered during parsing.
//
// Description:
// This function parses the input headers and extracts key-value pairs from them.
// Each entry in the "headers" slice should be in the format "headerKey=headerValue".
// The function checks if each entry contains both a non-empty key and a non-empty value.
// If so, it adds the key-value pair to the resulting map. If any errors are encountered
// during parsing, they are appended to the "msgs" slice.
// The function returns the map of key-value pairs and the error messages.
func parseJwtIssuerHeader(headers []string, msgs []string) (map[string]string, []string) {
result := make(map[string]string)
if len(headers) == 0 {
msgs = append(msgs, "empty header array")
return result, msgs
}
for _, headerItem := range headers {
components := strings.SplitN(strings.TrimSpace(headerItem), "=", 2)
if len(components) != 2 {
msgs = append(msgs, fmt.Sprintf("invalid jwt issuer header format, expected header_name=header_value: %s", headerItem))
continue
}
key := strings.TrimSpace(components[0])
value := strings.TrimSpace(components[1])
if key != "" && value != "" {
// Add the non-empty key and value to the result map
result[key] = value
}
}
return result, msgs
}
// newVerifierFromJwtIssuer takes in issuer information in jwtIssuer info and returns
// a verifier for that issuer.
func newVerifierFromJwtIssuer(audienceClaims []string, extraAudiences []string, jwtIssuer jwtIssuer) (internaloidc.IDTokenVerifier, error) {
func newVerifierFromJwtIssuer(audienceClaims []string, extraAudiences []string, jwtIssuer jwtIssuer, jwtIssuersHeaders map[string]string) (internaloidc.IDTokenVerifier, error) {
pvOpts := internaloidc.ProviderVerifierOptions{
AudienceClaims: audienceClaims,
ClientID: jwtIssuer.audience,
ExtraAudiences: extraAudiences,
IssuerURL: jwtIssuer.issuerURI,
AudienceClaims: audienceClaims,
ClientID: jwtIssuer.audience,
ExtraAudiences: extraAudiences,
IssuerURL: jwtIssuer.issuerURI,
IssuerCustomHeaders: jwtIssuersHeaders,
}
pv, err := internaloidc.NewProviderVerifier(context.TODO(), pvOpts)