diff --git a/adminfront/src/features/auth/LoginPage.tsx b/adminfront/src/features/auth/LoginPage.tsx
index bc6d780d..3c46ac63 100644
--- a/adminfront/src/features/auth/LoginPage.tsx
+++ b/adminfront/src/features/auth/LoginPage.tsx
@@ -64,6 +64,26 @@ function LoginPage() {
+ {auth.error && (
+
+
+
+ 인증 오류가 발생했습니다
+
+
{auth.error.message}
+
+
+ )}
+
diff --git a/adminfront/src/lib/auth.ts b/adminfront/src/lib/auth.ts
index aab02a2b..0d4a7f43 100644
--- a/adminfront/src/lib/auth.ts
+++ b/adminfront/src/lib/auth.ts
@@ -3,12 +3,13 @@ import type { AuthProviderProps } from "react-oidc-context";
export const oidcConfig: AuthProviderProps = {
authority:
- import.meta.env.VITE_OIDC_AUTHORITY || "http://localhost:5000/oidc", // Gateway Proxy URL
+ import.meta.env.VITE_OIDC_AUTHORITY || "https://sso.hmac.kr/oidc", // Gateway Proxy URL
client_id: import.meta.env.VITE_OIDC_CLIENT_ID || "adminfront",
redirect_uri: `${window.location.origin}/auth/callback`,
response_type: "code",
scope: "openid offline_access profile email", // offline_access for refresh token
post_logout_redirect_uri: window.location.origin,
+ popup_redirect_uri: `${window.location.origin}/auth/callback`,
userStore: new WebStorageStateStore({ store: window.localStorage }),
automaticSilentRenew: false,
};
diff --git a/backend/internal/domain/idp_models.go b/backend/internal/domain/idp_models.go
index e22b9392..fd9b9168 100644
--- a/backend/internal/domain/idp_models.go
+++ b/backend/internal/domain/idp_models.go
@@ -53,6 +53,7 @@ type AuthInfo struct {
RefreshToken *Token
// Subject는 IDP 세션이 대표하는 주체(예: Kratos identity.id)를 나타냅니다.
Subject string
+ SetCookies []*http.Cookie
}
// LinkLoginInit는 링크 로그인 초기화 결과입니다.
diff --git a/backend/internal/handler/auth_handler.go b/backend/internal/handler/auth_handler.go
index f7aed790..3c3ea26f 100644
--- a/backend/internal/handler/auth_handler.go
+++ b/backend/internal/handler/auth_handler.go
@@ -17,6 +17,7 @@ import (
"io"
"log/slog"
"math/rand"
+ "net"
"net/http"
"net/url"
"os"
@@ -1641,6 +1642,10 @@ func (h *AuthHandler) VerifyMagicLink(c *fiber.Ctx) error {
sessionToken := authInfo.SessionToken.JWT
c.Locals("login_id", loginID)
setSessionIDLocal(c, authInfo.SessionToken)
+
+ // Write Kratos session cookies to the response
+ h.writeAuthCookies(c, authInfo.SetCookies)
+
sessionID := extractSessionIDFromToken(authInfo.SessionToken)
slog.Info("[Verify] Success! Updating Redis session", "pendingRef", pendingRef)
@@ -1752,6 +1757,9 @@ func (h *AuthHandler) VerifyLoginCode(c *fiber.Ctx) error {
c.Locals("login_id", lookupLoginID)
setSessionIDLocal(c, authInfo.SessionToken)
+ // Write Kratos session cookies to the response
+ h.writeAuthCookies(c, authInfo.SetCookies)
+
h.RedisService.Delete(prefixLoginCode + lookupLoginID)
h.RedisService.Delete(prefixLoginCodeSmsTarget + lookupLoginID)
@@ -2414,6 +2422,10 @@ func (h *AuthHandler) completeApprovedLinkLogin(c *fiber.Ctx, pendingRef string)
c.Locals("login_id", loginID)
setSessionIDLocal(c, authInfo.SessionToken)
+
+ // Write Kratos session cookies to the response
+ h.writeAuthCookies(c, authInfo.SetCookies)
+
sessionID := extractSessionIDFromToken(authInfo.SessionToken)
if sessionID == "" && authInfo.SessionToken != nil && authInfo.SessionToken.JWT != "" {
if resolved, err := h.getKratosSessionID(authInfo.SessionToken.JWT); err == nil && resolved != "" {
@@ -2784,6 +2796,40 @@ func (h *AuthHandler) HeadlessLinkPoll(c *fiber.Ctx) error {
})
}
+func (h *AuthHandler) writeAuthCookies(c *fiber.Ctx, cookies []*http.Cookie) {
+ if len(cookies) == 0 {
+ return
+ }
+
+ host := c.Hostname()
+ domain := ""
+
+ // IP address or localhost check
+ if ip := net.ParseIP(host); ip != nil || host == "localhost" {
+ domain = host
+ } else {
+ // Extract root domain (e.g., .hmac.kr from sso.hmac.kr)
+ parts := strings.Split(host, ".")
+ if len(parts) >= 2 {
+ domain = "." + strings.Join(parts[len(parts)-2:], ".")
+ }
+ }
+
+ for _, cookie := range cookies {
+ c.Cookie(&fiber.Cookie{
+ Name: cookie.Name,
+ Value: cookie.Value,
+ Path: "/",
+ Domain: domain,
+ MaxAge: cookie.MaxAge,
+ Expires: cookie.Expires,
+ Secure: true,
+ HTTPOnly: true,
+ SameSite: "Lax",
+ })
+ }
+}
+
func (h *AuthHandler) PasswordLogin(c *fiber.Ctx) error {
startTime := time.Now()
ale := logger.NewAuditLogEntry(c, "login")
@@ -2832,6 +2878,10 @@ func (h *AuthHandler) PasswordLogin(c *fiber.Ctx) error {
c.Locals("user_id", authInfo.Subject)
c.Locals("login_id", loginID)
setSessionIDLocal(c, authInfo.SessionToken)
+
+ // Write Kratos session cookies to the response
+ h.writeAuthCookies(c, authInfo.SetCookies)
+
if req.LoginChallenge == "" {
attachAuditClientDetails(c, domain.HydraClient{
ClientID: "userfront",
@@ -2864,16 +2914,22 @@ func (h *AuthHandler) PasswordLogin(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to accept OIDC login request")
}
logOidcRedirectSummary("password_login", acceptResp.RedirectTo)
+
+ // IMPORTANT: Also return sessionJwt and token during OIDC flow to ensure portal session.
return c.JSON(fiber.Map{
"redirectTo": acceptResp.RedirectTo,
"status": "ok",
"provider": h.IdpProvider.Name(),
+ "sessionJwt": authInfo.SessionToken.JWT,
+ "token": authInfo.SessionToken.JWT,
+ "subject": authInfo.Subject,
})
}
// --- OIDC 로그인 흐름 처리 끝 ---
resp := fiber.Map{
"sessionJwt": authInfo.SessionToken.JWT,
+ "token": authInfo.SessionToken.JWT,
"status": "ok",
"provider": h.IdpProvider.Name(),
}
diff --git a/backend/internal/service/ory_service.go b/backend/internal/service/ory_service.go
index 0eb08d68..1cb13ab6 100644
--- a/backend/internal/service/ory_service.go
+++ b/backend/internal/service/ory_service.go
@@ -247,7 +247,8 @@ func (o *OryProvider) SignIn(loginID, password string) (*domain.AuthInfo, error)
Expiration: result.SessionTokenExpiresAt,
SessionID: result.Session.ID,
},
- Subject: result.Session.Identity.ID,
+ Subject: result.Session.Identity.ID,
+ SetCookies: resp.Cookies(),
}, nil
}
@@ -693,7 +694,8 @@ func (o *OryProvider) VerifyLoginCode(loginID, flowID, code string) (*domain.Aut
Expiration: result.SessionTokenExpiresAt,
SessionID: result.Session.ID,
},
- Subject: result.Session.Identity.ID,
+ Subject: result.Session.Identity.ID,
+ SetCookies: resp.Cookies(),
}, nil
}
diff --git a/devfront/src/features/auth/AuthGuard.tsx b/devfront/src/features/auth/AuthGuard.tsx
index 26069583..e204b150 100644
--- a/devfront/src/features/auth/AuthGuard.tsx
+++ b/devfront/src/features/auth/AuthGuard.tsx
@@ -9,7 +9,20 @@ export default function AuthGuard() {
}
if (auth.error) {
- return Auth Error: {auth.error.message}
;
+ return (
+
+
+
Authentication Error
+
{auth.error.message}
+
+
+
+ );
}
if (!auth.isAuthenticated) {
diff --git a/devfront/src/lib/auth.ts b/devfront/src/lib/auth.ts
index d0f0772e..210b507d 100644
--- a/devfront/src/lib/auth.ts
+++ b/devfront/src/lib/auth.ts
@@ -3,7 +3,7 @@ import type { AuthProviderProps } from "react-oidc-context";
export const oidcConfig: AuthProviderProps = {
authority:
- import.meta.env.VITE_OIDC_AUTHORITY || "http://localhost:5000/oidc", // Gateway Proxy URL
+ import.meta.env.VITE_OIDC_AUTHORITY || "https://sso.hmac.kr/oidc", // Gateway Proxy URL
client_id: import.meta.env.VITE_OIDC_CLIENT_ID || "devfront",
redirect_uri: `${window.location.origin}/auth/callback`,
response_type: "code",
diff --git a/userfront/lib/core/notifiers/auth_notifier.dart b/userfront/lib/core/notifiers/auth_notifier.dart
index 24282ca5..2591960a 100644
--- a/userfront/lib/core/notifiers/auth_notifier.dart
+++ b/userfront/lib/core/notifiers/auth_notifier.dart
@@ -1,8 +1,15 @@
import 'package:flutter/foundation.dart';
+import '../services/auth_token_store.dart';
class AuthNotifier extends ChangeNotifier {
static final AuthNotifier instance = AuthNotifier();
+ Future onLoginSuccess(String token, {String? provider}) async {
+ AuthTokenStore.setToken(token, provider: provider);
+ AuthTokenStore.clearPendingProvider();
+ notifyListeners();
+ }
+
void notify() {
notifyListeners();
}
diff --git a/userfront/lib/core/services/auth_proxy_service.dart b/userfront/lib/core/services/auth_proxy_service.dart
index 252fdf32..433c003d 100644
--- a/userfront/lib/core/services/auth_proxy_service.dart
+++ b/userfront/lib/core/services/auth_proxy_service.dart
@@ -92,6 +92,32 @@ class AuthProxyService {
}
}
+ static Future