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:
Justin Ryan
2025-04-21 13:40:39 +01:00
committed by GitHub
co-authored by Justin Ryan
parent bb6ff4ed14
commit 8abdbb5a18
7 changed files with 137 additions and 41 deletions
+13 -6
View File
@@ -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 == "" {
+80 -1
View File
@@ -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() {