forked from baron/baron-sso
fix auth link session conflict policy
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user