forked from baron/baron-sso
userfront 연동이력 맞춤
This commit is contained in:
@@ -521,6 +521,228 @@ func normalizePhoneForLoginID(phone string) string {
|
||||
return normalized
|
||||
}
|
||||
|
||||
func buildOidcClaimsFromTraits(traits map[string]any, scopes []string) map[string]any {
|
||||
claims := map[string]any{}
|
||||
if traits == nil {
|
||||
return claims
|
||||
}
|
||||
|
||||
scopeSet := map[string]struct{}{}
|
||||
for _, scope := range scopes {
|
||||
scope = strings.TrimSpace(scope)
|
||||
if scope == "" {
|
||||
continue
|
||||
}
|
||||
scopeSet[scope] = struct{}{}
|
||||
}
|
||||
|
||||
getString := func(key string) string {
|
||||
raw, ok := traits[key]
|
||||
if !ok || raw == nil {
|
||||
return ""
|
||||
}
|
||||
switch value := raw.(type) {
|
||||
case string:
|
||||
return strings.TrimSpace(value)
|
||||
default:
|
||||
return strings.TrimSpace(fmt.Sprint(value))
|
||||
}
|
||||
}
|
||||
|
||||
displayName := getString("displayname")
|
||||
if displayName == "" {
|
||||
displayName = getString("name")
|
||||
}
|
||||
if displayName != "" {
|
||||
claims["name"] = displayName
|
||||
}
|
||||
primaryEmail := getString("primary_email")
|
||||
if primaryEmail == "" {
|
||||
primaryEmail = getString("email")
|
||||
}
|
||||
if primaryEmail != "" {
|
||||
claims["email"] = primaryEmail
|
||||
}
|
||||
|
||||
if _, ok := scopeSet["profile"]; ok {
|
||||
profile := map[string]any{}
|
||||
names := map[string]any{}
|
||||
for _, key := range []string{
|
||||
"name",
|
||||
"displayname",
|
||||
"preferred_username",
|
||||
"given_name",
|
||||
"family_name",
|
||||
"middle_name",
|
||||
"nickname",
|
||||
} {
|
||||
if value := getString(key); value != "" {
|
||||
names[key] = value
|
||||
}
|
||||
}
|
||||
if len(names) > 0 {
|
||||
profile["names"] = names
|
||||
}
|
||||
|
||||
emails := collectEmailList(traits, primaryEmail)
|
||||
if len(emails) > 0 {
|
||||
profile["emails"] = emails
|
||||
}
|
||||
if len(profile) > 0 {
|
||||
claims["profile"] = profile
|
||||
}
|
||||
|
||||
for _, key := range []string{
|
||||
"department",
|
||||
"affiliationType",
|
||||
"companyCode",
|
||||
"displayname",
|
||||
"team",
|
||||
"grade",
|
||||
"familyCompany",
|
||||
"taxCode",
|
||||
"familyUniqueKey",
|
||||
"personal",
|
||||
} {
|
||||
if raw, ok := traits[key]; ok && raw != nil {
|
||||
switch value := raw.(type) {
|
||||
case string:
|
||||
if strings.TrimSpace(value) != "" {
|
||||
claims[key] = strings.TrimSpace(value)
|
||||
}
|
||||
default:
|
||||
claims[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if _, ok := scopeSet["phone"]; ok {
|
||||
if phone := getString("phone_number"); phone != "" {
|
||||
claims["phone_number"] = phone
|
||||
}
|
||||
}
|
||||
|
||||
return claims
|
||||
}
|
||||
|
||||
func collectEmailList(traits map[string]any, primaryEmail string) []string {
|
||||
emails := make([]string, 0)
|
||||
seen := make(map[string]struct{})
|
||||
add := func(value string) {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return
|
||||
}
|
||||
if _, ok := seen[value]; ok {
|
||||
return
|
||||
}
|
||||
seen[value] = struct{}{}
|
||||
emails = append(emails, value)
|
||||
}
|
||||
|
||||
add(primaryEmail)
|
||||
for _, key := range []string{"email", "primary_email"} {
|
||||
if raw, ok := traits[key]; ok {
|
||||
if value, ok := raw.(string); ok {
|
||||
add(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if raw, ok := traits["emails"]; ok {
|
||||
switch value := raw.(type) {
|
||||
case []string:
|
||||
for _, email := range value {
|
||||
add(email)
|
||||
}
|
||||
case []any:
|
||||
for _, email := range value {
|
||||
add(fmt.Sprint(email))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if raw, ok := traits["secondary_emails"]; ok {
|
||||
switch value := raw.(type) {
|
||||
case []string:
|
||||
for _, email := range value {
|
||||
add(email)
|
||||
}
|
||||
case []any:
|
||||
for _, email := range value {
|
||||
add(fmt.Sprint(email))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if raw, ok := traits["additional_emails"]; ok {
|
||||
switch value := raw.(type) {
|
||||
case []string:
|
||||
for _, email := range value {
|
||||
add(email)
|
||||
}
|
||||
case []any:
|
||||
for _, email := range value {
|
||||
add(fmt.Sprint(email))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return emails
|
||||
}
|
||||
|
||||
func buildIdentityLookupCandidates(loginID string) []string {
|
||||
seen := make(map[string]struct{})
|
||||
add := func(value string) {
|
||||
candidate := strings.TrimSpace(value)
|
||||
if candidate == "" {
|
||||
return
|
||||
}
|
||||
if _, ok := seen[candidate]; ok {
|
||||
return
|
||||
}
|
||||
seen[candidate] = struct{}{}
|
||||
}
|
||||
|
||||
normalized := strings.TrimSpace(loginID)
|
||||
add(normalized)
|
||||
if normalized != "" {
|
||||
add(strings.ToLower(normalized))
|
||||
}
|
||||
if normalized != "" && !strings.Contains(normalized, "@") {
|
||||
add(normalizePhoneForLoginID(normalized))
|
||||
}
|
||||
|
||||
candidates := make([]string, 0, len(seen))
|
||||
for candidate := range seen {
|
||||
candidates = append(candidates, candidate)
|
||||
}
|
||||
return candidates
|
||||
}
|
||||
|
||||
func (h *AuthHandler) resolveKratosIdentityID(ctx context.Context, identifiers ...string) (string, error) {
|
||||
if h.KratosAdmin == nil {
|
||||
return "", fmt.Errorf("kratos admin unavailable")
|
||||
}
|
||||
for _, identifier := range identifiers {
|
||||
candidate := strings.TrimSpace(identifier)
|
||||
if candidate == "" {
|
||||
continue
|
||||
}
|
||||
identityID, err := h.KratosAdmin.FindIdentityIDByIdentifier(ctx, candidate)
|
||||
if err == nil && identityID != "" {
|
||||
return identityID, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("kratos identity not found")
|
||||
}
|
||||
|
||||
func (h *AuthHandler) resolveKratosIdentityIDFromLoginID(ctx context.Context, loginID string) (string, error) {
|
||||
candidates := buildIdentityLookupCandidates(loginID)
|
||||
return h.resolveKratosIdentityID(ctx, candidates...)
|
||||
}
|
||||
|
||||
func (h *AuthHandler) getSignupState(key string) (*signupState, error) {
|
||||
val, err := h.RedisService.Get(key)
|
||||
if err != nil || val == "" {
|
||||
@@ -1117,6 +1339,12 @@ func (h *AuthHandler) VerifyLoginCode(c *fiber.Ctx) error {
|
||||
if authInfo == nil || authInfo.SessionToken == nil || authInfo.SessionToken.JWT == "" {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to issue session"})
|
||||
}
|
||||
subject, resolveErr := h.resolveKratosIdentityIDFromLoginID(c.Context(), lookupLoginID)
|
||||
if resolveErr != nil || subject == "" {
|
||||
slog.Error("[LoginCode] Failed to resolve kratos identity", "loginID", lookupLoginID, "error", resolveErr)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to resolve user identity"})
|
||||
}
|
||||
authInfo.Subject = subject
|
||||
c.Locals("login_id", lookupLoginID)
|
||||
setSessionIDLocal(c, authInfo.SessionToken)
|
||||
|
||||
@@ -1140,7 +1368,7 @@ func (h *AuthHandler) VerifyLoginCode(c *fiber.Ctx) error {
|
||||
"status": "approved",
|
||||
"pendingRef": pendingRef,
|
||||
"provider": h.IdpProvider.Name(),
|
||||
"subject": authInfo.Subject,
|
||||
"subject": subject,
|
||||
"message": "Login approved",
|
||||
})
|
||||
}
|
||||
@@ -1149,7 +1377,7 @@ func (h *AuthHandler) VerifyLoginCode(c *fiber.Ctx) error {
|
||||
"token": authInfo.SessionToken.JWT,
|
||||
"sessionJwt": authInfo.SessionToken.JWT,
|
||||
"provider": h.IdpProvider.Name(),
|
||||
"subject": authInfo.Subject,
|
||||
"subject": subject,
|
||||
"message": "Login successful",
|
||||
})
|
||||
}
|
||||
@@ -1226,6 +1454,12 @@ func (h *AuthHandler) VerifyLoginShortCode(c *fiber.Ctx) error {
|
||||
if authInfo == nil || authInfo.SessionToken == nil || authInfo.SessionToken.JWT == "" {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to issue session"})
|
||||
}
|
||||
subject, resolveErr := h.resolveKratosIdentityIDFromLoginID(c.Context(), payload.LoginID)
|
||||
if resolveErr != nil || subject == "" {
|
||||
slog.Error("[LoginShortCode] Failed to resolve kratos identity", "loginID", payload.LoginID, "error", resolveErr)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to resolve user identity"})
|
||||
}
|
||||
authInfo.Subject = subject
|
||||
c.Locals("login_id", payload.LoginID)
|
||||
setSessionIDLocal(c, authInfo.SessionToken)
|
||||
|
||||
@@ -1247,7 +1481,7 @@ func (h *AuthHandler) VerifyLoginShortCode(c *fiber.Ctx) error {
|
||||
"token": authInfo.SessionToken.JWT,
|
||||
"sessionJwt": authInfo.SessionToken.JWT,
|
||||
"provider": h.IdpProvider.Name(),
|
||||
"subject": authInfo.Subject,
|
||||
"subject": subject,
|
||||
"message": "Login approved",
|
||||
})
|
||||
}
|
||||
@@ -1256,7 +1490,7 @@ func (h *AuthHandler) VerifyLoginShortCode(c *fiber.Ctx) error {
|
||||
"token": authInfo.SessionToken.JWT,
|
||||
"sessionJwt": authInfo.SessionToken.JWT,
|
||||
"provider": h.IdpProvider.Name(),
|
||||
"subject": authInfo.Subject,
|
||||
"subject": subject,
|
||||
"message": "Login successful",
|
||||
})
|
||||
}
|
||||
@@ -1311,6 +1545,13 @@ func (h *AuthHandler) PasswordLogin(c *fiber.Ctx) error {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid credentials"})
|
||||
}
|
||||
|
||||
subject, resolveErr := h.resolveKratosIdentityIDFromLoginID(c.Context(), loginID)
|
||||
if resolveErr != nil || subject == "" {
|
||||
slog.Error("Failed to resolve kratos identity after login", "loginID", loginID, "error", resolveErr)
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to resolve user identity")
|
||||
}
|
||||
authInfo.Subject = subject
|
||||
|
||||
ale.Status = fiber.StatusOK
|
||||
ale.LatencyMs = time.Since(startTime)
|
||||
ale.SessionJwt = authInfo.SessionToken.JWT
|
||||
@@ -1320,7 +1561,7 @@ func (h *AuthHandler) PasswordLogin(c *fiber.Ctx) error {
|
||||
// --- OIDC 로그인 흐름 처리 ---
|
||||
if req.LoginChallenge != "" {
|
||||
slog.Info("OIDC login flow detected", "challenge", req.LoginChallenge)
|
||||
acceptResp, err := h.Hydra.AcceptLoginRequest(c.Context(), req.LoginChallenge, authInfo.Subject)
|
||||
acceptResp, err := h.Hydra.AcceptLoginRequest(c.Context(), req.LoginChallenge, subject)
|
||||
if err != nil {
|
||||
slog.Error("failed to accept hydra login request", "error", err)
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to accept OIDC login request")
|
||||
@@ -2326,12 +2567,21 @@ func (h *AuthHandler) GetMe(c *fiber.Ctx) error {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to load user profile"})
|
||||
}
|
||||
|
||||
identityID, resolveErr := h.resolveKratosIdentityID(
|
||||
c.Context(),
|
||||
userResponse.Email,
|
||||
normalizePhoneForLoginID(userResponse.Phone),
|
||||
)
|
||||
if resolveErr != nil || identityID == "" {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to resolve user identity"})
|
||||
}
|
||||
|
||||
dept, _ := userResponse.CustomAttributes["department"].(string)
|
||||
affType, _ := userResponse.CustomAttributes["affiliationType"].(string)
|
||||
compCode, _ := userResponse.CustomAttributes["companyCode"].(string)
|
||||
|
||||
resp := domain.UserProfileResponse{
|
||||
ID: userResponse.UserID,
|
||||
ID: identityID,
|
||||
Email: userResponse.Email,
|
||||
Name: userResponse.Name,
|
||||
Phone: h.formatPhoneForDisplay(userResponse.Phone),
|
||||
@@ -3033,24 +3283,43 @@ func (h *AuthHandler) ListLinkedRps(c *fiber.Ctx) error {
|
||||
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "hydra admin unavailable"})
|
||||
}
|
||||
|
||||
subject, err := h.resolveConsentSubject(c)
|
||||
if err != nil || subject == "" {
|
||||
subjects, err := h.resolveConsentSubjects(c)
|
||||
if err != nil || len(subjects) == 0 {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid session"})
|
||||
}
|
||||
|
||||
sessions, err := h.Hydra.ListConsentSessions(c.Context(), subject, "")
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
var sessions []service.HydraConsentSession
|
||||
var lastErr error
|
||||
hasSuccess := false
|
||||
for _, subject := range subjects {
|
||||
subject = strings.TrimSpace(subject)
|
||||
if subject == "" {
|
||||
continue
|
||||
}
|
||||
linked, listErr := h.Hydra.ListConsentSessions(c.Context(), subject, "")
|
||||
if listErr != nil {
|
||||
lastErr = listErr
|
||||
continue
|
||||
}
|
||||
hasSuccess = true
|
||||
sessions = append(sessions, linked...)
|
||||
}
|
||||
if !hasSuccess && lastErr != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": lastErr.Error()})
|
||||
}
|
||||
|
||||
records := make(map[string]*linkedRpRecord)
|
||||
for _, session := range sessions {
|
||||
clientID := strings.TrimSpace(session.Client.ClientID)
|
||||
client := session.Client
|
||||
if client.ClientID == "" && session.ConsentRequest != nil {
|
||||
client = session.ConsentRequest.Client
|
||||
}
|
||||
clientID := strings.TrimSpace(client.ClientID)
|
||||
if clientID == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
name := strings.TrimSpace(session.Client.ClientName)
|
||||
name := strings.TrimSpace(client.ClientName)
|
||||
if name == "" {
|
||||
name = clientID
|
||||
}
|
||||
@@ -3060,11 +3329,13 @@ func (h *AuthHandler) ListLinkedRps(c *fiber.Ctx) error {
|
||||
lastAuth = *session.AuthenticatedAt
|
||||
} else if session.RequestedAt != nil {
|
||||
lastAuth = *session.RequestedAt
|
||||
} else if session.HandledAt != nil {
|
||||
lastAuth = *session.HandledAt
|
||||
}
|
||||
|
||||
scopes := session.GrantedScope
|
||||
if len(scopes) == 0 && strings.TrimSpace(session.Client.Scope) != "" {
|
||||
scopes = strings.Fields(session.Client.Scope)
|
||||
if len(scopes) == 0 && strings.TrimSpace(client.Scope) != "" {
|
||||
scopes = strings.Fields(client.Scope)
|
||||
}
|
||||
|
||||
existing := records[clientID]
|
||||
@@ -3073,8 +3344,8 @@ func (h *AuthHandler) ListLinkedRps(c *fiber.Ctx) error {
|
||||
linkedRpSummary: linkedRpSummary{
|
||||
ID: clientID,
|
||||
Name: name,
|
||||
Logo: extractHydraClientLogo(session.Client.Metadata),
|
||||
Status: hydraClientStatus(session.Client.Metadata),
|
||||
Logo: extractHydraClientLogo(client.Metadata),
|
||||
Status: hydraClientStatus(client.Metadata),
|
||||
Scopes: scopes,
|
||||
},
|
||||
lastAuth: lastAuth,
|
||||
@@ -3086,7 +3357,7 @@ func (h *AuthHandler) ListLinkedRps(c *fiber.Ctx) error {
|
||||
existing.Name = name
|
||||
}
|
||||
if existing.Logo == "" {
|
||||
existing.Logo = extractHydraClientLogo(session.Client.Metadata)
|
||||
existing.Logo = extractHydraClientLogo(client.Metadata)
|
||||
}
|
||||
existing.Scopes = mergeScopes(existing.Scopes, scopes)
|
||||
if lastAuth.After(existing.lastAuth) {
|
||||
@@ -3145,7 +3416,19 @@ func (h *AuthHandler) AcceptConsentRequest(c *fiber.Ctx) error {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get consent information")
|
||||
}
|
||||
|
||||
acceptResp, err := h.Hydra.AcceptConsentRequest(c.Context(), req.ConsentChallenge, consentRequest)
|
||||
if consentRequest.Subject == "" {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Consent subject missing")
|
||||
}
|
||||
if h.KratosAdmin == nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Kratos admin unavailable")
|
||||
}
|
||||
identity, err := h.KratosAdmin.GetIdentity(c.Context(), consentRequest.Subject)
|
||||
if err != nil || identity == nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to load identity")
|
||||
}
|
||||
sessionClaims := buildOidcClaimsFromTraits(identity.Traits, consentRequest.RequestedScope)
|
||||
|
||||
acceptResp, err := h.Hydra.AcceptConsentRequest(c.Context(), req.ConsentChallenge, consentRequest, sessionClaims)
|
||||
if err != nil {
|
||||
slog.Error("failed to accept hydra consent request", "error", err)
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to accept consent request")
|
||||
@@ -3154,6 +3437,30 @@ func (h *AuthHandler) AcceptConsentRequest(c *fiber.Ctx) error {
|
||||
return c.JSON(acceptResp)
|
||||
}
|
||||
|
||||
func (h *AuthHandler) AcceptOidcLoginRequest(c *fiber.Ctx) error {
|
||||
var req struct {
|
||||
LoginChallenge string `json:"login_challenge"`
|
||||
}
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||
}
|
||||
if req.LoginChallenge == "" {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "login_challenge is required")
|
||||
}
|
||||
|
||||
subject, err := h.resolveConsentSubject(c)
|
||||
if err != nil || subject == "" {
|
||||
return fiber.NewError(fiber.StatusUnauthorized, "Authentication required")
|
||||
}
|
||||
|
||||
acceptResp, err := h.Hydra.AcceptLoginRequest(c.Context(), req.LoginChallenge, subject)
|
||||
if err != nil {
|
||||
slog.Error("failed to accept hydra login request", "error", err)
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to accept OIDC login request")
|
||||
}
|
||||
|
||||
return c.JSON(acceptResp)
|
||||
}
|
||||
|
||||
func (h *AuthHandler) resolveCurrentProfile(c *fiber.Ctx) (*domain.UserProfileResponse, error) {
|
||||
token := h.getBearerToken(c)
|
||||
@@ -3165,11 +3472,19 @@ func (h *AuthHandler) resolveCurrentProfile(c *fiber.Ctx) (*domain.UserProfileRe
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
identityID, resolveErr := h.resolveKratosIdentityID(
|
||||
c.Context(),
|
||||
userResponse.Email,
|
||||
normalizePhoneForLoginID(userResponse.Phone),
|
||||
)
|
||||
if resolveErr != nil || identityID == "" {
|
||||
return nil, fmt.Errorf("failed to resolve kratos identity for profile")
|
||||
}
|
||||
dept, _ := userResponse.CustomAttributes["department"].(string)
|
||||
affType, _ := userResponse.CustomAttributes["affiliationType"].(string)
|
||||
compCode, _ := userResponse.CustomAttributes["companyCode"].(string)
|
||||
return &domain.UserProfileResponse{
|
||||
ID: userResponse.UserID,
|
||||
ID: identityID,
|
||||
Email: userResponse.Email,
|
||||
Name: userResponse.Name,
|
||||
Phone: h.formatPhoneForDisplay(userResponse.Phone),
|
||||
@@ -3197,27 +3512,34 @@ func (h *AuthHandler) resolveCurrentProfile(c *fiber.Ctx) (*domain.UserProfileRe
|
||||
func (h *AuthHandler) resolveConsentSubject(c *fiber.Ctx) (string, error) {
|
||||
token := h.getBearerToken(c)
|
||||
if token != "" {
|
||||
if looksLikeJWT(token) && h.DescopeClient != nil && h.KratosAdmin != nil {
|
||||
if looksLikeJWT(token) && h.DescopeClient != nil {
|
||||
authorized, userToken, err := h.DescopeClient.Auth.ValidateSessionWithToken(c.Context(), token)
|
||||
if err == nil && authorized {
|
||||
userResponse, loadErr := h.DescopeClient.Management.User().Load(c.Context(), userToken.ID)
|
||||
if loadErr == nil {
|
||||
if email := strings.TrimSpace(userResponse.Email); email != "" {
|
||||
if identityID, err := h.KratosAdmin.FindIdentityIDByIdentifier(c.Context(), email); err == nil && identityID != "" {
|
||||
return identityID, nil
|
||||
}
|
||||
}
|
||||
if phone := strings.TrimSpace(userResponse.Phone); phone != "" {
|
||||
normalized := normalizePhoneForLoginID(phone)
|
||||
if identityID, err := h.KratosAdmin.FindIdentityIDByIdentifier(c.Context(), normalized); err == nil && identityID != "" {
|
||||
return identityID, nil
|
||||
}
|
||||
identityID, resolveErr := h.resolveKratosIdentityID(
|
||||
c.Context(),
|
||||
userResponse.Email,
|
||||
normalizePhoneForLoginID(userResponse.Phone),
|
||||
)
|
||||
if resolveErr == nil {
|
||||
return identityID, nil
|
||||
}
|
||||
}
|
||||
return userToken.ID, nil
|
||||
return "", fmt.Errorf("failed to resolve kratos identity for consent subject")
|
||||
}
|
||||
}
|
||||
return h.resolveIdentityID(c, token)
|
||||
identityID, resolveErr := h.resolveIdentityID(c, token)
|
||||
if resolveErr == nil && identityID != "" {
|
||||
return identityID, nil
|
||||
}
|
||||
if cookie := c.Get("Cookie"); cookie != "" {
|
||||
cookieID, _, cookieErr := h.getKratosIdentityWithCookie(cookie)
|
||||
if cookieErr == nil && cookieID != "" {
|
||||
return cookieID, nil
|
||||
}
|
||||
}
|
||||
return "", resolveErr
|
||||
}
|
||||
cookie := c.Get("Cookie")
|
||||
if cookie == "" {
|
||||
@@ -3227,6 +3549,114 @@ func (h *AuthHandler) resolveConsentSubject(c *fiber.Ctx) (string, error) {
|
||||
return identityID, err
|
||||
}
|
||||
|
||||
func (h *AuthHandler) resolveConsentSubjects(c *fiber.Ctx) ([]string, error) {
|
||||
token := h.getBearerToken(c)
|
||||
if token != "" && looksLikeJWT(token) && h.DescopeClient != nil {
|
||||
authorized, userToken, err := h.DescopeClient.Auth.ValidateSessionWithToken(c.Context(), token)
|
||||
if err == nil && authorized {
|
||||
subjects := make([]string, 0, 2)
|
||||
userResponse, loadErr := h.DescopeClient.Management.User().Load(c.Context(), userToken.ID)
|
||||
if loadErr == nil {
|
||||
subjects = appendLoginIDsFromValues(subjects, userResponse.Email, userResponse.Phone)
|
||||
identityID, resolveErr := h.resolveKratosIdentityID(
|
||||
c.Context(),
|
||||
userResponse.Email,
|
||||
normalizePhoneForLoginID(userResponse.Phone),
|
||||
)
|
||||
if resolveErr == nil && identityID != "" {
|
||||
subjects = append([]string{identityID}, subjects...)
|
||||
}
|
||||
}
|
||||
return uniqueStrings(subjects), nil
|
||||
}
|
||||
}
|
||||
|
||||
if token != "" {
|
||||
identityID, traits, err := h.getKratosIdentity(token)
|
||||
if err == nil && identityID != "" {
|
||||
subjects := []string{identityID}
|
||||
subjects = appendLoginIDsFromTraits(subjects, traits)
|
||||
return uniqueStrings(subjects), nil
|
||||
}
|
||||
}
|
||||
|
||||
cookie := c.Get("Cookie")
|
||||
if cookie == "" {
|
||||
return nil, fmt.Errorf("missing authorization token")
|
||||
}
|
||||
identityID, traits, err := h.getKratosIdentityWithCookie(cookie)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
subjects := []string{identityID}
|
||||
subjects = appendLoginIDsFromTraits(subjects, traits)
|
||||
return uniqueStrings(subjects), nil
|
||||
}
|
||||
|
||||
func uniqueStrings(items []string) []string {
|
||||
seen := make(map[string]struct{}, len(items))
|
||||
result := make([]string, 0, len(items))
|
||||
for _, item := range items {
|
||||
item = strings.TrimSpace(item)
|
||||
if item == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[item]; ok {
|
||||
continue
|
||||
}
|
||||
seen[item] = struct{}{}
|
||||
result = append(result, item)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func appendLoginIDsFromValues(subjects []string, email string, phone string) []string {
|
||||
if strings.TrimSpace(email) != "" {
|
||||
subjects = append(subjects, strings.TrimSpace(email))
|
||||
}
|
||||
if strings.TrimSpace(phone) != "" {
|
||||
subjects = append(subjects, normalizePhoneForLoginID(phone))
|
||||
}
|
||||
return subjects
|
||||
}
|
||||
|
||||
func appendLoginIDsFromTraits(subjects []string, traits map[string]interface{}) []string {
|
||||
if traits == nil {
|
||||
return subjects
|
||||
}
|
||||
if raw, ok := traits["email"]; ok {
|
||||
if value, ok := raw.(string); ok && strings.TrimSpace(value) != "" {
|
||||
subjects = append(subjects, strings.TrimSpace(value))
|
||||
}
|
||||
}
|
||||
if raw, ok := traits["phone"]; ok {
|
||||
if value, ok := raw.(string); ok && strings.TrimSpace(value) != "" {
|
||||
subjects = append(subjects, normalizePhoneForLoginID(value))
|
||||
}
|
||||
}
|
||||
if raw, ok := traits["phone_number"]; ok {
|
||||
if value, ok := raw.(string); ok && strings.TrimSpace(value) != "" {
|
||||
subjects = append(subjects, normalizePhoneForLoginID(value))
|
||||
}
|
||||
}
|
||||
if raw, ok := traits["phoneNumber"]; ok {
|
||||
if value, ok := raw.(string); ok && strings.TrimSpace(value) != "" {
|
||||
subjects = append(subjects, normalizePhoneForLoginID(value))
|
||||
}
|
||||
}
|
||||
if raw, ok := traits["mobile"]; ok {
|
||||
if value, ok := raw.(string); ok && strings.TrimSpace(value) != "" {
|
||||
subjects = append(subjects, normalizePhoneForLoginID(value))
|
||||
}
|
||||
}
|
||||
if raw, ok := traits["mobile_number"]; ok {
|
||||
if value, ok := raw.(string); ok && strings.TrimSpace(value) != "" {
|
||||
subjects = append(subjects, normalizePhoneForLoginID(value))
|
||||
}
|
||||
}
|
||||
return subjects
|
||||
}
|
||||
|
||||
func isAuthEventType(eventType string) bool {
|
||||
normalized := strings.ToLower(eventType)
|
||||
return strings.Contains(normalized, " /api/v1/auth/")
|
||||
@@ -3662,7 +4092,22 @@ func (h *AuthHandler) resolveIdentityID(c *fiber.Ctx, token string) (string, err
|
||||
if looksLikeJWT(token) && h.DescopeClient != nil {
|
||||
authorized, userToken, err := h.DescopeClient.Auth.ValidateSessionWithToken(c.Context(), token)
|
||||
if err == nil && authorized {
|
||||
return userToken.ID, nil
|
||||
if h.KratosAdmin == nil {
|
||||
return "", fmt.Errorf("kratos admin unavailable")
|
||||
}
|
||||
userResponse, loadErr := h.DescopeClient.Management.User().Load(c.Context(), userToken.ID)
|
||||
if loadErr != nil {
|
||||
return "", loadErr
|
||||
}
|
||||
identityID, resolveErr := h.resolveKratosIdentityID(
|
||||
c.Context(),
|
||||
userResponse.Email,
|
||||
normalizePhoneForLoginID(userResponse.Phone),
|
||||
)
|
||||
if resolveErr != nil || identityID == "" {
|
||||
return "", fmt.Errorf("failed to resolve kratos identity for token")
|
||||
}
|
||||
return identityID, nil
|
||||
}
|
||||
}
|
||||
id, _, err := h.getKratosIdentity(token)
|
||||
@@ -3688,7 +4133,7 @@ func (h *AuthHandler) tryIssueDescopeQrSession(c *fiber.Ctx, token string) (*dom
|
||||
if authInfo == nil || authInfo.SessionToken == nil || authInfo.SessionToken.JWT == "" {
|
||||
return nil, "", "", fmt.Errorf("descope issue session returned empty token")
|
||||
}
|
||||
return authInfo.SessionToken, loginID, userToken.ID, nil
|
||||
return authInfo.SessionToken, loginID, "", nil
|
||||
}
|
||||
|
||||
func (h *AuthHandler) resolveKratosLoginID(token string) (string, error) {
|
||||
@@ -4204,16 +4649,24 @@ func (h *AuthHandler) UpdateMe(c *fiber.Ctx) error {
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to load current user"})
|
||||
}
|
||||
identityID, resolveErr := h.resolveKratosIdentityID(
|
||||
c.Context(),
|
||||
currentUser.Email,
|
||||
normalizePhoneForLoginID(currentUser.Phone),
|
||||
)
|
||||
if resolveErr != nil || identityID == "" {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to resolve user identity"})
|
||||
}
|
||||
|
||||
newPhoneStorage := h.formatPhoneForStorage(req.Phone)
|
||||
oldPhoneStorage := currentUser.Phone
|
||||
|
||||
slog.Info("[UpdateMe] Checking changes", "userID", userToken.ID, "oldPhone", oldPhoneStorage, "newPhone", newPhoneStorage, "newName", req.Name)
|
||||
slog.Info("[UpdateMe] Checking changes", "userID", identityID, "oldPhone", oldPhoneStorage, "newPhone", newPhoneStorage, "newName", req.Name)
|
||||
|
||||
// 2. Handle Phone Number Change
|
||||
if newPhoneStorage != "" && newPhoneStorage != oldPhoneStorage {
|
||||
// Check verification status in Redis
|
||||
verifyKey := "verify_update_phone:" + userToken.ID + ":" + newPhoneStorage
|
||||
verifyKey := "verify_update_phone:" + identityID + ":" + newPhoneStorage
|
||||
val, _ := h.RedisService.Get(verifyKey)
|
||||
if val != "verified" {
|
||||
slog.Warn("[UpdateMe] Phone verification missing", "key", verifyKey)
|
||||
@@ -4221,7 +4674,7 @@ func (h *AuthHandler) UpdateMe(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
// Update Phone in Descope and mark as verified
|
||||
slog.Info("[UpdateMe] Updating phone number", "userID", userToken.ID, "newPhone", newPhoneStorage)
|
||||
slog.Info("[UpdateMe] Updating phone number", "userID", identityID, "newPhone", newPhoneStorage)
|
||||
_, err = h.DescopeClient.Management.User().UpdatePhone(c.Context(), userToken.ID, newPhoneStorage, true, false)
|
||||
if err != nil {
|
||||
slog.Error("Failed to update phone in Descope", "error", err)
|
||||
@@ -4250,7 +4703,7 @@ func (h *AuthHandler) UpdateMe(c *fiber.Ctx) error {
|
||||
|
||||
// 3. Update Name if changed
|
||||
if req.Name != "" && req.Name != currentUser.Name {
|
||||
slog.Info("[UpdateMe] Updating display name", "userID", userToken.ID, "newName", req.Name)
|
||||
slog.Info("[UpdateMe] Updating display name", "userID", identityID, "newName", req.Name)
|
||||
_, err = h.DescopeClient.Management.User().UpdateDisplayName(c.Context(), userToken.ID, req.Name)
|
||||
if err != nil {
|
||||
slog.Error("Failed to update user name", "error", err)
|
||||
@@ -4260,13 +4713,13 @@ func (h *AuthHandler) UpdateMe(c *fiber.Ctx) error {
|
||||
|
||||
// 4. Update Custom Attributes (Department)
|
||||
if req.Department != "" {
|
||||
slog.Info("[UpdateMe] Updating department", "userID", userToken.ID, "dept", req.Department)
|
||||
slog.Info("[UpdateMe] Updating department", "userID", identityID, "dept", req.Department)
|
||||
if _, err := h.DescopeClient.Management.User().UpdateCustomAttribute(c.Context(), userToken.ID, "department", req.Department); err != nil {
|
||||
slog.Error("Failed to update department", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
slog.Info("[UpdateMe] Profile update completed successfully", "userID", userToken.ID)
|
||||
slog.Info("[UpdateMe] Profile update completed successfully", "userID", identityID)
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"status": "success",
|
||||
|
||||
Reference in New Issue
Block a user