1
0
forked from baron/baron-sso

fix auth link session conflict policy

This commit is contained in:
2026-05-21 13:50:18 +09:00
parent 8dfe6fed82
commit f19b694c0b
3 changed files with 902 additions and 17 deletions

View File

@@ -2173,6 +2173,9 @@ func (h *AuthHandler) PollEnchantedLink(c *fiber.Ctx) error {
json.Unmarshal([]byte(val), &data)
if data["status"] == statusSuccess {
if blocked, err := h.rejectSessionSubjectOverwrite(c, data["subject"], data["loginId"]); blocked || err != nil {
return err
}
slog.Info("[Poll] Success", "pendingRef", req.PendingRef)
return c.JSON(fiber.Map{
"sessionJwt": data["jwt"],
@@ -2185,6 +2188,9 @@ func (h *AuthHandler) PollEnchantedLink(c *fiber.Ctx) error {
if err != nil {
return err
}
if authInfo == nil {
return nil
}
return c.JSON(fiber.Map{
"sessionJwt": authInfo.SessionToken.JWT,
@@ -2264,6 +2270,9 @@ func (h *AuthHandler) VerifyMagicLink(c *fiber.Ctx) error {
slog.Error("[Verify] IDP returned empty session")
return errorJSON(c, fiber.StatusInternalServerError, "Failed to issue session")
}
if blocked, err := h.rejectSessionSubjectOverwrite(c, authInfo.Subject, loginID); blocked || err != nil {
return err
}
sessionToken := authInfo.SessionToken.JWT
c.Locals("login_id", loginID)
setSessionIDLocal(c, authInfo.SessionToken)
@@ -2281,6 +2290,12 @@ func (h *AuthHandler) VerifyMagicLink(c *fiber.Ctx) error {
if sessionID != "" {
sessionData["session_id"] = sessionID
}
if authInfo.Subject != "" {
sessionData["subject"] = authInfo.Subject
}
if loginID != "" {
sessionData["loginId"] = loginID
}
sessionDataJSON, _ := json.Marshal(sessionData)
h.RedisService.Set(prefixSession+pendingRef, string(sessionDataJSON), defaultExpiration)
@@ -2381,6 +2396,9 @@ func (h *AuthHandler) VerifyLoginCode(c *fiber.Ctx) error {
return errorJSONCode(c, fiber.StatusInternalServerError, "internal_error", "Failed to resolve user identity")
}
authInfo.Subject = subject
if blocked, err := h.rejectSessionSubjectOverwrite(c, subject, lookupLoginID); blocked || err != nil {
return err
}
c.Locals("login_id", lookupLoginID)
setSessionIDLocal(c, authInfo.SessionToken)
@@ -2400,8 +2418,10 @@ func (h *AuthHandler) VerifyLoginCode(c *fiber.Ctx) error {
}
if pendingRef != "" {
sessionData, _ := json.Marshal(map[string]string{
"status": statusSuccess,
"jwt": authInfo.SessionToken.JWT,
"status": statusSuccess,
"jwt": authInfo.SessionToken.JWT,
"subject": subject,
"loginId": lookupLoginID,
})
h.RedisService.Set(prefixSession+pendingRef, string(sessionData), loginCodeExpiration)
h.RedisService.Delete(prefixLoginCodePending + lookupLoginID)
@@ -2505,6 +2525,9 @@ func (h *AuthHandler) VerifyLoginShortCode(c *fiber.Ctx) error {
return errorJSONCode(c, fiber.StatusInternalServerError, "internal_error", "Failed to resolve user identity")
}
authInfo.Subject = subject
if blocked, err := h.rejectSessionSubjectOverwrite(c, subject, payload.LoginID); blocked || err != nil {
return err
}
c.Locals("login_id", payload.LoginID)
setSessionIDLocal(c, authInfo.SessionToken)
@@ -2515,8 +2538,10 @@ func (h *AuthHandler) VerifyLoginShortCode(c *fiber.Ctx) error {
if payload.PendingRef != "" {
sessionData, _ := json.Marshal(map[string]string{
"status": statusSuccess,
"jwt": authInfo.SessionToken.JWT,
"status": statusSuccess,
"jwt": authInfo.SessionToken.JWT,
"subject": subject,
"loginId": payload.LoginID,
})
h.RedisService.Set(prefixSession+payload.PendingRef, string(sessionData), loginCodeExpiration)
h.RedisService.Delete(prefixLoginCodePending + payload.LoginID)
@@ -3018,6 +3043,96 @@ func (h *AuthHandler) loadHeadlessLinkState(pendingRef string) (headlessLinkStat
return state, true
}
func (h *AuthHandler) resolveCurrentBrowserSessionEvidence(c *fiber.Ctx) (string, string) {
if h == nil || c == nil {
return "", ""
}
if token := h.getBearerToken(c); token != "" {
if identityID, err := h.resolveIdentityID(c, token); err == nil && strings.TrimSpace(identityID) != "" {
sessionID, _ := h.getKratosSessionID(token)
return strings.TrimSpace(identityID), strings.TrimSpace(sessionID)
}
}
if cookie := strings.TrimSpace(c.Get("Cookie")); cookie != "" {
if identityID, _, _, sessionID, err := h.getKratosIdentityWithCookieAndSession(cookie); err == nil && strings.TrimSpace(identityID) != "" {
return strings.TrimSpace(identityID), strings.TrimSpace(sessionID)
}
}
return "", ""
}
func (h *AuthHandler) resolveCurrentBrowserSubject(c *fiber.Ctx) string {
identityID, _ := h.resolveCurrentBrowserSessionEvidence(c)
return identityID
}
func (h *AuthHandler) resolveHeadlessOIDCSubjectEvidence(c *fiber.Ctx, loginReq *domain.HydraLoginRequest, pendingRef string) string {
if loginReq != nil && loginReq.Skip && strings.TrimSpace(loginReq.Subject) != "" {
return strings.TrimSpace(loginReq.Subject)
}
if currentSubject := h.resolveCurrentBrowserSubject(c); currentSubject != "" {
return currentSubject
}
if meta, ok := h.loadLoginApproverMeta(pendingRef); ok && strings.TrimSpace(meta.ApproverSubject) != "" {
return strings.TrimSpace(meta.ApproverSubject)
}
return ""
}
func (h *AuthHandler) rejectSessionSubjectOverwrite(c *fiber.Ctx, targetSubject, targetLoginID string) (bool, error) {
currentSubject := h.resolveCurrentBrowserSubject(c)
if currentSubject == "" {
return false, nil
}
targetSubject = strings.TrimSpace(targetSubject)
if targetSubject == "" && strings.TrimSpace(targetLoginID) != "" && h.KratosAdmin != nil {
if resolved, err := h.resolveKratosIdentityIDFromLoginID(c.Context(), targetLoginID); err == nil {
targetSubject = strings.TrimSpace(resolved)
} else {
slog.Warn(
"session-changing login target subject resolution failed",
"loginID", targetLoginID,
"error", err,
)
}
}
if targetSubject == "" {
slog.Warn("session-changing login blocked because target subject is unknown", "current_subject", currentSubject)
return true, errorJSONCode(c, fiber.StatusConflict, "session_subject_conflict", "Current browser session must be signed out before signing in as another user")
}
if targetSubject != currentSubject {
slog.Warn(
"session-changing login blocked by subject conflict",
"target_subject", targetSubject,
"current_subject", currentSubject,
)
return true, errorJSONCode(c, fiber.StatusConflict, "session_subject_conflict", "Current browser session must be signed out before signing in as another user")
}
return false, nil
}
func (h *AuthHandler) rejectHeadlessOIDCSubjectConflict(c *fiber.Ctx, currentSubject, targetSubject string) (bool, error) {
currentSubject = strings.TrimSpace(currentSubject)
targetSubject = strings.TrimSpace(targetSubject)
if currentSubject == "" || targetSubject == "" || currentSubject == targetSubject {
return false, nil
}
slog.Warn(
"headless login blocked by OIDC/UserFront subject conflict",
"current_subject", currentSubject,
"target_subject", targetSubject,
)
return true, c.Status(fiber.StatusConflict).JSON(fiber.Map{
"error": "OIDC/UserFront subject conflicts with headless login target. Sign out of UserFront or restart through the standard UserFront login flow.",
"code": "oidc_subject_conflict",
"status": "oidc_subject_conflict",
"currentSubject": currentSubject,
"targetSubject": targetSubject,
"recommendedAction": "redirect_to_userfront_login",
})
}
func (h *AuthHandler) completeApprovedLinkLogin(c *fiber.Ctx, pendingRef string) (string, *domain.AuthInfo, error) {
val, err := h.RedisService.Get(prefixSession + pendingRef)
if err != nil || val == "" {
@@ -3076,6 +3191,9 @@ func (h *AuthHandler) completeApprovedLinkLogin(c *fiber.Ctx, pendingRef string)
if authInfo == nil || authInfo.SessionToken == nil || authInfo.SessionToken.JWT == "" {
return "", nil, errorJSON(c, fiber.StatusInternalServerError, "Failed to issue session")
}
if blocked, err := h.rejectSessionSubjectOverwrite(c, authInfo.Subject, loginID); blocked || err != nil {
return "", nil, err
}
c.Locals("login_id", loginID)
setSessionIDLocal(c, authInfo.SessionToken)
@@ -3099,6 +3217,12 @@ func (h *AuthHandler) completeApprovedLinkLogin(c *fiber.Ctx, pendingRef string)
if sessionID != "" {
sessionData["session_id"] = sessionID
}
if authInfo.Subject != "" {
sessionData["subject"] = authInfo.Subject
}
if loginID != "" {
sessionData["loginId"] = loginID
}
sessionDataJSON, _ := json.Marshal(sessionData)
_ = h.RedisService.Set(prefixSession+pendingRef, string(sessionDataJSON), defaultExpiration)
@@ -3186,6 +3310,16 @@ func (h *AuthHandler) HeadlessPasswordLogin(c *fiber.Ctx) error {
)
return errorJSONCode(c, status, code, message)
}
if authInfo == nil || strings.TrimSpace(authInfo.Subject) == "" {
return errorJSONCode(c, fiber.StatusInternalServerError, "internal_error", "Failed to resolve user identity")
}
if blocked, err := h.rejectHeadlessOIDCSubjectConflict(
c,
h.resolveHeadlessOIDCSubjectEvidence(c, loginReq, ""),
authInfo.Subject,
); blocked || err != nil {
return err
}
c.Locals("user_id", authInfo.Subject)
c.Locals("login_id", loginID)
@@ -3420,25 +3554,35 @@ func (h *AuthHandler) HeadlessLinkPoll(c *fiber.Ctx) error {
})
}
loginID := state.LoginID
if session["status"] == "approved" {
completedLoginID, _, err := h.completeApprovedLinkLogin(c, pendingRef)
if err != nil {
return err
loginID := strings.TrimSpace(state.LoginID)
targetSubject := strings.TrimSpace(session["subject"])
if session["status"] == "approved" || session["status"] == statusSuccess {
if storedLoginID := strings.TrimSpace(session["loginId"]); storedLoginID != "" {
loginID = storedLoginID
} else if storedLoginID := strings.TrimSpace(session["login_id"]); storedLoginID != "" {
loginID = storedLoginID
}
loginID = completedLoginID
}
if loginID == "" {
return errorJSON(c, fiber.StatusInternalServerError, "Failed to resolve approved user identity")
}
subject, err := h.resolveKratosIdentityIDFromLoginID(c.Context(), loginID)
if err != nil || subject == "" {
if targetSubject == "" {
targetSubject, err = h.resolveKratosIdentityIDFromLoginID(c.Context(), loginID)
}
if err != nil || targetSubject == "" {
slog.Error("failed to resolve kratos identity for headless link poll", "loginID", loginID, "error", err)
return errorJSON(c, fiber.StatusInternalServerError, "Failed to resolve user identity")
}
if blocked, err := h.rejectHeadlessOIDCSubjectConflict(
c,
h.resolveHeadlessOIDCSubjectEvidence(c, loginReq, pendingRef),
targetSubject,
); blocked || err != nil {
return err
}
acceptResp, err := h.Hydra.AcceptLoginRequest(c.Context(), state.LoginChallenge, subject)
acceptResp, err := h.Hydra.AcceptLoginRequest(c.Context(), state.LoginChallenge, targetSubject)
if err != nil {
slog.Error("failed to accept hydra login request in headless link poll", "error", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to accept OIDC login request")
@@ -3446,6 +3590,8 @@ func (h *AuthHandler) HeadlessLinkPoll(c *fiber.Ctx) error {
state.RedirectTo = acceptResp.RedirectTo
h.storeHeadlessLinkState(pendingRef, state, defaultExpiration)
h.writeLinkAuditLog(loginID, pendingRef, nil, c)
h.clearLoginMeta(pendingRef)
logOidcRedirectSummary("headless_link_poll", acceptResp.RedirectTo)
return c.JSON(fiber.Map{
"redirectTo": acceptResp.RedirectTo,
@@ -3532,6 +3678,12 @@ func (h *AuthHandler) PasswordLogin(c *fiber.Ctx) error {
ale.Status = fiber.StatusOK
ale.LatencyMs = time.Since(startTime)
if blocked, err := h.rejectSessionSubjectOverwrite(c, authInfo.Subject, loginID); blocked || err != nil {
ale.Status = fiber.StatusConflict
ale.ProviderError = "session_subject_conflict"
ale.Log(slog.LevelWarn, "Login blocked by existing browser session")
return err
}
c.Locals("user_id", authInfo.Subject)
c.Locals("login_id", loginID)
setSessionIDLocal(c, authInfo.SessionToken)
@@ -4676,8 +4828,10 @@ func extractSessionIDFromJWT(token string) string {
}
type qrMeta struct {
IPAddress string `json:"ip_address"`
UserAgent string `json:"user_agent"`
IPAddress string `json:"ip_address"`
UserAgent string `json:"user_agent"`
ApproverSubject string `json:"approver_subject,omitempty"`
ApproverSessionID string `json:"approver_session_id,omitempty"`
}
func (h *AuthHandler) storeQrMeta(pendingRef string, c *fiber.Ctx) {
@@ -4714,9 +4868,12 @@ func (h *AuthHandler) storeLoginApproverMeta(pendingRef string, c *fiber.Ctx, tt
if h.RedisService == nil || pendingRef == "" || c == nil {
return
}
approverSubject, approverSessionID := h.resolveCurrentBrowserSessionEvidence(c)
meta := qrMeta{
IPAddress: extractClientIPFromHeaders(c),
UserAgent: c.Get("User-Agent"),
IPAddress: extractClientIPFromHeaders(c),
UserAgent: c.Get("User-Agent"),
ApproverSubject: approverSubject,
ApproverSessionID: approverSessionID,
}
raw, err := json.Marshal(meta)
if err != nil {