1
0
forked from baron/baron-sso

feat: add robust login ID collision prevention and UI validation (#440)

- Add `ValidateLoginID` to enforce ID collision and security rules (prevents phone number collision, email format usage, and reserved words).
- Add `POST /api/v1/auth/signup/check-login-id` endpoint for real-time ID availability checks.
- Add `checkLoginIDAvailability` API call to userfront's `AuthProxyService`.
- Implement "Check Duplication" button and error/success messaging for the Login ID field in the signup screen.
- Add "000000" magic code bypass for `VerifySignupCode` in non-production environments to streamline testing.
This commit is contained in:
2026-03-27 11:19:28 +09:00
parent aa60a22d57
commit 75cc6737bd
10 changed files with 257 additions and 14 deletions

View File

@@ -194,6 +194,35 @@ func (h *AuthHandler) CheckEmail(c *fiber.Ctx) error {
return c.JSON(fiber.Map{"available": true})
}
// CheckLoginID - 로그인 ID 사용 가능 여부를 확인합니다.
func (h *AuthHandler) CheckLoginID(c *fiber.Ctx) error {
var req domain.CheckLoginIDRequest
if err := c.BodyParser(&req); err != nil {
return errorJSON(c, fiber.StatusBadRequest, "Invalid request")
}
if h.IdpProvider == nil {
return errorJSON(c, fiber.StatusServiceUnavailable, "Identity provider unavailable")
}
// Basic validation via our ValidateLoginID helper (without email/phone since we just check format & collision with reserved words)
if err := domain.ValidateLoginID(req.LoginID, "", ""); err != nil {
return c.JSON(fiber.Map{"available": false, "message": err.Error()})
}
// We don't prepend companyCode to Kratos lookup if traits.id is unique globally
// Assuming Kratos traits.id handles unique constraints per tenant or globally based on schema
exists, err := h.IdpProvider.UserExists(req.LoginID)
if err != nil {
return errorJSON(c, fiber.StatusServiceUnavailable, "Identity provider unavailable")
}
if exists {
return c.JSON(fiber.Map{"available": false, "message": "ID already registered"})
}
return c.JSON(fiber.Map{"available": true})
}
// SendSignupEmailCode - Sends verification code to email
func (h *AuthHandler) SendSignupEmailCode(c *fiber.Ctx) error {
var req domain.SendSignupCodeRequest
@@ -329,8 +358,9 @@ func (h *AuthHandler) VerifySignupCode(c *fiber.Ctx) error {
return errorJSON(c, fiber.StatusTooManyRequests, "Too many failed attempts")
}
// Check Code match
if state.Code != req.Code {
// Check Code match (Allow magic code 000000 in non-production environments)
isMagicCodeAllowed := service.IsDryRunAllowed() && req.Code == "000000"
if state.Code != req.Code && !isMagicCodeAllowed {
state.FailCount++
h.saveSignupState(key, state, signupStateExpiration)
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
@@ -465,9 +495,14 @@ func (h *AuthHandler) Signup(c *fiber.Ctx) error {
}
}
finalLoginID := extractTraitString(attributes, "id")
if err := domain.ValidateLoginID(finalLoginID, req.Email, normalizedPhone); err != nil {
return errorJSON(c, fiber.StatusBadRequest, err.Error())
}
brokerUser := &domain.BrokerUser{
Email: req.Email,
LoginID: extractTraitString(attributes, "id"),
LoginID: finalLoginID,
Name: req.Name,
PhoneNumber: normalizedPhone,
Attributes: attributes,
@@ -5315,6 +5350,13 @@ func (h *AuthHandler) UpdateMe(c *fiber.Ctx) error {
}
}
finalLoginID := extractTraitString(traits, "id")
userEmail := extractTraitString(traits, "email")
userPhone := extractTraitString(traits, "phone")
if err := domain.ValidateLoginID(finalLoginID, userEmail, userPhone); err != nil {
return errorJSON(c, fiber.StatusBadRequest, err.Error())
}
if err := h.updateKratosIdentity(identityID, traits); err != nil {
slog.Error("Failed to update profile in Kratos", "error", err)
return errorJSON(c, fiber.StatusInternalServerError, "프로필 업데이트에 실패했습니다.")