@@ -252,7 +256,7 @@ func (h *AuthHandler) SendSignupSmsCode(c *fiber.Ctx) error {
h.saveSignupState(key, newState, signupStateExpiration)
// 4. Send SMS
- content := fmt.Sprintf("[Baron SSO] 인증번호 [%s]를 입력해주세요.", code)
+ content := fmt.Sprintf("[Baron 통합로그인] 인증번호 [%s]를 입력해주세요.", code)
go h.SmsService.SendSms(phone, content)
return c.JSON(fiber.Map{"message": "Verification code sent"})
@@ -535,7 +539,7 @@ func (h *AuthHandler) SendSms(c *fiber.Ctx) error {
sanitizedPhone := strings.ReplaceAll(req.PhoneNumber, "-", "")
rand.Seed(time.Now().UnixNano())
code := fmt.Sprintf("%06d", rand.Intn(1000000))
- content := fmt.Sprintf("[Baron SSO] 인증번호: %s", code)
+ content := fmt.Sprintf("[Baron 통합로그인] 인증번호: %s", code)
h.RedisService.StoreVerificationCode(sanitizedPhone, code)
if err := h.SmsService.SendSms(sanitizedPhone, content); err != nil {
@@ -621,8 +625,19 @@ func (h *AuthHandler) InitEnchantedLink(c *fiber.Ctx) error {
}
if init, err := h.IdpProvider.InitiateLinkLogin(lookupLoginID, userfrontURL); err == nil && init != nil && init.Mode != "" {
+ keyLoginID := lookupLoginID
+ if init.LoginID != "" {
+ keyLoginID = init.LoginID
+ }
if init.FlowID != "" {
- _ = h.RedisService.Set(prefixLoginCode+lookupLoginID, init.FlowID, loginCodeExpiration)
+ _ = h.RedisService.Set(prefixLoginCode+keyLoginID, init.FlowID, loginCodeExpiration)
+ }
+ pendingRef := GenerateSecureToken(3)
+ h.RedisService.Set(prefixSession+pendingRef, fmt.Sprintf(`{"status":"%s"}`, statusPending), loginCodeExpiration)
+ _ = h.RedisService.Set(prefixLoginCodePending+keyLoginID, pendingRef, loginCodeExpiration)
+ if !strings.Contains(loginID, "@") && keyLoginID != lookupLoginID {
+ _ = h.RedisService.Set(prefixLoginCodeSmsTarget+keyLoginID, lookupLoginID, loginCodeExpiration)
+ _ = h.RedisService.Set(prefixLoginCodeSmsLookup+lookupLoginID, keyLoginID, loginCodeExpiration)
}
expiresIn := 0
if !init.ExpiresAt.IsZero() {
@@ -630,7 +645,7 @@ func (h *AuthHandler) InitEnchantedLink(c *fiber.Ctx) error {
}
return c.JSON(fiber.Map{
"linkId": "Sent",
- "pendingRef": init.FlowID,
+ "pendingRef": pendingRef,
"maskedEmail": loginID,
"mode": init.Mode,
"provider": h.IdpProvider.Name(),
@@ -665,7 +680,7 @@ func (h *AuthHandler) InitEnchantedLink(c *fiber.Ctx) error {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Email service not configured"})
}
- subject := "[Baron SSO] 로그인 링크"
+ subject := "[Baron 통합로그인] 링크"
body := fmt.Sprintf(`
Baron SSO 로그인
@@ -686,7 +701,7 @@ func (h *AuthHandler) InitEnchantedLink(c *fiber.Ctx) error {
}
} else {
// Send SMS
- content := fmt.Sprintf("[Baron SSO] 로그인 링크: %s | 코드: %s", link, userCode)
+ content := fmt.Sprintf("[Baron 통합로그인] 로그인 링크: %s | 코드: %s", link, userCode)
slog.Info("[Enchanted] Sending SMS via Naver Cloud", "loginID", loginID)
if err := h.SmsService.SendSms(loginID, content); err != nil {
@@ -714,7 +729,7 @@ func (h *AuthHandler) PollEnchantedLink(c *fiber.Ctx) error {
pollKey := prefixPollMeta + "enchanted:" + req.PendingRef
if slowDown, interval := checkPollInterval(h.RedisService, pollKey, minPollInterval); slowDown {
- return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
+ return c.JSON(fiber.Map{
"error": "slow_down",
"interval": interval,
})
@@ -722,7 +737,7 @@ func (h *AuthHandler) PollEnchantedLink(c *fiber.Ctx) error {
val, err := h.RedisService.Get(prefixSession + req.PendingRef)
if err != nil || val == "" {
- return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "expired_token"})
+ return c.JSON(fiber.Map{"error": "expired_token"})
}
var data map[string]string
@@ -736,7 +751,7 @@ func (h *AuthHandler) PollEnchantedLink(c *fiber.Ctx) error {
})
}
- return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
+ return c.JSON(fiber.Map{
"error": "authorization_pending",
"interval": int(minPollInterval.Seconds()),
})
@@ -802,8 +817,9 @@ func (h *AuthHandler) VerifyMagicLink(c *fiber.Ctx) error {
// VerifyLoginCode - Verify Kratos login code and issue session.
func (h *AuthHandler) VerifyLoginCode(c *fiber.Ctx) error {
var req struct {
- LoginID string `json:"loginId"`
- Code string `json:"code"`
+ LoginID string `json:"loginId"`
+ Code string `json:"code"`
+ PendingRef string `json:"pendingRef"`
}
if err := c.BodyParser(&req); err != nil {
slog.Error("[LoginCode] Body parse error", "error", err)
@@ -843,6 +859,110 @@ func (h *AuthHandler) VerifyLoginCode(c *fiber.Ctx) error {
}
h.RedisService.Delete(prefixLoginCode + lookupLoginID)
+ h.RedisService.Delete(prefixLoginCodeSmsTarget + lookupLoginID)
+
+ pendingRef := strings.TrimSpace(req.PendingRef)
+ if pendingRef == "" {
+ storedRef, _ := h.RedisService.Get(prefixLoginCodePending + lookupLoginID)
+ pendingRef = storedRef
+ }
+ if pendingRef != "" {
+ sessionData, _ := json.Marshal(map[string]string{
+ "status": statusSuccess,
+ "jwt": authInfo.SessionToken.JWT,
+ })
+ h.RedisService.Set(prefixSession+pendingRef, string(sessionData), loginCodeExpiration)
+ h.RedisService.Delete(prefixLoginCodePending + lookupLoginID)
+ h.RedisService.Delete(prefixLoginCodeSmsTarget + lookupLoginID)
+ return c.JSON(fiber.Map{
+ "status": "approved",
+ "pendingRef": pendingRef,
+ "provider": h.IdpProvider.Name(),
+ "subject": authInfo.Subject,
+ "message": "Login approved",
+ })
+ }
+
+ return c.JSON(fiber.Map{
+ "token": authInfo.SessionToken.JWT,
+ "sessionJwt": authInfo.SessionToken.JWT,
+ "provider": h.IdpProvider.Name(),
+ "subject": authInfo.Subject,
+ "message": "Login successful",
+ })
+}
+
+// VerifyLoginShortCode - Verify short code (2 letters + 6 digits) and issue/approve session.
+func (h *AuthHandler) VerifyLoginShortCode(c *fiber.Ctx) error {
+ var req struct {
+ ShortCode string `json:"shortCode"`
+ }
+ if err := c.BodyParser(&req); err != nil {
+ slog.Error("[LoginShortCode] Body parse error", "error", err)
+ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"})
+ }
+
+ shortCode := strings.ToUpper(strings.TrimSpace(req.ShortCode))
+ if shortCode == "" {
+ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "shortCode is required"})
+ }
+
+ val, _ := h.RedisService.Get(prefixLoginCodeShort + shortCode)
+ if val == "" {
+ return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid or expired code"})
+ }
+
+ var payload shortLoginCodePayload
+ if err := json.Unmarshal([]byte(val), &payload); err != nil {
+ return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Invalid code payload"})
+ }
+ if payload.LoginID == "" || payload.Code == "" {
+ return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid or expired code"})
+ }
+
+ if h.IdpProvider == nil {
+ return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "Identity provider unavailable"})
+ }
+
+ flowID, err := h.RedisService.Get(prefixLoginCode + payload.LoginID)
+ if err != nil || flowID == "" {
+ return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Login flow expired"})
+ }
+
+ authInfo, err := h.IdpProvider.VerifyLoginCode(payload.LoginID, flowID, payload.Code)
+ if err != nil {
+ if errors.Is(err, domain.ErrNotSupported) {
+ return c.Status(fiber.StatusNotImplemented).JSON(fiber.Map{"error": "Login method not supported"})
+ }
+ slog.Error("[LoginShortCode] Verify failed", "loginID", payload.LoginID, "error", err)
+ return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid code"})
+ }
+ if authInfo == nil || authInfo.SessionToken == nil || authInfo.SessionToken.JWT == "" {
+ return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to issue session"})
+ }
+
+ h.RedisService.Delete(prefixLoginCode + payload.LoginID)
+ h.RedisService.Delete(prefixLoginCodeShort + shortCode)
+ h.RedisService.Delete(prefixLoginCodeSmsTarget + payload.LoginID)
+ h.RedisService.Delete(prefixLoginCodeSmsLookup + payload.LoginID)
+
+ if payload.PendingRef != "" {
+ sessionData, _ := json.Marshal(map[string]string{
+ "status": statusSuccess,
+ "jwt": authInfo.SessionToken.JWT,
+ })
+ h.RedisService.Set(prefixSession+payload.PendingRef, string(sessionData), loginCodeExpiration)
+ h.RedisService.Delete(prefixLoginCodePending + payload.LoginID)
+ return c.JSON(fiber.Map{
+ "status": "approved",
+ "pendingRef": payload.PendingRef,
+ "token": authInfo.SessionToken.JWT,
+ "sessionJwt": authInfo.SessionToken.JWT,
+ "provider": h.IdpProvider.Name(),
+ "subject": authInfo.Subject,
+ "message": "Login approved",
+ })
+ }
return c.JSON(fiber.Map{
"token": authInfo.SessionToken.JWT,
@@ -998,7 +1118,7 @@ func (h *AuthHandler) InitiatePasswordReset(c *fiber.Ctx) error {
ale.Log(slog.LevelError, "Email service not configured")
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Email service not configured"})
}
- subject := "[Baron SSO] 비밀번호 재설정"
+ subject := "[Baron 통합로그인] 비밀번호 재설정"
body := fmt.Sprintf(`
Baron SSO 비밀번호 재설정
@@ -1017,7 +1137,7 @@ func (h *AuthHandler) InitiatePasswordReset(c *fiber.Ctx) error {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to send reset email"})
}
} else {
- if err := h.SmsService.SendSms(loginID, fmt.Sprintf("[Baron SSO] 비밀번호 재설정 링크: %s", resetLink)); err != nil {
+ if err := h.SmsService.SendSms(loginID, fmt.Sprintf("[Baron 통합로그인] 비밀번호 재설정 링크: %s", resetLink)); err != nil {
ale.Status = fiber.StatusInternalServerError
ale.LatencyMs = time.Since(startTime)
ale.DescopeError = err.Error()
@@ -1350,6 +1470,22 @@ func (h *AuthHandler) HandleKratosCourierRelay(c *fiber.Ctx) error {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Empty message"})
}
+ if strings.Contains(req.Recipient, "@") {
+ if target, _ := h.RedisService.Get(prefixLoginCodeSmsTarget + req.Recipient); target != "" {
+ phone := sanitizePhoneForSms(target)
+ smsBody := h.buildKratosShortSmsBody(&req, req.Recipient, phone)
+ if smsBody == "" {
+ smsBody = body
+ }
+ if err := h.SmsService.SendSms(phone, smsBody); err != nil {
+ slog.Error("[Kratos Courier] SMS send failed", "to", phone, "error", err)
+ return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to send SMS"})
+ }
+ slog.Info("[Kratos Courier] SMS sent (email relay)", "to", phone, "template", req.TemplateType)
+ return c.JSON(fiber.Map{"status": "ok"})
+ }
+ }
+
if strings.Contains(req.Recipient, "@") {
if h.EmailService == nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Email service not configured"})
@@ -1366,7 +1502,20 @@ func (h *AuthHandler) HandleKratosCourierRelay(c *fiber.Ctx) error {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "SMS service not configured"})
}
phone := sanitizePhoneForSms(req.Recipient)
- if err := h.SmsService.SendSms(phone, body); err != nil {
+ loginID := req.Recipient
+ if !strings.Contains(loginID, "@") {
+ lookup := normalizePhoneForLoginID(loginID)
+ if email, _ := h.RedisService.Get(prefixLoginCodeSmsLookup + lookup); email != "" {
+ loginID = email
+ } else {
+ loginID = lookup
+ }
+ }
+ smsBody := h.buildKratosShortSmsBody(&req, loginID, phone)
+ if smsBody == "" {
+ smsBody = body
+ }
+ if err := h.SmsService.SendSms(phone, smsBody); err != nil {
slog.Error("[Kratos Courier] SMS send failed", "to", phone, "error", err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to send SMS"})
}
@@ -1379,7 +1528,7 @@ func (h *AuthHandler) buildKratosCourierMessage(req *kratosCourierRequest) (stri
body := strings.TrimSpace(req.Body)
if body != "" || subject != "" {
if subject == "" {
- subject = "[Baron SSO] 알림"
+ subject = "[Baron 통합로그인] 알림"
}
return subject, body
}
@@ -1403,23 +1552,38 @@ func (h *AuthHandler) buildKratosCourierMessage(req *kratosCourierRequest) (stri
if subject == "" {
if label == "알림" {
- subject = "[Baron SSO] 알림"
+ subject = "[Baron 통합로그인] 알림"
} else {
- subject = fmt.Sprintf("[Baron SSO] %s 코드", label)
+ subject = fmt.Sprintf("[Baron 통합로그인] %s 코드", label)
}
}
if code == "" {
- return subject, fmt.Sprintf("[Baron SSO] %s 요청이 도착했습니다", label)
+ return subject, fmt.Sprintf("[Baron 통합로그인] %s 요청이 도착했습니다", label)
}
- message := fmt.Sprintf("[Baron SSO] %s 코드: %s", label, code)
+ message := fmt.Sprintf("[Baron 통합로그인] %s 코드: %s", label, code)
if label == "로그인" {
baseURL := os.Getenv("USERFRONT_URL")
if baseURL == "" {
baseURL = "http://localhost:5000"
}
baseURL = strings.TrimRight(baseURL, "/")
+ loginID := req.Recipient
+ if !strings.Contains(loginID, "@") {
+ loginID = normalizePhoneForLoginID(loginID)
+ }
+ pendingRef, _ := h.RedisService.Get(prefixLoginCodePending + loginID)
+ if pendingRef != "" {
+ message = fmt.Sprintf("%s | 링크: %s/verify?loginId=%s&code=%s&pendingRef=%s",
+ message,
+ baseURL,
+ url.QueryEscape(req.Recipient),
+ url.QueryEscape(code),
+ url.QueryEscape(pendingRef),
+ )
+ return subject, message
+ }
link := fmt.Sprintf("%s/verify?loginId=%s&code=%s",
baseURL,
url.QueryEscape(req.Recipient),
@@ -1431,6 +1595,78 @@ func (h *AuthHandler) buildKratosCourierMessage(req *kratosCourierRequest) (stri
return subject, message
}
+type shortLoginCodePayload struct {
+ LoginID string `json:"loginId"`
+ Code string `json:"code"`
+ PendingRef string `json:"pendingRef"`
+}
+
+func (h *AuthHandler) buildKratosShortSmsBody(req *kratosCourierRequest, loginID, phone string) string {
+ if req == nil || loginID == "" {
+ return ""
+ }
+ code := normalizeLoginCode(extractFirstString(req.TemplateData, "login_code"))
+ if code == "" {
+ return ""
+ }
+ shortCode := h.generateShortCode(code)
+ if shortCode == "" {
+ return ""
+ }
+
+ pendingRef, _ := h.RedisService.Get(prefixLoginCodePending + loginID)
+ payload := shortLoginCodePayload{
+ LoginID: loginID,
+ Code: code,
+ PendingRef: pendingRef,
+ }
+ raw, _ := json.Marshal(payload)
+ _ = h.RedisService.Set(prefixLoginCodeShort+shortCode, string(raw), loginCodeExpiration)
+
+ baseURL := strings.TrimRight(os.Getenv("USERFRONT_URL"), "/")
+ if baseURL == "" {
+ baseURL = "http://localhost:5000"
+ }
+
+ link := fmt.Sprintf("%s/l/%s", baseURL, shortCode)
+ return fmt.Sprintf("[Baron 통합로그인] %s", link)
+}
+
+func (h *AuthHandler) generateShortCode(code string) string {
+ const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ for i := 0; i < 10; i++ {
+ b := make([]byte, 2)
+ if _, err := crand.Read(b); err != nil {
+ break
+ }
+ prefix := string(letters[int(b[0])%len(letters)]) + string(letters[int(b[1])%len(letters)])
+ shortCode := prefix + code
+ if val, _ := h.RedisService.Get(prefixLoginCodeShort + shortCode); val == "" {
+ return shortCode
+ }
+ }
+ return ""
+}
+
+func normalizeLoginCode(code string) string {
+ if code == "" {
+ return ""
+ }
+ digits := make([]rune, 0, len(code))
+ for _, ch := range code {
+ if ch >= '0' && ch <= '9' {
+ digits = append(digits, ch)
+ }
+ }
+ if len(digits) < 6 {
+ return ""
+ }
+ if len(digits) > 6 {
+ digits = digits[:6]
+ }
+ return string(digits)
+}
+
func firstNonEmpty(values ...string) string {
for _, value := range values {
if value != "" {
@@ -1941,7 +2177,7 @@ func (h *AuthHandler) SendUpdateCode(c *fiber.Ctx) error {
h.RedisService.Set(key, code, 5*time.Minute)
// Send SMS
- content := fmt.Sprintf("[Baron SSO] 정보 수정 인증번호: [%s]", code)
+ content := fmt.Sprintf("[Baron 통합로그인] 정보 수정 인증번호: [%s]", code)
go h.SmsService.SendSms(phone, content)
return c.JSON(fiber.Map{"message": "인증번호가 전송되었습니다."})
diff --git a/backend/internal/service/ory_service.go b/backend/internal/service/ory_service.go
index 655455a2..9bc54e4f 100644
--- a/backend/internal/service/ory_service.go
+++ b/backend/internal/service/ory_service.go
@@ -232,22 +232,64 @@ func (o *OryProvider) InitiateLinkLogin(loginID, returnTo string) (*domain.LinkL
return nil, fmt.Errorf("ory provider: loginID is required")
}
- init, err := o.submitLoginCodeInit(loginID, returnTo)
+ effectiveLoginID, err := o.resolveEffectiveLoginID(loginID)
+ if err != nil {
+ return nil, err
+ }
+
+ if err := o.ensureCodeLoginIdentifier(effectiveLoginID); err != nil {
+ return nil, err
+ }
+
+ init, err := o.submitLoginCodeInit(effectiveLoginID, returnTo)
if err == nil {
+ init.LoginID = effectiveLoginID
return init, nil
}
if shouldBootstrapCodeLogin(err) {
- if ensureErr := o.ensureCodeLoginIdentifier(loginID); ensureErr == nil {
- return o.submitLoginCodeInit(loginID, returnTo)
+ if ensureErr := o.ensureCodeLoginIdentifier(effectiveLoginID); ensureErr == nil {
+ init, initErr := o.submitLoginCodeInit(effectiveLoginID, returnTo)
+ if initErr == nil {
+ init.LoginID = effectiveLoginID
+ }
+ return init, initErr
} else {
- slog.Warn("Ory code login bootstrap failed", "loginID", loginID, "error", ensureErr)
+ slog.Warn("Ory code login bootstrap failed", "loginID", effectiveLoginID, "error", ensureErr)
}
}
return nil, err
}
+func (o *OryProvider) resolveEffectiveLoginID(loginID string) (string, error) {
+ if strings.Contains(loginID, "@") {
+ return loginID, nil
+ }
+
+ identityID, err := o.findIdentityID(loginID)
+ if err != nil {
+ return "", err
+ }
+ if identityID == "" {
+ return "", fmt.Errorf("ory provider: identity not found for loginID=%s", loginID)
+ }
+
+ fullIdentity, err := o.fetchIdentityFull(identityID)
+ if err != nil {
+ return "", err
+ }
+ if fullIdentity != nil {
+ if emailRaw, ok := fullIdentity.Traits["email"]; ok {
+ if email, ok := emailRaw.(string); ok && email != "" {
+ return email, nil
+ }
+ }
+ }
+
+ return "", fmt.Errorf("ory provider: email trait missing for loginID=%s", loginID)
+}
+
func (o *OryProvider) submitLoginCodeInit(loginID, returnTo string) (*domain.LinkLoginInit, error) {
flowID, err := o.startLoginFlow(returnTo)
if err != nil {
@@ -404,13 +446,83 @@ func (o *OryProvider) ensureCodeLoginIdentifier(loginID string) error {
return nil
}
- return o.patchIdentity(identityID, ops)
+ if err := o.patchIdentity(identityID, ops); err != nil {
+ slog.Warn("Ory identity patch failed, trying full update", "identity_id", identityID, "error", err)
+ }
+
+ fullIdentity, err := o.fetchIdentityFull(identityID)
+ if err != nil {
+ return err
+ }
+
+ addresses = make([]kratosVerifiableAddress, 0, len(fullIdentity.VerifiableAddresses)+1)
+ found := false
+ for _, addr := range fullIdentity.VerifiableAddresses {
+ addresses = append(addresses, kratosVerifiableAddress{
+ Value: addr.Value,
+ Via: addr.Via,
+ Verified: addr.Verified,
+ Status: addr.Status,
+ })
+ if addr.Value == loginID && addr.Via == via {
+ found = true
+ }
+ }
+ if !found {
+ addresses = append(addresses, kratosVerifiableAddress{
+ Value: loginID,
+ Via: via,
+ Verified: true,
+ Status: "completed",
+ })
+ }
+
+ payload := map[string]interface{}{
+ "schema_id": fullIdentity.SchemaID,
+ "traits": fullIdentity.Traits,
+ "verifiable_addresses": addresses,
+ }
+ if len(fullIdentity.RecoveryAddresses) > 0 {
+ payload["recovery_addresses"] = fullIdentity.RecoveryAddresses
+ }
+
+ body, _ := json.Marshal(payload)
+ req, err := http.NewRequestWithContext(context.Background(), http.MethodPut, fmt.Sprintf("%s/admin/identities/%s", o.KratosAdminURL, identityID), bytes.NewReader(body))
+ if err != nil {
+ return fmt.Errorf("ory provider: build identity update failed: %w", err)
+ }
+ req.Header.Set("Content-Type", "application/json")
+
+ resp, err := o.httpClient().Do(req)
+ if err != nil {
+ return fmt.Errorf("ory provider: identity update failed: %w", err)
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode >= 300 {
+ respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
+ return fmt.Errorf("ory provider: identity update failed status=%d body=%s", resp.StatusCode, string(respBody))
+ }
+
+ slog.Info("Ory identity updated with verifiable address", "identity_id", identityID, "loginID", loginID, "via", via)
+ return nil
}
type kratosIdentity struct {
VerifiableAddresses []kratosVerifiableAddress `json:"verifiable_addresses"`
}
+type kratosRecoveryAddress struct {
+ Value string `json:"value"`
+ Via string `json:"via"`
+}
+
+type kratosIdentityFull struct {
+ SchemaID string `json:"schema_id"`
+ Traits map[string]interface{} `json:"traits"`
+ VerifiableAddresses []kratosVerifiableAddress `json:"verifiable_addresses"`
+ RecoveryAddresses []kratosRecoveryAddress `json:"recovery_addresses"`
+}
+
func (o *OryProvider) patchIdentity(identityID string, ops []map[string]interface{}) error {
body, _ := json.Marshal(ops)
req, err := http.NewRequestWithContext(context.Background(), http.MethodPatch, fmt.Sprintf("%s/admin/identities/%s", o.KratosAdminURL, identityID), bytes.NewReader(body))
@@ -457,6 +569,30 @@ func (o *OryProvider) fetchIdentity(identityID string) (*kratosIdentity, error)
return &identity, nil
}
+func (o *OryProvider) fetchIdentityFull(identityID string) (*kratosIdentityFull, error) {
+ req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, fmt.Sprintf("%s/admin/identities/%s", o.KratosAdminURL, identityID), nil)
+ if err != nil {
+ return nil, fmt.Errorf("ory provider: build identity get failed: %w", err)
+ }
+
+ resp, err := o.httpClient().Do(req)
+ if err != nil {
+ return nil, fmt.Errorf("ory provider: identity get failed: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode >= 300 {
+ body, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
+ return nil, fmt.Errorf("ory provider: identity get failed status=%d body=%s", resp.StatusCode, string(body))
+ }
+
+ var identity kratosIdentityFull
+ if err := json.NewDecoder(resp.Body).Decode(&identity); err != nil {
+ return nil, fmt.Errorf("ory provider: decode identity failed: %w", err)
+ }
+ return &identity, nil
+}
+
// VerifyLoginCode는 Kratos 로그인 코드 제출로 세션을 발급합니다.
func (o *OryProvider) VerifyLoginCode(loginID, flowID, code string) (*domain.AuthInfo, error) {
if loginID == "" || flowID == "" || code == "" {
diff --git a/compose.infra.yaml b/compose.infra.yaml
index f53bfe4e..2baa1405 100644
--- a/compose.infra.yaml
+++ b/compose.infra.yaml
@@ -27,6 +27,7 @@ services:
clickhouse:
image: clickhouse/clickhouse-server:latest
container_name: baron_clickhouse
+ restart: always
environment:
CLICKHOUSE_USER: ${CLICKHOUSE_USER:-baron}
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:-password}
diff --git a/docker/ory/kratos/courier-templates/login_code/valid/sms.body.gotmpl b/docker/ory/kratos/courier-templates/login_code/valid/sms.body.gotmpl
index f3f6fdf4..cacba938 100644
--- a/docker/ory/kratos/courier-templates/login_code/valid/sms.body.gotmpl
+++ b/docker/ory/kratos/courier-templates/login_code/valid/sms.body.gotmpl
@@ -1,4 +1,4 @@
-[Baron SSO] 로그인 링크
+[Baron 통합로그인] 로그인 링크
# 운영 링크: https://app.brsw.kr/verify?loginId={{ .To }}&code={{ .LoginCode }}
http://localhost:5000/verify?loginId={{ .To }}&code={{ .LoginCode }}
코드: {{ .LoginCode }}
diff --git a/test/test_sms.py b/test/test_sms.py
index 3ce0c0bb..a8cc4520 100644
--- a/test/test_sms.py
+++ b/test/test_sms.py
@@ -61,7 +61,7 @@ def main():
"contentType": "COMM",
"countryCode": "82",
"from": sender_phone,
- "content": "[Baron SSO] Test message from Python script.",
+ "content": "[Baron 통합로그인] Test message from Python script.",
"messages": [
{
"to": recipient_phone
diff --git a/userfront/lib/core/services/auth_proxy_service.dart b/userfront/lib/core/services/auth_proxy_service.dart
index 0cc15c0e..43bd815d 100644
--- a/userfront/lib/core/services/auth_proxy_service.dart
+++ b/userfront/lib/core/services/auth_proxy_service.dart
@@ -104,15 +104,38 @@ class AuthProxyService {
}
}
- static Future