Files
Agent-System-Backend/internal/server/one_time_password_test.go
T
agent-exec-transport a3b67ee891
governance / governance (pull_request) Successful in 22s
ci / vet-and-test (pull_request) Successful in 1m15s
agent-exec transport 34e93ed2817e051b9b824abe27d8c08a/3eba67b6e84f
2026-10-20 01:45:06 +00:00

275 lines
13 KiB
Go

package server
import (
"encoding/json"
"net/http"
"strings"
"testing"
"agent-system-backend/internal/store"
)
func otpUser(t *testing.T, st *store.Store, owner, password string) {
t.Helper()
hash, err := store.HashPassword(password)
if err != nil {
t.Fatal(err)
}
if err := st.CreateAccount(owner, "", hash, false); err != nil {
t.Fatal(err)
}
if err := st.ClearMustChangePassword(owner); err != nil {
t.Fatal(err)
}
}
func issueOTP(t *testing.T, h http.Handler, admin, owner string) string {
t.Helper()
rec := do(h, "POST", "/api/admin/users/"+owner+"/one-time-password", `{}`, admin)
if rec.Code != http.StatusOK || rec.Header().Get("Cache-Control") != "no-store" {
t.Fatalf("issue one-time password: %d %s", rec.Code, rec.Body.String())
}
var out struct {
Owner string `json:"owner"`
ID string `json:"id"`
OneTimePassword string `json:"oneTimePassword"`
}
if err := json.Unmarshal(rec.Body.Bytes(), &out); err != nil || out.Owner != owner || out.ID == "" || out.OneTimePassword == "" {
t.Fatalf("issue response: %+v %v", out, err)
}
return out.OneTimePassword
}
func loginBody(owner, password string) string {
b, _ := json.Marshal(map[string]string{"owner": owner, "password": password})
return string(b)
}
func TestAdminOneTimePasswordLoginForcesReplacement(t *testing.T) {
srv, st := newAuthTestServer(t)
h := srv.Handler()
admin := bootAdmin(t, h, st)
otpUser(t, st, "olivia", "olivia-normal-1")
normal := do(h, "POST", "/api/auth/login", loginBody("olivia", "olivia-normal-1"), "")
if normal.Code != http.StatusOK {
t.Fatalf("normal login: %d %s", normal.Code, normal.Body.String())
}
otp := issueOTP(t, h, admin, "olivia")
if rec := do(h, "GET", "/api/me", "", cookieOf(normal)); rec.Code != http.StatusOK {
t.Fatalf("issuance revoked a normal session: %d", rec.Code)
}
login := do(h, "POST", "/api/auth/login", loginBody("olivia", otp), "")
body := login.Body.String()
if login.Code != http.StatusOK || !strings.Contains(body, `"sessionScope":"set_password_only"`) ||
!strings.Contains(body, `"passwordChangeMode":"one_time_password"`) || !strings.Contains(body, `"mustChangePassword":true`) {
t.Fatalf("one-time password login: %d %s", login.Code, body)
}
restricted := cookieOf(login)
if rec := do(h, "GET", "/api/projects", "", restricted); rec.Code != http.StatusForbidden {
t.Fatalf("restricted session reached business API: %d", rec.Code)
}
if rec := do(h, "GET", "/api/me", "", restricted); rec.Code != http.StatusOK || !strings.Contains(rec.Body.String(), `"passwordChangeMode":"one_time_password"`) {
t.Fatalf("me on restricted session: %d %s", rec.Code, rec.Body.String())
}
if rec := do(h, "POST", "/api/auth/login", loginBody("olivia", otp), ""); rec.Code != http.StatusUnauthorized {
t.Fatalf("one-time password reused: %d %s", rec.Code, rec.Body.String())
}
done := do(h, "POST", "/api/auth/password", `{"next":"olivia-replaced-2"}`, restricted)
if done.Code != http.StatusOK || !strings.Contains(done.Body.String(), `"reloginRequired":true`) {
t.Fatalf("replacement: %d %s", done.Code, done.Body.String())
}
if rec := do(h, "GET", "/api/me", "", cookieOf(normal)); rec.Code != http.StatusUnauthorized {
t.Fatalf("old normal session survived replacement: %d", rec.Code)
}
if rec := do(h, "POST", "/api/auth/login", loginBody("olivia", "olivia-normal-1"), ""); rec.Code != http.StatusUnauthorized {
t.Fatalf("old normal password survived replacement: %d", rec.Code)
}
fresh := do(h, "POST", "/api/auth/login", loginBody("olivia", "olivia-replaced-2"), "")
if fresh.Code != http.StatusOK || !strings.Contains(fresh.Body.String(), `"sessionScope":"full"`) || !strings.Contains(fresh.Body.String(), `"mustChangePassword":false`) {
t.Fatalf("replaced password login: %d %s", fresh.Code, fresh.Body.String())
}
}
func TestNormalLoginInvalidatesAdminOneTimePasswordWithoutForcedChange(t *testing.T) {
srv, st := newAuthTestServer(t)
h := srv.Handler()
admin := bootAdmin(t, h, st)
otpUser(t, st, "nora", "nora-normal-1")
otp := issueOTP(t, h, admin, "nora")
normal := do(h, "POST", "/api/auth/login", loginBody("nora", "nora-normal-1"), "")
if normal.Code != http.StatusOK || !strings.Contains(normal.Body.String(), `"sessionScope":"full"`) || !strings.Contains(normal.Body.String(), `"mustChangePassword":false`) {
t.Fatalf("normal login after issuance must stay full: %d %s", normal.Code, normal.Body.String())
}
if rec := do(h, "GET", "/api/projects", "", cookieOf(normal)); rec.Code != http.StatusOK {
t.Fatalf("normal session gated: %d", rec.Code)
}
if rec := do(h, "POST", "/api/auth/login", loginBody("nora", otp), ""); rec.Code != http.StatusUnauthorized {
t.Fatalf("one-time password survived normal login: %d %s", rec.Code, rec.Body.String())
}
}
func TestNormalLoginRevokesExistingOneTimePasswordRestrictedSession(t *testing.T) {
srv, st := newAuthTestServer(t)
h := srv.Handler()
admin := bootAdmin(t, h, st)
otpUser(t, st, "ivy", "ivy-normal-1")
otp := issueOTP(t, h, admin, "ivy")
restrictedLogin := do(h, "POST", "/api/auth/login", loginBody("ivy", otp), "")
if restrictedLogin.Code != http.StatusOK || !strings.Contains(restrictedLogin.Body.String(), `"passwordChangeMode":"one_time_password"`) {
t.Fatalf("otp restricted login: %d %s", restrictedLogin.Code, restrictedLogin.Body.String())
}
restricted := cookieOf(restrictedLogin)
normal := do(h, "POST", "/api/auth/login", loginBody("ivy", "ivy-normal-1"), "")
if normal.Code != http.StatusOK || !strings.Contains(normal.Body.String(), `"sessionScope":"full"`) {
t.Fatalf("normal login: %d %s", normal.Code, normal.Body.String())
}
if rec := do(h, "POST", "/api/auth/password", `{"next":"ivy-reset-2"}`, restricted); rec.Code != http.StatusUnauthorized {
t.Fatalf("restricted session survived normal login for password endpoint: %d %s", rec.Code, rec.Body.String())
}
if rec := do(h, "GET", "/api/me", "", restricted); rec.Code != http.StatusUnauthorized {
t.Fatalf("restricted session survived normal login for me endpoint: %d %s", rec.Code, rec.Body.String())
}
}
func TestAdminOneTimePasswordIssuanceBoundaries(t *testing.T) {
srv, st := newAuthTestServer(t)
h := srv.Handler()
admin := bootAdmin(t, h, st)
otpUser(t, st, "bob", "bob-normal-01")
otpUser(t, st, "carl", "carl-normal-1")
bob := do(h, "POST", "/api/auth/login", loginBody("bob", "bob-normal-01"), "")
if bob.Code != http.StatusOK {
t.Fatalf("bob login: %d", bob.Code)
}
if rec := do(h, "POST", "/api/admin/users/carl/one-time-password", `{}`, cookieOf(bob)); rec.Code != http.StatusForbidden {
t.Fatalf("non-admin issued: %d %s", rec.Code, rec.Body.String())
}
if rec := do(h, "POST", "/api/admin/users/missing/one-time-password", `{}`, admin); rec.Code != http.StatusNotFound {
t.Fatalf("missing target: %d %s", rec.Code, rec.Body.String())
}
if rec := do(h, "POST", "/api/admin/users/carl/one-time-password", `{"password":"chosen"}`, admin); rec.Code != http.StatusBadRequest {
t.Fatalf("administrator-chosen password accepted: %d %s", rec.Code, rec.Body.String())
}
otp := issueOTP(t, h, admin, "carl")
if rec := do(h, "POST", "/api/auth/login", loginBody("bob", otp), ""); rec.Code != http.StatusUnauthorized {
t.Fatalf("one-time password used by another owner: %d", rec.Code)
}
if rec := do(h, "GET", "/api/projects", "", ""); rec.Code != http.StatusUnauthorized {
t.Fatalf("anonymous request: %d", rec.Code)
}
if rec := do(h, "POST", "/api/auth/login", loginBody("carl", otp), ""); rec.Code != http.StatusOK {
t.Fatalf("owner one-time login: %d %s", rec.Code, rec.Body.String())
}
}
func TestAdminOneTimePasswordInitializesPendingAccount(t *testing.T) {
srv, st := newAuthTestServer(t)
h := srv.Handler()
admin := bootAdmin(t, h, st)
account, err := st.CreatePendingAccount("admin", "pending", store.AccountProfile{})
if err != nil {
t.Fatal(err)
}
otp := issueOTP(t, h, admin, "pending")
login := do(h, "POST", "/api/auth/login", loginBody("pending", otp), "")
if login.Code != http.StatusOK || !strings.Contains(login.Body.String(), `"passwordChangeMode":"one_time_password"`) ||
!strings.Contains(login.Body.String(), `"sessionScope":"set_password_only"`) {
t.Fatalf("pending otp login: %d %s", login.Code, login.Body.String())
}
restricted := cookieOf(login)
if rec := do(h, "POST", "/api/auth/password", `{"next":"pending-pass-2"}`, restricted); rec.Code != http.StatusOK {
t.Fatalf("pending completion: %d %s", rec.Code, rec.Body.String())
}
fresh := do(h, "POST", "/api/auth/login", loginBody("pending", "pending-pass-2"), "")
if fresh.Code != http.StatusOK || !strings.Contains(fresh.Body.String(), `"sessionScope":"full"`) {
t.Fatalf("pending normal login: %d %s", fresh.Code, fresh.Body.String())
}
view := do(h, "GET", "/api/admin/users/"+account.ID, "", admin)
if view.Code != http.StatusOK || !strings.Contains(view.Body.String(), `"initializationState":"initialized"`) ||
!strings.Contains(view.Body.String(), `"passwordSet":true`) {
t.Fatalf("pending account state after completion: %d %s", view.Code, view.Body.String())
}
}
func TestAdminOneTimePasswordInitializesHistoricalPasswordlessAccount(t *testing.T) {
srv, st := newAuthTestServer(t)
h := srv.Handler()
admin := bootAdmin(t, h, st)
if err := st.CreateAccount("hank", "", "", false); err != nil {
t.Fatal(err)
}
account, err := st.GetAccountByOwner("hank")
if err != nil || account.HasPassword || !account.Enabled {
t.Fatalf("historical passwordless fixture: %+v %v", account, err)
}
if rec := do(h, "POST", "/api/auth/login", loginBody("hank", ""), ""); rec.Code != http.StatusUnauthorized {
t.Fatalf("passwordless account logged in without a credential: %d %s", rec.Code, rec.Body.String())
}
otp := issueOTP(t, h, admin, "hank")
login := do(h, "POST", "/api/auth/login", loginBody("hank", otp), "")
body := login.Body.String()
if login.Code != http.StatusOK || !strings.Contains(body, `"sessionScope":"set_password_only"`) ||
!strings.Contains(body, `"passwordChangeMode":"one_time_password"`) || !strings.Contains(body, `"mustChangePassword":true`) {
t.Fatalf("initialization login: %d %s", login.Code, body)
}
restricted := cookieOf(login)
if rec := do(h, "GET", "/api/projects", "", restricted); rec.Code != http.StatusForbidden {
t.Fatalf("initialization session reached business API: %d", rec.Code)
}
if rec := do(h, "GET", "/api/me/profile", "", restricted); rec.Code != http.StatusForbidden {
t.Fatalf("initialization session reached profile API: %d", rec.Code)
}
if rec := do(h, "POST", "/api/auth/password", `{"next":"short"}`, restricted); rec.Code != http.StatusBadRequest {
t.Fatalf("short initialization password accepted: %d", rec.Code)
}
done := do(h, "POST", "/api/auth/password", `{"next":"hank-real-pass-1"}`, restricted)
if done.Code != http.StatusOK || !strings.Contains(done.Body.String(), `"reloginRequired":true`) {
t.Fatalf("initialization password: %d %s", done.Code, done.Body.String())
}
if rec := do(h, "GET", "/api/me", "", restricted); rec.Code != http.StatusUnauthorized {
t.Fatalf("initialization session survived completion: %d", rec.Code)
}
view := do(h, "GET", "/api/admin/users/"+account.ID, "", admin)
if view.Code != http.StatusOK || !strings.Contains(view.Body.String(), `"passwordSet":true`) ||
!strings.Contains(view.Body.String(), `"initializationState":"initialized"`) {
t.Fatalf("admin view after initialization: %d %s", view.Code, view.Body.String())
}
fresh := do(h, "POST", "/api/auth/login", loginBody("hank", "hank-real-pass-1"), "")
if fresh.Code != http.StatusOK || !strings.Contains(fresh.Body.String(), `"sessionScope":"full"`) || !strings.Contains(fresh.Body.String(), `"mustChangePassword":false`) {
t.Fatalf("real password login: %d %s", fresh.Code, fresh.Body.String())
}
if rec := do(h, "GET", "/api/projects", "", cookieOf(fresh)); rec.Code != http.StatusOK {
t.Fatalf("initialized account gated: %d", rec.Code)
}
}
func TestRecoveryProposalRoutesAreRemoved(t *testing.T) {
srv, st := newAuthTestServer(t)
h := srv.Handler()
admin := bootAdmin(t, h, st)
account, err := st.GetAccountByOwner("admin")
if err != nil {
t.Fatal(err)
}
for _, route := range []struct{ method, path, body string }{
{"POST", "/api/admin/users/" + account.ID + "/recovery-proposals", `{"expectedStateVersion":1,"idempotencyKey":"k"}`},
{"GET", "/api/admin/users/" + account.ID + "/recovery-proposals", ""},
{"GET", "/api/me/recovery-proposals", ""},
{"POST", "/api/auth/recovery-proposals/rp_x/confirm", `{"expectedVersion":1}`},
{"POST", "/api/auth/recovery-proposals/rp_x/rotate-grant", `{"expectedVersion":1,"expectedGeneration":1}`},
{"POST", "/api/auth/recovery-grants/redeem", `{"key":"alk_x"}`},
{"POST", "/api/auth/login-key/redeem", `{"key":"alk_x"}`},
{"POST", "/api/admin/users/admin/login-keys", `{}`},
{"GET", "/api/admin/users/admin/login-keys", ""},
{"DELETE", "/api/admin/users/admin/login-keys/lk_x", ""},
{"POST", "/api/admin/users/admin/password", `{}`},
} {
if rec := do(h, route.method, route.path, route.body, admin); rec.Code != http.StatusNotFound && rec.Code != http.StatusMethodNotAllowed {
t.Fatalf("%s %s still served: %d %s", route.method, route.path, rec.Code, rec.Body.String())
}
}
if rec := do(h, "POST", "/api/auth/login-key/redeem", `{"key":"alk_x"}`, ""); rec.Code != http.StatusUnauthorized {
t.Fatalf("legacy redeem remained an unauthenticated entrance: %d", rec.Code)
}
}