You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
131 lines
5.6 KiB
131 lines
5.6 KiB
package api
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"tera-clone/services/tera-api/internal/adapters/mock"
|
|
"tera-clone/services/tera-api/internal/authcaptcha"
|
|
"tera-clone/services/tera-api/internal/domain"
|
|
)
|
|
|
|
type captchaServiceStub struct {
|
|
createdFor string
|
|
verifiedFor string
|
|
verifiedID string
|
|
verifiedAns string
|
|
}
|
|
|
|
func (s *captchaServiceStub) Create(subject string) (authcaptcha.Challenge, error) {
|
|
s.createdFor = subject
|
|
return authcaptcha.Challenge{
|
|
ChallengeID: "challenge-1",
|
|
ImageDataURL: "data:image/png;base64,iVBORw0KGgo=",
|
|
ExpiresAt: fixedClock().Add(2 * time.Minute),
|
|
}, nil
|
|
}
|
|
|
|
func (s *captchaServiceStub) Verify(subject, challengeID, answer string) bool {
|
|
s.verifiedFor, s.verifiedID, s.verifiedAns = subject, challengeID, answer
|
|
return subject == "192.0.2.44" && challengeID == "challenge-1" && answer == "23456789"
|
|
}
|
|
|
|
func TestLoginPolicyDefaultsAndDisabledCaptchaEndpoint(t *testing.T) {
|
|
handler := NewRouter(mock.NewStore(fixedClock))
|
|
policy := getJSON(t, handler, http.MethodGet, "/api/v1/auth/login-policy", "")
|
|
if policy["captcha_enabled"] != false || policy["login_detection_minutes"] != float64(0) || policy["login_security_hit_count"] != float64(0) {
|
|
t.Fatalf("default login policy = %#v", policy)
|
|
}
|
|
if policy["fixed_safety_threshold"] != float64(5) || policy["fixed_safety_minutes"] != float64(15) {
|
|
t.Fatalf("fixed login safety policy = %#v", policy)
|
|
}
|
|
policyResponse := httptest.NewRecorder()
|
|
handler.ServeHTTP(policyResponse, httptest.NewRequest(http.MethodGet, "/api/v1/auth/login-policy", nil))
|
|
if policyResponse.Header().Get("Cache-Control") != "no-store" {
|
|
t.Fatalf("login policy cache control=%q", policyResponse.Header().Get("Cache-Control"))
|
|
}
|
|
|
|
response := httptest.NewRecorder()
|
|
handler.ServeHTTP(response, httptest.NewRequest(http.MethodPost, "/api/v1/auth/captcha", nil))
|
|
if response.Code != http.StatusConflict {
|
|
t.Fatalf("disabled captcha endpoint status=%d body=%s", response.Code, response.Body.String())
|
|
}
|
|
assertErrorCode(t, response, "captcha_disabled")
|
|
}
|
|
|
|
func TestEnabledCaptchaIsPeerBoundAndRequiredBeforeCredentialVerification(t *testing.T) {
|
|
store := mock.NewStore(fixedClock)
|
|
enabled, detection, hits := true, 9, 4
|
|
if _, err := store.UpdateManagementAccess(context.Background(), domain.ManagementAccessUpdate{
|
|
CaptchaEnabled: &enabled,
|
|
LoginDetectionMinutes: &detection,
|
|
LoginSecurityHitCount: &hits,
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := store.ApplyConfig(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
captcha := &captchaServiceStub{}
|
|
handler := NewRouterWithOptions(store, RouterOptions{CaptchaService: captcha})
|
|
|
|
policy := getJSON(t, handler, http.MethodGet, "/api/v1/auth/login-policy", "")
|
|
if policy["captcha_enabled"] != true || policy["login_detection_minutes"] != float64(9) || policy["login_security_hit_count"] != float64(4) {
|
|
t.Fatalf("applied login policy = %#v", policy)
|
|
}
|
|
|
|
challengeResponse := httptest.NewRecorder()
|
|
challengeRequest := httptest.NewRequest(http.MethodPost, "/api/v1/auth/captcha", nil)
|
|
challengeRequest.RemoteAddr = "127.0.0.1:41000"
|
|
challengeRequest.Header.Set("X-Forwarded-For", "192.0.2.44")
|
|
handler.ServeHTTP(challengeResponse, challengeRequest)
|
|
if challengeResponse.Code != http.StatusOK || captcha.createdFor != "192.0.2.44" || !strings.Contains(challengeResponse.Body.String(), "challenge-1") {
|
|
t.Fatalf("challenge status=%d subject=%q body=%s", challengeResponse.Code, captcha.createdFor, challengeResponse.Body.String())
|
|
}
|
|
|
|
missingResponse := httptest.NewRecorder()
|
|
missingRequest := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", strings.NewReader(`{"username":"admin","password":"mock-admin-secret"}`))
|
|
missingRequest.RemoteAddr = "127.0.0.1:41001"
|
|
missingRequest.Header.Set("X-Forwarded-For", "192.0.2.44")
|
|
handler.ServeHTTP(missingResponse, missingRequest)
|
|
if missingResponse.Code != http.StatusUnauthorized {
|
|
t.Fatalf("login without captcha status=%d body=%s", missingResponse.Code, missingResponse.Body.String())
|
|
}
|
|
assertErrorCode(t, missingResponse, "captcha_failed")
|
|
|
|
loginResponse := httptest.NewRecorder()
|
|
loginRequest := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", strings.NewReader(`{"username":"admin","password":"mock-admin-secret","captcha_id":"challenge-1","captcha_answer":"23456789"}`))
|
|
loginRequest.RemoteAddr = "127.0.0.1:41002"
|
|
loginRequest.Header.Set("X-Forwarded-For", "192.0.2.44")
|
|
handler.ServeHTTP(loginResponse, loginRequest)
|
|
if loginResponse.Code != http.StatusOK {
|
|
t.Fatalf("login with captcha status=%d body=%s", loginResponse.Code, loginResponse.Body.String())
|
|
}
|
|
if captcha.verifiedFor != "192.0.2.44" || captcha.verifiedID != "challenge-1" || captcha.verifiedAns != "23456789" {
|
|
t.Fatalf("captcha verification = subject=%q id=%q answer=%q", captcha.verifiedFor, captcha.verifiedID, captcha.verifiedAns)
|
|
}
|
|
}
|
|
|
|
func TestCaptchaPeerSubjectTrustsOnlySingleAddressFromLoopbackGateway(t *testing.T) {
|
|
request := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
request.RemoteAddr = "127.0.0.1:1234"
|
|
request.Header.Set("X-Forwarded-For", "198.51.100.20")
|
|
if got := requestPeerSubject(request); got != "198.51.100.20" {
|
|
t.Fatalf("loopback gateway subject=%q", got)
|
|
}
|
|
|
|
request.Header.Set("X-Forwarded-For", "203.0.113.9, 198.51.100.20")
|
|
if got := requestPeerSubject(request); got != "127.0.0.1" {
|
|
t.Fatalf("forwarded chain subject=%q, want loopback peer", got)
|
|
}
|
|
|
|
request.RemoteAddr = "192.0.2.77:4321"
|
|
request.Header.Set("X-Forwarded-For", "198.51.100.20")
|
|
if got := requestPeerSubject(request); got != "192.0.2.77" {
|
|
t.Fatalf("direct peer subject=%q, want direct peer", got)
|
|
}
|
|
}
|
|
|