618 lines
20 KiB
Go
618 lines
20 KiB
Go
package turnstile
|
|
|
|
import (
|
|
"context"
|
|
_ "embed"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/caddyserver/caddy/v2"
|
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
|
)
|
|
|
|
//go:embed settings.html
|
|
var forgejoSettingsHTML []byte
|
|
|
|
func TestSessionPassCookieRoundTrip(t *testing.T) {
|
|
signer, err := newSessionPassSigner("test-secret-key-please-change", "__turnstile_session_pass", time.Hour)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
rec := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "https://example.com/owner/repo/src/x", nil)
|
|
if err := signer.set(rec, req, "forgejo-session-token", "remember-token", "alice"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
req2 := httptest.NewRequest(http.MethodGet, "https://example.com/owner/repo/src/x", nil)
|
|
req2.AddCookie(rec.Result().Cookies()[0])
|
|
username, ok := signer.valid(req2, "forgejo-session-token", "")
|
|
if !ok || username != "alice" {
|
|
t.Fatalf("expected session pass cookie to validate with session alone, got %q ok=%v", username, ok)
|
|
}
|
|
username, ok = signer.valid(req2, "", "remember-token")
|
|
if !ok || username != "alice" {
|
|
t.Fatalf("expected session pass cookie to validate with remember-me alone, got %q ok=%v", username, ok)
|
|
}
|
|
}
|
|
|
|
func TestSessionPassCookieRejectsTokenMismatch(t *testing.T) {
|
|
signer, err := newSessionPassSigner("test-secret-key-please-change", "__turnstile_session_pass", time.Hour)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
rec := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "https://example.com/", nil)
|
|
if err := signer.set(rec, req, "old-session-token", "old-remember", "alice"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
req2 := httptest.NewRequest(http.MethodGet, "https://example.com/", nil)
|
|
req2.AddCookie(rec.Result().Cookies()[0])
|
|
if _, ok := signer.valid(req2, "new-session-token", "new-remember"); ok {
|
|
t.Fatal("cookie for different credentials must not validate")
|
|
}
|
|
// Changed session must reject even when remember-me still matches.
|
|
if _, ok := signer.valid(req2, "new-session-token", "old-remember"); ok {
|
|
t.Fatal("mismatched session must not fall through to remember-me")
|
|
}
|
|
}
|
|
|
|
func TestSessionPassCookieRejectsEmptyCredentials(t *testing.T) {
|
|
signer, err := newSessionPassSigner("test-secret-key-please-change", "__turnstile_session_pass", time.Hour)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
rec := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "https://example.com/", nil)
|
|
if err := signer.set(rec, req, "", "", "alice"); err == nil {
|
|
t.Fatal("expected set with empty credentials to fail")
|
|
}
|
|
if err := signer.set(rec, req, "session-token", "", "alice"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
req2 := httptest.NewRequest(http.MethodGet, "https://example.com/", nil)
|
|
req2.AddCookie(rec.Result().Cookies()[0])
|
|
if _, ok := signer.valid(req2, "", ""); ok {
|
|
t.Fatal("empty credentials must not validate")
|
|
}
|
|
if _, ok := signer.valid(req2, "", "unrelated-remember"); ok {
|
|
t.Fatal("empty stored remember binding must not match a present remember cookie")
|
|
}
|
|
}
|
|
|
|
func TestParseForgejoSettingsUser(t *testing.T) {
|
|
username, ok := parseForgejoSettingsUser(forgejoSettingsHTML)
|
|
if !ok || username != "alice" {
|
|
t.Fatalf("expected alice from settings.html, got %q ok=%v", username, ok)
|
|
}
|
|
}
|
|
|
|
func TestForgejoAuthorizationHeaderToken(t *testing.T) {
|
|
tests := []struct {
|
|
header string
|
|
want string
|
|
ok bool
|
|
}{
|
|
{"token abc123", "token abc123", true},
|
|
{"Bearer oauth-token", "Bearer oauth-token", true},
|
|
{"TOKEN abc123", "TOKEN abc123", true},
|
|
{"", "", false},
|
|
{"Basic dXNlcjpwYXNz", "", false},
|
|
{"token", "", false},
|
|
{"token ", "", false},
|
|
}
|
|
for _, tc := range tests {
|
|
req := httptest.NewRequest(http.MethodGet, "https://example.com/", nil)
|
|
if tc.header != "" {
|
|
req.Header.Set("Authorization", tc.header)
|
|
}
|
|
got, ok := forgejoAuthorizationHeaderToken(req)
|
|
if ok != tc.ok || got != tc.want {
|
|
t.Fatalf("forgejoAuthorizationHeaderToken(%q) = (%q, %v), want (%q, %v)", tc.header, got, ok, tc.want, tc.ok)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseForgejoAPIUser(t *testing.T) {
|
|
body, err := json.Marshal(map[string]any{"username": "alice", "id": 1})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
username, ok := parseForgejoAPIUser(body)
|
|
if !ok || username != "alice" {
|
|
t.Fatalf("expected alice, got %q ok=%v", username, ok)
|
|
}
|
|
if _, ok := parseForgejoAPIUser([]byte(`{"id":1}`)); ok {
|
|
t.Fatal("expected missing username to fail")
|
|
}
|
|
}
|
|
|
|
func TestVerifyForgejoAuthorizationToken(t *testing.T) {
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != forgejoUserPath {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
if r.Header.Get("Authorization") != "token valid-api-token" {
|
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"username": "alice", "id": 1})
|
|
}))
|
|
defer upstream.Close()
|
|
|
|
m := Middleware{
|
|
ForgejoCookieName: "i_like_gitea",
|
|
Upstream: upstream.URL,
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "https://example.com/owner/repo/src/x", nil)
|
|
req.Host = "example.com"
|
|
req.Header.Set("Authorization", "token valid-api-token")
|
|
username, ok, err := m.verifyForgejoAuthorizationToken(req, "token valid-api-token")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !ok || username != "alice" {
|
|
t.Fatalf("expected valid authorization token with user alice, got %q ok=%v", username, ok)
|
|
}
|
|
|
|
req2 := httptest.NewRequest(http.MethodGet, "https://example.com/owner/repo/src/x", nil)
|
|
username, ok, err = m.verifyForgejoAuthorizationToken(req2, "token invalid-api-token")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if ok {
|
|
t.Fatalf("expected invalid authorization token, got user %q", username)
|
|
}
|
|
}
|
|
|
|
func TestEnsureSessionUserIDPrefersAuthorizationHeader(t *testing.T) {
|
|
var seenPath string
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
seenPath = r.URL.Path
|
|
switch r.URL.Path {
|
|
case forgejoUserPath:
|
|
if r.Header.Get("Authorization") != "token valid-api-token" {
|
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"username": "bob", "id": 2})
|
|
case forgejoSettingsPath:
|
|
t.Fatal("session cookie verification must not run when Authorization header is present")
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer upstream.Close()
|
|
|
|
signer, err := newSessionPassSigner("test-secret-key-please-change", "__turnstile_session_pass", time.Hour)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
m := Middleware{
|
|
ForgejoCookieName: "i_like_gitea",
|
|
Upstream: upstream.URL,
|
|
sessionPasses: signer,
|
|
}
|
|
|
|
rec := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "https://example.com/owner/repo/src/x", nil)
|
|
req.Header.Set("Authorization", "token valid-api-token")
|
|
req.AddCookie(&http.Cookie{Name: "i_like_gitea", Value: "valid-session"})
|
|
vars := map[string]any{}
|
|
ctx := context.WithValue(req.Context(), caddyhttp.VarsCtxKey, vars)
|
|
repl := caddy.NewReplacer()
|
|
ctx = context.WithValue(ctx, caddy.ReplacerCtxKey, repl)
|
|
req = req.WithContext(ctx)
|
|
if !m.ensureSessionUserID(rec, req) {
|
|
t.Fatal("expected authorization header auth")
|
|
}
|
|
if seenPath != forgejoUserPath {
|
|
t.Fatalf("expected %s, got %q", forgejoUserPath, seenPath)
|
|
}
|
|
if vars["user_id"] != "bob" {
|
|
t.Fatalf("expected user_id bob in vars, got %#v", vars["user_id"])
|
|
}
|
|
|
|
for _, c := range rec.Result().Cookies() {
|
|
req.AddCookie(c)
|
|
}
|
|
seenPath = ""
|
|
if !m.ensureSessionUserID(rec, req) {
|
|
t.Fatal("expected cached authorization auth")
|
|
}
|
|
if seenPath != "" {
|
|
t.Fatal("expected pass cookie to skip upstream verification")
|
|
}
|
|
}
|
|
|
|
func TestEnsureSessionUserIDFromAuthorizationPassCookie(t *testing.T) {
|
|
signer, err := newSessionPassSigner("test-secret-key-please-change", "__turnstile_session_pass", time.Hour)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
m := Middleware{
|
|
ForgejoCookieName: "i_like_gitea",
|
|
sessionPasses: signer,
|
|
}
|
|
|
|
rec := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "https://example.com/", nil)
|
|
req.Header.Set("Authorization", "token api-token")
|
|
if err := signer.set(rec, req, "token api-token", "", "bob"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
req.AddCookie(rec.Result().Cookies()[0])
|
|
|
|
vars := map[string]any{}
|
|
ctx := context.WithValue(req.Context(), caddyhttp.VarsCtxKey, vars)
|
|
repl := caddy.NewReplacer()
|
|
ctx = context.WithValue(ctx, caddy.ReplacerCtxKey, repl)
|
|
req = req.WithContext(ctx)
|
|
if !m.ensureSessionUserID(rec, req) {
|
|
t.Fatal("expected authorization pass cookie auth")
|
|
}
|
|
if vars["user_id"] != "bob" {
|
|
t.Fatalf("expected user_id bob in vars, got %#v", vars["user_id"])
|
|
}
|
|
}
|
|
|
|
func TestEnsureSessionUserIDDisabledWithoutForgejoCookieName(t *testing.T) {
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
t.Fatal("forgejo upstream must not be called when forgejo auth is disabled")
|
|
}))
|
|
defer upstream.Close()
|
|
|
|
signer, err := newSessionPassSigner("test-secret-key-please-change", "__turnstile_session_pass", time.Hour)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
m := Middleware{
|
|
Upstream: upstream.URL,
|
|
sessionPasses: signer,
|
|
}
|
|
|
|
rec := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "https://example.com/", nil)
|
|
req.Header.Set("Authorization", "token valid-api-token")
|
|
req.AddCookie(&http.Cookie{Name: "i_like_gitea", Value: "valid-session"})
|
|
if m.ensureSessionUserID(rec, req) {
|
|
t.Fatal("expected forgejo auth to be disabled without forgejo_cookie_name")
|
|
}
|
|
}
|
|
|
|
func TestNormalizeUpstream(t *testing.T) {
|
|
tests := []struct {
|
|
in string
|
|
want string
|
|
}{
|
|
{"127.0.0.1:3000", "http://127.0.0.1:3000"},
|
|
{"http://127.0.0.1:3000", "http://127.0.0.1:3000"},
|
|
{"http://forgejo.internal:3000/", "http://forgejo.internal:3000"},
|
|
}
|
|
for _, tc := range tests {
|
|
got, err := normalizeUpstream(tc.in)
|
|
if err != nil {
|
|
t.Fatalf("normalizeUpstream(%q): %v", tc.in, err)
|
|
}
|
|
if got != tc.want {
|
|
t.Fatalf("normalizeUpstream(%q) = %q, want %q", tc.in, got, tc.want)
|
|
}
|
|
}
|
|
if _, err := normalizeUpstream(""); err == nil {
|
|
t.Fatal("expected empty upstream to be rejected")
|
|
}
|
|
if _, err := normalizeUpstream("http://127.0.0.1:3000/extra"); err == nil {
|
|
t.Fatal("expected path in upstream to be rejected")
|
|
}
|
|
}
|
|
|
|
func TestVerifyForgejoSession(t *testing.T) {
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != forgejoSettingsPath {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
c, err := r.Cookie("i_like_gitea")
|
|
if err != nil || c.Value != "valid-session" {
|
|
http.Redirect(w, r, "/user/login", http.StatusSeeOther)
|
|
return
|
|
}
|
|
_, _ = w.Write(forgejoSettingsHTML)
|
|
}))
|
|
defer upstream.Close()
|
|
|
|
m := Middleware{
|
|
ForgejoCookieName: "i_like_gitea",
|
|
Upstream: upstream.URL,
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "https://example.com/owner/repo/src/x", nil)
|
|
req.Host = "example.com"
|
|
req.AddCookie(&http.Cookie{Name: "i_like_gitea", Value: "valid-session"})
|
|
username, ok, err := m.verifyForgejoSession(req, "valid-session")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !ok || username != "alice" {
|
|
t.Fatalf("expected valid forgejo session with user alice, got %q ok=%v", username, ok)
|
|
}
|
|
|
|
req2 := httptest.NewRequest(http.MethodGet, "https://example.com/owner/repo/src/x", nil)
|
|
username, ok, err = m.verifyForgejoSession(req2, "invalid-session")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if ok {
|
|
t.Fatalf("expected invalid forgejo session, got user %q", username)
|
|
}
|
|
}
|
|
|
|
func TestVerifyForgejoSessionRejectsRedirect(t *testing.T) {
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
http.Redirect(w, r, "/user/login", http.StatusSeeOther)
|
|
}))
|
|
defer upstream.Close()
|
|
|
|
m := Middleware{
|
|
ForgejoCookieName: "i_like_gitea",
|
|
Upstream: upstream.URL,
|
|
}
|
|
req := httptest.NewRequest(http.MethodGet, "https://example.com/", nil)
|
|
_, ok, err := m.verifyForgejoSession(req, "valid-session")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if ok {
|
|
t.Fatal("expected redirect response to be treated as unauthenticated")
|
|
}
|
|
}
|
|
|
|
func TestVerifyForgejoSessionForwardsHost(t *testing.T) {
|
|
var seenHost string
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
seenHost = r.Host
|
|
_, _ = w.Write(forgejoSettingsHTML)
|
|
}))
|
|
defer upstream.Close()
|
|
|
|
m := Middleware{
|
|
ForgejoCookieName: "i_like_gitea",
|
|
Upstream: upstream.URL,
|
|
}
|
|
req := httptest.NewRequest(http.MethodGet, "https://git.example.com/", nil)
|
|
req.Host = "git.example.com"
|
|
_, ok, err := m.verifyForgejoSession(req, "valid-session")
|
|
if err != nil || !ok {
|
|
t.Fatalf("verifyForgejoSession() = ok=%v err=%v", ok, err)
|
|
}
|
|
if seenHost != "git.example.com" {
|
|
t.Fatalf("expected Host git.example.com, got %q", seenHost)
|
|
}
|
|
}
|
|
|
|
func TestEnsureSessionUserIDSetsCookie(t *testing.T) {
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != forgejoSettingsPath {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
_, _ = w.Write(forgejoSettingsHTML)
|
|
}))
|
|
defer upstream.Close()
|
|
|
|
signer, err := newSessionPassSigner("test-secret-key-please-change", "__turnstile_session_pass", time.Hour)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
m := Middleware{
|
|
ForgejoCookieName: "i_like_gitea",
|
|
Upstream: upstream.URL,
|
|
sessionPasses: signer,
|
|
}
|
|
|
|
rec := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "https://example.com/owner/repo/src/x", nil)
|
|
req.AddCookie(&http.Cookie{Name: "i_like_gitea", Value: "valid-session"})
|
|
if !m.ensureSessionUserID(rec, req) {
|
|
t.Fatal("expected session auth")
|
|
}
|
|
cookies := rec.Result().Cookies()
|
|
if len(cookies) != 1 || cookies[0].Name != "__turnstile_session_pass" {
|
|
t.Fatalf("expected session pass cookie, got %#v", cookies)
|
|
}
|
|
}
|
|
|
|
func TestEnsureSessionUserIDBeforeTurnstilePassCookie(t *testing.T) {
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != forgejoSettingsPath {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
_, _ = w.Write(forgejoSettingsHTML)
|
|
}))
|
|
defer upstream.Close()
|
|
|
|
sessionSigner, err := newSessionPassSigner("test-secret-key-please-change", "__turnstile_session_pass", time.Hour)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
passSigner, err := newCookieSigner("turnstile-secret", "__turnstile_pass", time.Hour)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
m := Middleware{
|
|
ForgejoCookieName: "i_like_gitea",
|
|
Upstream: upstream.URL,
|
|
sessionPasses: sessionSigner,
|
|
cookies: passSigner,
|
|
}
|
|
|
|
rec := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "https://example.com/", nil)
|
|
req.AddCookie(&http.Cookie{Name: "i_like_gitea", Value: "valid-session"})
|
|
if err := passSigner.set(rec, req); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
req.AddCookie(rec.Result().Cookies()[0])
|
|
|
|
vars := map[string]any{}
|
|
ctx := context.WithValue(req.Context(), caddyhttp.VarsCtxKey, vars)
|
|
repl := caddy.NewReplacer()
|
|
ctx = context.WithValue(ctx, caddy.ReplacerCtxKey, repl)
|
|
req = req.WithContext(ctx)
|
|
|
|
if !m.ensureSessionUserID(rec, req) {
|
|
t.Fatal("expected session auth")
|
|
}
|
|
if vars["user_id"] != "alice" {
|
|
t.Fatalf("expected user_id alice in vars, got %#v", vars["user_id"])
|
|
}
|
|
if id, _ := repl.Get("http.auth.user.id"); id != "alice" {
|
|
t.Fatalf("expected http.auth.user.id alice in replacer, got %#v", id)
|
|
}
|
|
}
|
|
|
|
func TestEnsureSessionUserIDFromPassCookie(t *testing.T) {
|
|
signer, err := newSessionPassSigner("test-secret-key-please-change", "__turnstile_session_pass", time.Hour)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
m := Middleware{
|
|
ForgejoCookieName: "i_like_gitea",
|
|
sessionPasses: signer,
|
|
}
|
|
|
|
rec := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "https://example.com/", nil)
|
|
req.AddCookie(&http.Cookie{Name: "i_like_gitea", Value: "forgejo-session-token"})
|
|
if err := signer.set(rec, req, "forgejo-session-token", "", "alice"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
req.AddCookie(rec.Result().Cookies()[0])
|
|
|
|
vars := map[string]any{}
|
|
ctx := context.WithValue(req.Context(), caddyhttp.VarsCtxKey, vars)
|
|
repl := caddy.NewReplacer()
|
|
ctx = context.WithValue(ctx, caddy.ReplacerCtxKey, repl)
|
|
req = req.WithContext(ctx)
|
|
m.ensureSessionUserID(rec, req)
|
|
|
|
if vars["user_id"] != "alice" {
|
|
t.Fatalf("expected user_id alice in vars, got %#v", vars["user_id"])
|
|
}
|
|
if id, _ := repl.Get("http.auth.user.id"); id != "alice" {
|
|
t.Fatalf("expected http.auth.user.id alice in replacer, got %#v", id)
|
|
}
|
|
}
|
|
|
|
func TestEnsureSessionUserIDFromRememberPassCookie(t *testing.T) {
|
|
signer, err := newSessionPassSigner("test-secret-key-please-change", "__turnstile_session_pass", time.Hour)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
m := Middleware{
|
|
ForgejoCookieName: "i_like_gitea",
|
|
ForgejoRememberCookieName: "persistent",
|
|
sessionPasses: signer,
|
|
}
|
|
|
|
rec := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "https://example.com/", nil)
|
|
if err := signer.set(rec, req, "forgejo-session-token", "remember-token", "alice"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Browser restart: session cookie gone, remember-me remains.
|
|
req2 := httptest.NewRequest(http.MethodGet, "https://example.com/", nil)
|
|
req2.AddCookie(rec.Result().Cookies()[0])
|
|
req2.AddCookie(&http.Cookie{Name: "persistent", Value: "remember-token"})
|
|
|
|
vars := map[string]any{}
|
|
ctx := context.WithValue(req2.Context(), caddyhttp.VarsCtxKey, vars)
|
|
repl := caddy.NewReplacer()
|
|
ctx = context.WithValue(ctx, caddy.ReplacerCtxKey, repl)
|
|
req2 = req2.WithContext(ctx)
|
|
if !m.ensureSessionUserID(httptest.NewRecorder(), req2) {
|
|
t.Fatal("expected remember-me pass cookie auth")
|
|
}
|
|
if vars["user_id"] != "alice" {
|
|
t.Fatalf("expected user_id alice in vars, got %#v", vars["user_id"])
|
|
}
|
|
}
|
|
|
|
func TestEnsureSessionUserIDReverifiesChangedSession(t *testing.T) {
|
|
var verifiedSessions []string
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != forgejoSettingsPath {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
c, err := r.Cookie("i_like_gitea")
|
|
if err != nil {
|
|
http.Redirect(w, r, "/user/login", http.StatusSeeOther)
|
|
return
|
|
}
|
|
verifiedSessions = append(verifiedSessions, c.Value)
|
|
_, _ = w.Write(forgejoSettingsHTML)
|
|
}))
|
|
defer upstream.Close()
|
|
|
|
signer, err := newSessionPassSigner("test-secret-key-please-change", "__turnstile_session_pass", time.Hour)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
m := Middleware{
|
|
ForgejoCookieName: "i_like_gitea",
|
|
ForgejoRememberCookieName: "persistent",
|
|
Upstream: upstream.URL,
|
|
sessionPasses: signer,
|
|
}
|
|
|
|
rec := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "https://example.com/", nil)
|
|
if err := signer.set(rec, req, "old-session", "remember-token", "alice"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
req2 := httptest.NewRequest(http.MethodGet, "https://example.com/", nil)
|
|
req2.AddCookie(rec.Result().Cookies()[0])
|
|
req2.AddCookie(&http.Cookie{Name: "i_like_gitea", Value: "new-session"})
|
|
req2.AddCookie(&http.Cookie{Name: "persistent", Value: "remember-token"})
|
|
|
|
vars := map[string]any{}
|
|
ctx := context.WithValue(req2.Context(), caddyhttp.VarsCtxKey, vars)
|
|
repl := caddy.NewReplacer()
|
|
ctx = context.WithValue(ctx, caddy.ReplacerCtxKey, repl)
|
|
req2 = req2.WithContext(ctx)
|
|
|
|
out := httptest.NewRecorder()
|
|
if !m.ensureSessionUserID(out, req2) {
|
|
t.Fatal("expected changed session to re-verify successfully")
|
|
}
|
|
if len(verifiedSessions) != 1 || verifiedSessions[0] != "new-session" {
|
|
t.Fatalf("expected upstream verify of new-session, got %#v", verifiedSessions)
|
|
}
|
|
if vars["user_id"] != "alice" {
|
|
t.Fatalf("expected user_id alice, got %#v", vars["user_id"])
|
|
}
|
|
passCookies := out.Result().Cookies()
|
|
if len(passCookies) != 1 || passCookies[0].Name != "__turnstile_session_pass" {
|
|
t.Fatalf("expected refreshed session pass cookie, got %#v", passCookies)
|
|
}
|
|
req3 := httptest.NewRequest(http.MethodGet, "https://example.com/", nil)
|
|
req3.AddCookie(passCookies[0])
|
|
if _, ok := signer.valid(req3, "new-session", "remember-token"); !ok {
|
|
t.Fatal("refreshed pass cookie should validate with new session")
|
|
}
|
|
}
|