1
0
forked from baron/baron-sso

활서 세션 카드 audit 메타데이터 기록 보강

This commit is contained in:
2026-04-06 11:17:17 +09:00
parent fe70fd216b
commit 8942c78fb4
2 changed files with 173 additions and 35 deletions

View File

@@ -17,6 +17,7 @@ import (
"io"
"log/slog"
"math/rand"
"net"
"net/http"
"net/url"
"os"
@@ -2426,6 +2427,8 @@ func (h *AuthHandler) HeadlessPasswordLogin(c *fiber.Ctx) error {
return errorJSONCode(c, status, code, message)
}
c.Locals("user_id", authInfo.Subject)
c.Locals("login_id", loginID)
setSessionIDLocal(c, authInfo.SessionToken)
acceptResp, err := h.Hydra.AcceptLoginRequest(c.Context(), loginChallenge, authInfo.Subject)
@@ -2732,7 +2735,15 @@ func (h *AuthHandler) PasswordLogin(c *fiber.Ctx) error {
ale.Status = fiber.StatusOK
ale.LatencyMs = time.Since(startTime)
c.Locals("user_id", authInfo.Subject)
c.Locals("login_id", loginID)
setSessionIDLocal(c, authInfo.SessionToken)
if req.LoginChallenge == "" {
attachAuditClientDetails(c, domain.HydraClient{
ClientID: "userfront",
ClientName: "UserFront",
})
}
ale.Log(slog.LevelInfo, "Login successful", slog.String("provider", h.IdpProvider.Name()), slog.String("subject", authInfo.Subject))
// --- OIDC 로그인 흐름 처리 ---
@@ -2741,11 +2752,14 @@ func (h *AuthHandler) PasswordLogin(c *fiber.Ctx) error {
// Check if the client is active
loginReq, err := h.Hydra.GetLoginRequest(c.Context(), req.LoginChallenge)
if err == nil && loginReq != nil && loginReq.Client.Metadata != nil {
if status, ok := loginReq.Client.Metadata["status"].(string); ok {
if strings.ToLower(status) == "inactive" {
slog.Warn("Login rejected for inactive client in PasswordLogin", "client_id", loginReq.Client.ClientID)
return fiber.NewError(fiber.StatusForbidden, "The client application is disabled.")
if err == nil && loginReq != nil {
attachAuditClientDetails(c, loginReq.Client)
if loginReq.Client.Metadata != nil {
if status, ok := loginReq.Client.Metadata["status"].(string); ok {
if strings.ToLower(status) == "inactive" {
slog.Warn("Login rejected for inactive client in PasswordLogin", "client_id", loginReq.Client.ClientID)
return fiber.NewError(fiber.StatusForbidden, "The client application is disabled.")
}
}
}
}
@@ -2778,6 +2792,27 @@ func (h *AuthHandler) PasswordLogin(c *fiber.Ctx) error {
return c.JSON(resp)
}
func attachAuditClientDetails(c *fiber.Ctx, client domain.HydraClient) {
if c == nil {
return
}
clientID := strings.TrimSpace(client.ClientID)
if clientID == "" {
return
}
clientName := strings.TrimSpace(client.ClientName)
if clientName == "" {
clientName = clientID
}
c.Locals("audit_details_extra", map[string]any{
"client_id": clientID,
"client_name": clientName,
})
}
// InitiatePasswordReset - 사용자가 비밀번호 재설정을 시작하면, loginID 유형에 따라 이메일 또는 SMS를 보냅니다.
func (h *AuthHandler) InitiatePasswordReset(c *fiber.Ctx) error {
startTime := time.Now()
@@ -4983,18 +5018,7 @@ func (h *AuthHandler) AcceptOidcLoginRequest(c *fiber.Ctx) error {
// Check if the client is active
loginReq, err := h.Hydra.GetLoginRequest(c.Context(), req.LoginChallenge)
if err == nil && loginReq != nil {
// Audit 상세 정보 보강: OIDC 로그인 시점에 client 정보를 저장
clientID := strings.TrimSpace(loginReq.Client.ClientID)
if clientID != "" {
clientName := strings.TrimSpace(loginReq.Client.ClientName)
if clientName == "" {
clientName = clientID
}
c.Locals("audit_details_extra", map[string]any{
"client_id": clientID,
"client_name": clientName,
})
}
attachAuditClientDetails(c, loginReq.Client)
if loginReq.Client.Metadata != nil {
if status, ok := loginReq.Client.Metadata["status"].(string); ok {
@@ -6786,6 +6810,9 @@ func (h *AuthHandler) ListMySessions(c *fiber.Ctx) error {
if item.IPAddress == "" && len(session.Devices) > 0 {
item.IPAddress = strings.TrimSpace(session.Devices[0].IPAddress)
}
if item.IsCurrent {
applyCurrentSessionRequestHints(c, &item)
}
items = append(items, item)
}
@@ -6981,6 +7008,46 @@ func (h *AuthHandler) resolveCurrentSessionID(c *fiber.Ctx) string {
return ""
}
func applyCurrentSessionRequestHints(c *fiber.Ctx, item *userSessionItem) {
if c == nil || item == nil || !item.IsCurrent {
return
}
if item.IPAddress == "" {
item.IPAddress = strings.TrimSpace(resolveRequestClientIP(c))
}
if item.UserAgent == "" {
userAgent := strings.TrimSpace(c.Get("User-Agent"))
if !looksLikeInternalUserAgent(userAgent) {
item.UserAgent = userAgent
}
}
if strings.TrimSpace(item.ClientID) == "" {
item.ClientID = "userfront"
}
if strings.TrimSpace(item.AppName) == "" {
item.AppName = "UserFront"
}
}
func resolveRequestClientIP(c *fiber.Ctx) string {
if c == nil {
return ""
}
if forwarded := c.Get("X-Forwarded-For"); forwarded != "" {
parts := strings.Split(forwarded, ",")
if len(parts) > 0 {
if ip := strings.TrimSpace(parts[0]); ip != "" {
return ip
}
}
}
if realIP := strings.TrimSpace(c.Get("X-Real-IP")); realIP != "" {
return realIP
}
return c.IP()
}
func (h *AuthHandler) loadSessionAuditHints(ctx context.Context, userID string) map[string]sessionAuditHint {
hints := make(map[string]sessionAuditHint)
if h.AuditRepo == nil || strings.TrimSpace(userID) == "" {
@@ -6993,6 +7060,7 @@ func (h *AuthHandler) loadSessionAuditHints(ctx context.Context, userID string)
"link_login_success",
"code_login_success",
"password_login_success",
"consent.granted",
"POST /api/v1/auth/oidc/login/accept",
"POST /api/v1/auth/password/login",
"POST /api/v1/auth/magic-link/verify",
@@ -7016,11 +7084,6 @@ func (h *AuthHandler) loadSessionAuditHints(ctx context.Context, userID string)
continue
}
existing, ok := hints[sessionID]
if ok && existing.Timestamp != nil && existing.Timestamp.After(log.Timestamp) {
continue
}
ts := log.Timestamp
ipAddress := strings.TrimSpace(log.IPAddress)
userAgent := strings.TrimSpace(log.UserAgent)
@@ -7036,17 +7099,75 @@ func (h *AuthHandler) loadSessionAuditHints(ctx context.Context, userID string)
if looksLikeInternalUserAgent(userAgent) {
userAgent = ""
}
hints[sessionID] = sessionAuditHint{
hints[sessionID] = mergeSessionAuditHint(hints[sessionID], sessionAuditHint{
Timestamp: &ts,
IPAddress: ipAddress,
UserAgent: userAgent,
ClientID: clientID,
AppName: appName,
}
})
}
return hints
}
func mergeSessionAuditHint(existing sessionAuditHint, candidate sessionAuditHint) sessionAuditHint {
if candidate.Timestamp != nil &&
(existing.Timestamp == nil || candidate.Timestamp.After(*existing.Timestamp)) {
existing.Timestamp = candidate.Timestamp
}
if shouldReplaceSessionIP(existing.IPAddress, candidate.IPAddress) {
existing.IPAddress = candidate.IPAddress
}
if existing.UserAgent == "" && candidate.UserAgent != "" {
existing.UserAgent = candidate.UserAgent
}
if existing.ClientID == "" && candidate.ClientID != "" {
existing.ClientID = candidate.ClientID
}
if existing.AppName == "" && candidate.AppName != "" {
existing.AppName = candidate.AppName
}
return existing
}
func shouldReplaceSessionIP(existing string, candidate string) bool {
existing = strings.TrimSpace(existing)
candidate = strings.TrimSpace(candidate)
if candidate == "" {
return false
}
if existing == "" {
return true
}
if isPrivateIPAddress(existing) && !isPrivateIPAddress(candidate) {
return true
}
return false
}
func isPrivateIPAddress(raw string) bool {
ip := net.ParseIP(strings.TrimSpace(raw))
if ip == nil {
return false
}
if ip.IsLoopback() || ip.IsLinkLocalMulticast() || ip.IsLinkLocalUnicast() {
return true
}
for _, cidr := range []string{
"10.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16",
"100.64.0.0/10",
"fc00::/7",
} {
_, network, err := net.ParseCIDR(cidr)
if err == nil && network.Contains(ip) {
return true
}
}
return false
}
func deriveSessionClientInfo(log domain.AuditLog) (string, string) {
details, _ := parseAuditDetails(log.Details)
clientID := ""

View File

@@ -1201,25 +1201,42 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
}
String _sessionPrimaryLabel(UserSessionSummary session) {
if (session.isCurrent) {
return tr('ui.userfront.dashboard.sessions.current_badge');
final appLabel = _sessionAppLabel(session);
if (appLabel.isNotEmpty) {
return appLabel;
}
final appName = session.appName.trim();
if (appName.isNotEmpty) {
return appName;
if (session.isCurrent) {
return 'UserFront';
}
return tr('ui.userfront.dashboard.sessions.unknown_session');
}
String _sessionClientLabel(UserSessionSummary session) {
return '';
}
String _sessionAppLabel(UserSessionSummary session) {
final appName = session.appName.trim();
if (appName.isEmpty || session.isCurrent) {
return '';
if (appName.isNotEmpty) {
return appName;
}
return tr(
'msg.userfront.dashboard.sessions.recent_app',
params: {'app': appName},
);
final clientId = session.clientId.trim().toLowerCase();
if (clientId.isEmpty) {
return session.isCurrent ? 'UserFront' : '';
}
if (clientId.contains('adminfront')) {
return 'AdminFront';
}
if (clientId.contains('devfront')) {
return 'DevFront';
}
if (clientId.contains('userfront')) {
return 'UserFront';
}
if (clientId.contains('baron')) {
return tr('ui.userfront.app_label.baron');
}
return session.clientId.trim();
}
String _sessionUserAgentLabel(String userAgent) {