Fix broken auth redirect

This commit is contained in:
Viktor Liu
2026-02-08 23:51:36 +08:00
parent 7f11e3205d
commit 260c46df04
7 changed files with 131 additions and 25 deletions

View File

@@ -98,8 +98,7 @@ func (mw *Middleware) Protect(next http.Handler) http.Handler {
for _, scheme := range config.Schemes {
token, promptData := scheme.Authenticate(r)
if token != "" {
userid, _, err := auth.ValidateSessionJWT(token, host, config.SessionPublicKey)
if err != nil {
if _, _, err := auth.ValidateSessionJWT(token, host, config.SessionPublicKey); err != nil {
if cd := proxy.CapturedDataFromContext(r.Context()); cd != nil {
cd.SetOrigin(proxy.OriginAuth)
}
@@ -120,9 +119,12 @@ func (mw *Middleware) Protect(next http.Handler) http.Handler {
MaxAge: int(expiration.Seconds()),
})
ctx := withAuthMethod(r.Context(), scheme.Type())
ctx = withAuthUser(ctx, userid)
next.ServeHTTP(w, r.WithContext(ctx))
// Redirect instead of forwarding the auth POST to the backend.
// The browser will follow with a GET carrying the new session cookie.
if cd := proxy.CapturedDataFromContext(r.Context()); cd != nil {
cd.SetOrigin(proxy.OriginAuth)
}
http.Redirect(w, r, r.URL.RequestURI(), http.StatusSeeOther)
return
}
methods[scheme.Type().String()] = promptData
@@ -131,7 +133,7 @@ func (mw *Middleware) Protect(next http.Handler) http.Handler {
if cd := proxy.CapturedDataFromContext(r.Context()); cd != nil {
cd.SetOrigin(proxy.OriginAuth)
}
web.ServeHTTP(w, r, map[string]any{"methods": methods})
web.ServeHTTP(w, r, map[string]any{"methods": methods}, http.StatusUnauthorized)
})
}

View File

@@ -332,7 +332,7 @@ func TestProtect_WrongKeyCookieIsRejected(t *testing.T) {
assert.False(t, backendCalled, "cookie signed by wrong key should be rejected")
}
func TestProtect_SchemeAuthSetsSessionCookie(t *testing.T) {
func TestProtect_SchemeAuthRedirectsWithCookie(t *testing.T) {
mw := NewMiddleware(log.StandardLogger())
kp := generateTestKeyPair(t)
@@ -350,20 +350,23 @@ func TestProtect_SchemeAuthSetsSessionCookie(t *testing.T) {
}
require.NoError(t, mw.AddDomain("example.com", []Scheme{scheme}, kp.PublicKey, time.Hour))
handler := mw.Protect(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "pin-user", UserFromContext(r.Context()))
assert.Equal(t, auth.MethodPIN, MethodFromContext(r.Context()))
var backendCalled bool
backend := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
backendCalled = true
w.WriteHeader(http.StatusOK)
}))
})
handler := mw.Protect(backend)
// Submit the PIN via form POST.
form := url.Values{"pin": {"111111"}}
req := httptest.NewRequest(http.MethodPost, "http://example.com/", strings.NewReader(form.Encode()))
req := httptest.NewRequest(http.MethodPost, "http://example.com/somepath", strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
assert.Equal(t, http.StatusOK, rec.Code)
assert.False(t, backendCalled, "backend should not be called during auth, only a redirect should be returned")
assert.Equal(t, http.StatusSeeOther, rec.Code)
assert.Equal(t, "/somepath", rec.Header().Get("Location"), "redirect should point to the original request URI")
cookies := rec.Result().Cookies()
var sessionCookie *http.Cookie
@@ -427,10 +430,12 @@ func TestProtect_MultipleSchemes(t *testing.T) {
}
require.NoError(t, mw.AddDomain("example.com", []Scheme{pinScheme, passwordScheme}, kp.PublicKey, time.Hour))
handler := mw.Protect(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, auth.MethodPassword, MethodFromContext(r.Context()))
var backendCalled bool
backend := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
backendCalled = true
w.WriteHeader(http.StatusOK)
}))
})
handler := mw.Protect(backend)
form := url.Values{"password": {"secret"}}
req := httptest.NewRequest(http.MethodPost, "http://example.com/", strings.NewReader(form.Encode()))
@@ -438,7 +443,8 @@ func TestProtect_MultipleSchemes(t *testing.T) {
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
assert.Equal(t, http.StatusOK, rec.Code)
assert.False(t, backendCalled, "backend should not be called during auth")
assert.Equal(t, http.StatusSeeOther, rec.Code)
}
func TestProtect_InvalidTokenFromSchemeReturns400(t *testing.T) {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -62,10 +62,11 @@ function App() {
fetch(window.location.href, {
method: "POST",
body: formData,
redirect: "follow",
redirect: "manual",
})
.then((res) => {
if (res.ok || res.redirected) {
if (res.type === "opaqueredirect" || res.status === 0) {
setSubmitting("redirect");
window.location.reload();
} else {
handleAuthError(method, "Authentication failed. Please try again.");
@@ -92,6 +93,21 @@ function App() {
const hasCredentialAuth = methods.password || methods.pin;
const hasBothCredentials = methods.password && methods.pin;
if (submitting === "redirect") {
return (
<main className="mt-20">
<Card className="max-w-105 mx-auto">
<Title>Authenticated</Title>
<Description>Loading service...</Description>
<div className="flex justify-center mt-7">
<Loader2 className="animate-spin" size={24} />
</div>
</Card>
<PoweredByNetBird />
</main>
);
}
return (
<main className="mt-20">
<Card className="max-w-105 mx-auto">