mirror of
https://github.com/oauth2-proxy/oauth2-proxy.git
synced 2025-05-19 22:23:30 +02:00
Add --bearer-token-login-fallback option (#2924)
* add --deny-invalid-bearer-tokens * update changelog * PR feedback, update api-routes description * update --api-routes description * revert load_test fix that I needed locally --------- Co-authored-by: Justin Ryan <j.ryan@mwam.com>
This commit is contained in:
parent
bb6ff4ed14
commit
8abdbb5a18
@ -32,6 +32,7 @@
|
||||
|
||||
## Changes since v7.8.1
|
||||
|
||||
- [#2918](https://github.com/oauth2-proxy/oauth2-proxy/issues/2918) feat: add --bearer-token-login-fallback (@carillonator)
|
||||
- [#2927](https://github.com/oauth2-proxy/oauth2-proxy/pull/2927) chore(deps/build): bump golang to 1.23 and use go.mod as single point of truth for all build files (@tuunit)
|
||||
- [#2697](https://github.com/oauth2-proxy/oauth2-proxy/pull/2697) Use `Max-Age` instead of `Expires` for cookie expiration (@matpen-wi)
|
||||
- [#2969](https://github.com/oauth2-proxy/oauth2-proxy/pull/2969) Update golang.org/x/oauth2 to v0.27.0 to address CVE-2025-22868 (@dsymonds)
|
||||
|
@ -191,8 +191,9 @@ Provider specific options can be found on their respective subpages.
|
||||
| Flag / Config Field | Type | Description | Default |
|
||||
| ------------------------------------------------------------------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
|
||||
| flag: `--allow-query-semicolons`<br/>toml: `allow_query_semicolons` | bool | allow the use of semicolons in query args ([required for some legacy applications](https://github.com/golang/go/issues/25192)) | `false` |
|
||||
| flag: `--api-route`<br/>toml: `api_routes` | string \| list | return HTTP 401 instead of redirecting to authentication server if token is not valid. Format: path_regex | |
|
||||
| flag: `--api-route`<br/>toml: `api_routes` | string \| list | Requests to these paths must already be authenticated with a cookie, or a JWT if `--skip-jwt-bearer-tokens` is set. No redirect to login will be done. Return 401 if not. Format: path_regex | |
|
||||
| flag: `--authenticated-emails-file`<br/>toml: `authenticated_emails_file` | string | authenticate against emails via file (one per line) | |
|
||||
| flag: `--bearer-token-login-fallback`<br/>toml: `bearer_token_login_fallback` | bool | if `--skip-jwt-bearer-tokens` is set, if a request includes an invalid JWT (expired, malformed, missing required audiences, etc), fall back to normal login redirect as if the token were not sent at all. If false, respond 403 | true |
|
||||
| flag: `--email-domain`<br/>toml: `email_domains` | string \| list | authenticate emails with the specified domain (may be given multiple times). Use `*` to authenticate any email | |
|
||||
| flag: `--encode-state`<br/>toml: `encode_state` | bool | encode the state parameter as UrlEncodedBase64 | false |
|
||||
| flag: `--extra-jwt-issuers`<br/>toml: `extra_jwt_issuers` | string | if `--skip-jwt-bearer-tokens` is set, a list of extra JWT `issuer=audience` (see a token's `iss`, `aud` fields) pairs (where the issuer URL has a `.well-known/openid-configuration` or a `.well-known/jwks.json`) | |
|
||||
|
@ -163,6 +163,10 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr
|
||||
for _, issuer := range opts.ExtraJwtIssuers {
|
||||
logger.Printf("Skipping JWT tokens from extra JWT issuer: %q", issuer)
|
||||
}
|
||||
if !opts.BearerTokenLoginFallback {
|
||||
logger.Println("Denying requests with invalid JWT tokens")
|
||||
}
|
||||
|
||||
}
|
||||
redirectURL := opts.GetRedirectURL()
|
||||
if redirectURL.Path == "" {
|
||||
@ -402,7 +406,7 @@ func buildSessionChain(opts *options.Options, provider providers.Provider, sessi
|
||||
middlewareapi.CreateTokenToSessionFunc(verifier.Verify))
|
||||
}
|
||||
|
||||
chain = chain.Append(middleware.NewJwtSessionLoader(sessionLoaders))
|
||||
chain = chain.Append(middleware.NewJwtSessionLoader(sessionLoaders, opts.BearerTokenLoginFallback))
|
||||
}
|
||||
|
||||
if validator != nil {
|
||||
|
@ -47,16 +47,17 @@ var _ = Describe("Load", func() {
|
||||
},
|
||||
|
||||
Options: Options{
|
||||
ProxyPrefix: "/oauth2",
|
||||
PingPath: "/ping",
|
||||
ReadyPath: "/ready",
|
||||
RealClientIPHeader: "X-Real-IP",
|
||||
ForceHTTPS: false,
|
||||
Cookie: cookieDefaults(),
|
||||
Session: sessionOptionsDefaults(),
|
||||
Templates: templatesDefaults(),
|
||||
SkipAuthPreflight: false,
|
||||
Logging: loggingDefaults(),
|
||||
BearerTokenLoginFallback: true,
|
||||
ProxyPrefix: "/oauth2",
|
||||
PingPath: "/ping",
|
||||
ReadyPath: "/ready",
|
||||
RealClientIPHeader: "X-Real-IP",
|
||||
ForceHTTPS: false,
|
||||
Cookie: cookieDefaults(),
|
||||
Session: sessionOptionsDefaults(),
|
||||
Templates: templatesDefaults(),
|
||||
SkipAuthPreflight: false,
|
||||
Logging: loggingDefaults(),
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -52,17 +52,18 @@ type Options struct {
|
||||
|
||||
Providers Providers `cfg:",internal"`
|
||||
|
||||
APIRoutes []string `flag:"api-route" cfg:"api_routes"`
|
||||
SkipAuthRegex []string `flag:"skip-auth-regex" cfg:"skip_auth_regex"`
|
||||
SkipAuthRoutes []string `flag:"skip-auth-route" cfg:"skip_auth_routes"`
|
||||
SkipJwtBearerTokens bool `flag:"skip-jwt-bearer-tokens" cfg:"skip_jwt_bearer_tokens"`
|
||||
ExtraJwtIssuers []string `flag:"extra-jwt-issuers" cfg:"extra_jwt_issuers"`
|
||||
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"`
|
||||
ForceJSONErrors bool `flag:"force-json-errors" cfg:"force_json_errors"`
|
||||
EncodeState bool `flag:"encode-state" cfg:"encode_state"`
|
||||
AllowQuerySemicolons bool `flag:"allow-query-semicolons" cfg:"allow_query_semicolons"`
|
||||
APIRoutes []string `flag:"api-route" cfg:"api_routes"`
|
||||
SkipAuthRegex []string `flag:"skip-auth-regex" cfg:"skip_auth_regex"`
|
||||
SkipAuthRoutes []string `flag:"skip-auth-route" cfg:"skip_auth_routes"`
|
||||
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"`
|
||||
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"`
|
||||
ForceJSONErrors bool `flag:"force-json-errors" cfg:"force_json_errors"`
|
||||
EncodeState bool `flag:"encode-state" cfg:"encode_state"`
|
||||
AllowQuerySemicolons bool `flag:"allow-query-semicolons" cfg:"allow_query_semicolons"`
|
||||
|
||||
SignatureKey string `flag:"signature-key" cfg:"signature_key"`
|
||||
GCPHealthChecks bool `flag:"gcp-healthchecks" cfg:"gcp_healthchecks"`
|
||||
@ -97,17 +98,18 @@ func (o *Options) SetRealClientIPParser(s ipapi.RealClientIPParser) { o.re
|
||||
// NewOptions constructs a new Options with defaulted values
|
||||
func NewOptions() *Options {
|
||||
return &Options{
|
||||
ProxyPrefix: "/oauth2",
|
||||
Providers: providerDefaults(),
|
||||
PingPath: "/ping",
|
||||
ReadyPath: "/ready",
|
||||
RealClientIPHeader: "X-Real-IP",
|
||||
ForceHTTPS: false,
|
||||
Cookie: cookieDefaults(),
|
||||
Session: sessionOptionsDefaults(),
|
||||
Templates: templatesDefaults(),
|
||||
SkipAuthPreflight: false,
|
||||
Logging: loggingDefaults(),
|
||||
BearerTokenLoginFallback: true,
|
||||
ProxyPrefix: "/oauth2",
|
||||
Providers: providerDefaults(),
|
||||
PingPath: "/ping",
|
||||
ReadyPath: "/ready",
|
||||
RealClientIPHeader: "X-Real-IP",
|
||||
ForceHTTPS: false,
|
||||
Cookie: cookieDefaults(),
|
||||
Session: sessionOptionsDefaults(),
|
||||
Templates: templatesDefaults(),
|
||||
SkipAuthPreflight: false,
|
||||
Logging: loggingDefaults(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -128,6 +130,7 @@ func NewFlagSet() *pflag.FlagSet {
|
||||
flagSet.Bool("skip-auth-preflight", false, "will skip authentication for OPTIONS requests")
|
||||
flagSet.Bool("ssl-insecure-skip-verify", false, "skip validation of certificates presented when using HTTPS providers")
|
||||
flagSet.Bool("skip-jwt-bearer-tokens", false, "will skip requests that have verified JWT bearer tokens (default false)")
|
||||
flagSet.Bool("bearer-token-login-fallback", true, "if skip-jwt-bearer-tokens is set, fall back to normal login redirect with an invalid JWT. If false, 403 instead")
|
||||
flagSet.Bool("force-json-errors", false, "will force JSON errors instead of HTTP error pages or redirects")
|
||||
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")
|
||||
|
@ -15,10 +15,11 @@ import (
|
||||
|
||||
const jwtRegexFormat = `^ey[a-zA-Z0-9_-]*\.ey[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]+$`
|
||||
|
||||
func NewJwtSessionLoader(sessionLoaders []middlewareapi.TokenToSessionFunc) alice.Constructor {
|
||||
func NewJwtSessionLoader(sessionLoaders []middlewareapi.TokenToSessionFunc, bearerTokenLoginFallback bool) alice.Constructor {
|
||||
js := &jwtSessionLoader{
|
||||
jwtRegex: regexp.MustCompile(jwtRegexFormat),
|
||||
sessionLoaders: sessionLoaders,
|
||||
jwtRegex: regexp.MustCompile(jwtRegexFormat),
|
||||
sessionLoaders: sessionLoaders,
|
||||
denyInvalidJWTs: !bearerTokenLoginFallback,
|
||||
}
|
||||
return js.loadSession
|
||||
}
|
||||
@ -26,14 +27,16 @@ func NewJwtSessionLoader(sessionLoaders []middlewareapi.TokenToSessionFunc) alic
|
||||
// jwtSessionLoader is responsible for loading sessions from JWTs in
|
||||
// Authorization headers.
|
||||
type jwtSessionLoader struct {
|
||||
jwtRegex *regexp.Regexp
|
||||
sessionLoaders []middlewareapi.TokenToSessionFunc
|
||||
jwtRegex *regexp.Regexp
|
||||
sessionLoaders []middlewareapi.TokenToSessionFunc
|
||||
denyInvalidJWTs bool
|
||||
}
|
||||
|
||||
// loadSession attempts to load a session from a JWT stored in an Authorization
|
||||
// header within the request.
|
||||
// If no authorization header is found, or the header is invalid, no session
|
||||
// will be loaded and the request will be passed to the next handler.
|
||||
// Or if the JWT is invalid and denyInvalidJWTs, return 403 now.
|
||||
// If a session was loaded by a previous handler, it will not be replaced.
|
||||
func (j *jwtSessionLoader) loadSession(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
@ -49,6 +52,10 @@ func (j *jwtSessionLoader) loadSession(next http.Handler) http.Handler {
|
||||
session, err := j.getJwtSession(req)
|
||||
if err != nil {
|
||||
logger.Errorf("Error retrieving session from token in Authorization header: %v", err)
|
||||
if j.denyInvalidJWTs {
|
||||
http.Error(rw, http.StatusText(http.StatusForbidden), http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Add the session to the scope if it was found
|
||||
@ -58,7 +65,7 @@ func (j *jwtSessionLoader) loadSession(next http.Handler) http.Handler {
|
||||
}
|
||||
|
||||
// getJwtSession loads a session based on a JWT token in the authorization header.
|
||||
// (see the config options skip-jwt-bearer-tokens and extra-jwt-issuers)
|
||||
// (see the config options skip-jwt-bearer-tokens, extra-jwt-issuers, and bearer-token-login-fallback)
|
||||
func (j *jwtSessionLoader) getJwtSession(req *http.Request) (*sessionsapi.SessionState, error) {
|
||||
auth := req.Header.Get("Authorization")
|
||||
if auth == "" {
|
||||
|
@ -92,6 +92,7 @@ Nnc3a3lGVWFCNUMxQnNJcnJMTWxka1dFaHluYmI4Ongtb2F1dGgtYmFzaWM=`
|
||||
authorizationHeader string
|
||||
existingSession *sessionsapi.SessionState
|
||||
expectedSession *sessionsapi.SessionState
|
||||
expectedStatus int
|
||||
}
|
||||
|
||||
DescribeTable("with an authorization header",
|
||||
@ -114,12 +115,13 @@ Nnc3a3lGVWFCNUMxQnNJcnJMTWxka1dFaHluYmI4Ongtb2F1dGgtYmFzaWM=`
|
||||
// Create the handler with a next handler that will capture the session
|
||||
// from the scope
|
||||
var gotSession *sessionsapi.SessionState
|
||||
handler := NewJwtSessionLoader(sessionLoaders)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
handler := NewJwtSessionLoader(sessionLoaders, true)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gotSession = middlewareapi.GetRequestScope(r).Session
|
||||
}))
|
||||
handler.ServeHTTP(rw, req)
|
||||
|
||||
Expect(gotSession).To(Equal(in.expectedSession))
|
||||
Expect(rw.Code).To(Equal(200))
|
||||
},
|
||||
Entry("<no value>", jwtSessionLoaderTableInput{
|
||||
authorizationHeader: "",
|
||||
@ -163,6 +165,83 @@ Nnc3a3lGVWFCNUMxQnNJcnJMTWxka1dFaHluYmI4Ongtb2F1dGgtYmFzaWM=`
|
||||
}),
|
||||
)
|
||||
|
||||
DescribeTable("with an authorization header, denyInvalidJWTs",
|
||||
func(in jwtSessionLoaderTableInput) {
|
||||
scope := &middlewareapi.RequestScope{
|
||||
Session: in.existingSession,
|
||||
}
|
||||
|
||||
// Set up the request with the authorization header and a request scope
|
||||
req := httptest.NewRequest("", "/", nil)
|
||||
req.Header.Set("Authorization", in.authorizationHeader)
|
||||
req = middlewareapi.AddRequestScope(req, scope)
|
||||
|
||||
rw := httptest.NewRecorder()
|
||||
|
||||
sessionLoaders := []middlewareapi.TokenToSessionFunc{
|
||||
middlewareapi.CreateTokenToSessionFunc(verifier),
|
||||
}
|
||||
|
||||
// Create the handler with a next handler that will capture the session
|
||||
// from the scope
|
||||
var gotSession *sessionsapi.SessionState
|
||||
handler := NewJwtSessionLoader(sessionLoaders, false)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gotSession = middlewareapi.GetRequestScope(r).Session
|
||||
}))
|
||||
handler.ServeHTTP(rw, req)
|
||||
|
||||
Expect(gotSession).To(Equal(in.expectedSession))
|
||||
Expect(rw.Code).To(Equal(in.expectedStatus))
|
||||
},
|
||||
Entry("<no value>", jwtSessionLoaderTableInput{
|
||||
authorizationHeader: "",
|
||||
existingSession: nil,
|
||||
expectedSession: nil,
|
||||
expectedStatus: 200,
|
||||
}),
|
||||
Entry("abcdef", jwtSessionLoaderTableInput{
|
||||
authorizationHeader: "abcdef",
|
||||
existingSession: nil,
|
||||
expectedSession: nil,
|
||||
expectedStatus: 403,
|
||||
}),
|
||||
Entry("abcdef (with existing session)", jwtSessionLoaderTableInput{
|
||||
authorizationHeader: "abcdef",
|
||||
existingSession: &sessionsapi.SessionState{User: "user"},
|
||||
expectedSession: &sessionsapi.SessionState{User: "user"},
|
||||
expectedStatus: 200,
|
||||
}),
|
||||
Entry("Bearer <verifiedToken>", jwtSessionLoaderTableInput{
|
||||
authorizationHeader: fmt.Sprintf("Bearer %s", verifiedToken),
|
||||
existingSession: nil,
|
||||
expectedSession: verifiedSession,
|
||||
expectedStatus: 200,
|
||||
}),
|
||||
Entry("Bearer <nonVerifiedToken>", jwtSessionLoaderTableInput{
|
||||
authorizationHeader: fmt.Sprintf("Bearer %s", nonVerifiedToken),
|
||||
existingSession: nil,
|
||||
expectedSession: nil,
|
||||
expectedStatus: 403,
|
||||
}),
|
||||
Entry("Bearer <verifiedToken> (with existing session)", jwtSessionLoaderTableInput{
|
||||
authorizationHeader: fmt.Sprintf("Bearer %s", verifiedToken),
|
||||
existingSession: &sessionsapi.SessionState{User: "user"},
|
||||
expectedSession: &sessionsapi.SessionState{User: "user"},
|
||||
expectedStatus: 200,
|
||||
}),
|
||||
Entry("Basic Base64(<nonVerifiedToken>:) (No password)", jwtSessionLoaderTableInput{
|
||||
authorizationHeader: "Basic ZXlKZm9vYmFyLmV5SmZvb2Jhci4xMjM0NWFzZGY6",
|
||||
existingSession: nil,
|
||||
expectedSession: nil,
|
||||
expectedStatus: 403,
|
||||
}),
|
||||
Entry("Basic Base64(<verifiedToken>:x-oauth-basic) (Sentinel password)", jwtSessionLoaderTableInput{
|
||||
authorizationHeader: fmt.Sprintf("Basic %s", verifiedTokenXOAuthBasicBase64),
|
||||
existingSession: nil,
|
||||
expectedSession: verifiedSession,
|
||||
expectedStatus: 200,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
Context("getJWTSession", func() {
|
||||
|
Loading…
x
Reference in New Issue
Block a user