mirror of
https://github.com/netbirdio/netbird.git
synced 2026-07-17 20:19:55 +00:00
Fix broken auth redirect
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,6 +2,7 @@
|
||||
.run
|
||||
*.iml
|
||||
dist/
|
||||
!proxy/web/dist/
|
||||
bin/
|
||||
.env
|
||||
conf.json
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package reverseproxy
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func validProxy() *ReverseProxy {
|
||||
return &ReverseProxy{
|
||||
Name: "test",
|
||||
Domain: "example.com",
|
||||
Targets: []Target{
|
||||
{TargetId: "peer-1", TargetType: TargetTypePeer, Host: "10.0.0.1", Port: 80, Protocol: "http", Enabled: true},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidate_Valid(t *testing.T) {
|
||||
require.NoError(t, validProxy().Validate())
|
||||
}
|
||||
|
||||
func TestValidate_EmptyName(t *testing.T) {
|
||||
rp := validProxy()
|
||||
rp.Name = ""
|
||||
assert.ErrorContains(t, rp.Validate(), "name is required")
|
||||
}
|
||||
|
||||
func TestValidate_EmptyDomain(t *testing.T) {
|
||||
rp := validProxy()
|
||||
rp.Domain = ""
|
||||
assert.ErrorContains(t, rp.Validate(), "domain is required")
|
||||
}
|
||||
|
||||
func TestValidate_NoTargets(t *testing.T) {
|
||||
rp := validProxy()
|
||||
rp.Targets = nil
|
||||
assert.ErrorContains(t, rp.Validate(), "at least one target")
|
||||
}
|
||||
|
||||
func TestValidate_EmptyTargetId(t *testing.T) {
|
||||
rp := validProxy()
|
||||
rp.Targets[0].TargetId = ""
|
||||
assert.ErrorContains(t, rp.Validate(), "empty target_id")
|
||||
}
|
||||
|
||||
func TestValidate_InvalidTargetType(t *testing.T) {
|
||||
rp := validProxy()
|
||||
rp.Targets[0].TargetType = "invalid"
|
||||
assert.ErrorContains(t, rp.Validate(), "invalid target_type")
|
||||
}
|
||||
|
||||
func TestValidate_ResourceTarget(t *testing.T) {
|
||||
rp := validProxy()
|
||||
rp.Targets = append(rp.Targets, Target{
|
||||
TargetId: "resource-1",
|
||||
TargetType: TargetTypeResource,
|
||||
Host: "example.org",
|
||||
Port: 443,
|
||||
Protocol: "https",
|
||||
Enabled: true,
|
||||
})
|
||||
require.NoError(t, rp.Validate())
|
||||
}
|
||||
|
||||
func TestValidate_MultipleTargetsOneInvalid(t *testing.T) {
|
||||
rp := validProxy()
|
||||
rp.Targets = append(rp.Targets, Target{
|
||||
TargetId: "",
|
||||
TargetType: TargetTypePeer,
|
||||
Host: "10.0.0.2",
|
||||
Port: 80,
|
||||
Protocol: "http",
|
||||
Enabled: true,
|
||||
})
|
||||
err := rp.Validate()
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "target 1")
|
||||
assert.Contains(t, err.Error(), "empty target_id")
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
10
proxy/web/dist/assets/index.js
vendored
10
proxy/web/dist/assets/index.js
vendored
File diff suppressed because one or more lines are too long
2
proxy/web/dist/assets/style.css
vendored
2
proxy/web/dist/assets/style.css
vendored
File diff suppressed because one or more lines are too long
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user