forked from baron/baron-sso
활서 세션 카드 audit 메타데이터 기록 보강
This commit is contained in:
@@ -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 := ""
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user