1
0
forked from baron/baron-sso

headless password login 접속 이력 반영

This commit is contained in:
2026-04-14 10:49:11 +09:00
parent 772e3ed5e3
commit 92f8e9a61a
3 changed files with 301 additions and 3 deletions

View File

@@ -879,6 +879,129 @@ func TestHeadlessPasswordLogin_HeadlessLoginClientSuccess(t *testing.T) {
}
}
func TestHeadlessPasswordLogin_AuditIncludesClientMetadata(t *testing.T) {
mockIdp := new(MockIdentityProvider)
mockIdp.On("SignIn", "employee001", "password").Return(&domain.AuthInfo{
SessionToken: &domain.Token{JWT: "valid-jwt", SessionID: "session-123"},
Subject: "kratos-identity-id",
}, nil)
privateKey, jwks := mustHeadlessRSAJWK(t)
jwksBody, _ := json.Marshal(jwks)
hydraHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case strings.Contains(r.URL.Path, "/oauth2/auth/requests/login") && r.Method == http.MethodGet:
json.NewEncoder(w).Encode(domain.HydraLoginRequest{
Challenge: "challenge-123",
Client: domain.HydraClient{
ClientID: "headless-login-client",
ClientName: "Headless Login Portal",
TokenEndpointAuthMethod: "none",
Metadata: map[string]interface{}{
"status": "active",
"headless_login_enabled": true,
"headless_token_endpoint_auth_method": "private_key_jwt",
"headless_jwks_uri": "https://rp.example.com/.well-known/jwks.json",
},
},
})
case strings.Contains(r.URL.Path, "/oauth2/auth/requests/login/accept") && r.Method == http.MethodPut:
json.NewEncoder(w).Encode(map[string]string{"redirect_to": "http://rp/cb"})
default:
http.NotFound(w, r)
}
})
mockKratos := new(MockKratosAdminService)
mockKratos.On("FindIdentityIDByIdentifier", mock.Anything, "employee001").Return("kratos-identity-id", nil)
auditRepo := &mockAuditRepo{}
headlessClient := &http.Client{Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
if r.URL.Host == "rp.example.com" && r.URL.Path == "/.well-known/jwks.json" {
return httpResponse(r, http.StatusOK, string(jwksBody)), nil
}
return httpResponse(r, http.StatusNotFound, "not found"), nil
})}
h := &AuthHandler{
IdpProvider: mockIdp,
KratosAdmin: mockKratos,
AuditRepo: auditRepo,
HeadlessJWKS: service.NewHeadlessJWKSCacheService(
nil,
headlessClient,
),
Hydra: &service.HydraAdminService{
AdminURL: "http://hydra.test",
HTTPClient: &http.Client{Transport: mockHydraTransport(hydraHandler)},
},
}
app := fiber.New()
app.Use(middleware.AuditMiddleware(middleware.AuditConfig{
Repo: auditRepo,
BodyDump: true,
}))
app.Post("/api/v1/auth/headless/password/login", h.HeadlessPasswordLogin)
clientAssertion := mustHeadlessClientAssertion(
t,
privateKey,
"headless-login-client",
"http://example.com/api/v1/auth/headless/password/login",
)
body, _ := json.Marshal(map[string]string{
"client_id": "headless-login-client",
"client_assertion": clientAssertion,
"loginId": "employee001",
"password": "password",
"login_challenge": "challenge-123",
})
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/headless/password/login", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/146.0.0.0 Safari/537.36")
resp, err := app.Test(req)
if err != nil {
t.Fatalf("request failed: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
bodyBytes, _ := io.ReadAll(resp.Body)
t.Fatalf("expected 200, got %d, body: %s", resp.StatusCode, string(bodyBytes))
}
if len(auditRepo.logs) != 1 {
t.Fatalf("expected 1 audit log, got %d", len(auditRepo.logs))
}
log := auditRepo.logs[0]
if log.EventType != "POST /api/v1/auth/headless/password/login" {
t.Fatalf("expected headless password login audit event, got %q", log.EventType)
}
if log.UserID != "kratos-identity-id" {
t.Fatalf("expected audit user_id kratos-identity-id, got %q", log.UserID)
}
if log.SessionID != "session-123" {
t.Fatalf("expected audit session_id session-123, got %q", log.SessionID)
}
details, err := parseAuditDetails(log.Details)
if err != nil {
t.Fatalf("failed to parse audit details: %v", err)
}
if got, _ := details["client_id"].(string); got != "headless-login-client" {
t.Fatalf("expected client_id headless-login-client, got %v", details["client_id"])
}
if got, _ := details["client_name"].(string); got != "Headless Login Portal" {
t.Fatalf("expected client_name Headless Login Portal, got %v", details["client_name"])
}
if got, _ := details["login_challenge"].(string); got != "challenge-123" {
t.Fatalf("expected login_challenge challenge-123, got %v", details["login_challenge"])
}
}
func TestHeadlessPasswordLogin_IgnoresInlineHeadlessJWKSWhenJWKSURIIsConfigured(t *testing.T) {
mockIdp := new(MockIdentityProvider)
mockIdp.On("SignIn", "employee001", "password").Return(&domain.AuthInfo{