From 71808f535813e479ff3e56031b70d68734305d19 Mon Sep 17 00:00:00 2001 From: Gourab Singha Date: Fri, 26 Jun 2026 01:03:25 +0530 Subject: [PATCH] feat: update identities.last_sign_in_at on subsequent sign-ins Fixes #2563 --- internal/api/external.go | 3 +++ internal/api/token.go | 3 +++ internal/api/verify.go | 6 ++++++ internal/models/refresh_token.go | 1 + internal/tokens/service.go | 15 +++++++++++++++ 5 files changed, 28 insertions(+) diff --git a/internal/api/external.go b/internal/api/external.go index 0f88a64b8f..ec9cb71321 100644 --- a/internal/api/external.go +++ b/internal/api/external.go @@ -165,6 +165,7 @@ func (a *API) internalExternalProviderCallback(w http.ResponseWriter, r *http.Re grantParams.FillGrantParams(r) providerType, emailOptional := getExternalProviderType(ctx) + grantParams.Provider = providerType data, err := a.handleOAuthCallback(r) if err != nil { return err @@ -367,6 +368,8 @@ func (a *API) createAccountFromExternalIdentity(tx *storage.Connection, r *http. user = decision.User identity = decision.Identities[0] + now := time.Now() + identity.LastSignInAt = &now identity.IdentityData = identityData if terr = tx.UpdateOnly(identity, "identity_data", "last_sign_in_at"); terr != nil { return 0, nil, terr diff --git a/internal/api/token.go b/internal/api/token.go index 9ef0ae727a..9925da799c 100644 --- a/internal/api/token.go +++ b/internal/api/token.go @@ -91,12 +91,14 @@ func (a *API) ResourceOwnerPasswordGrant(ctx context.Context, w http.ResponseWri if params.Email != "" { provider = "email" + grantParams.Provider = "email" if !config.External.Email.Enabled { return apierrors.NewUnprocessableEntityError(apierrors.ErrorCodeEmailProviderDisabled, "Email logins are disabled") } user, err = models.FindUserByEmailAndAudience(db, params.Email, aud) } else if params.Phone != "" { provider = "phone" + grantParams.Provider = "phone" if !config.External.Phone.Enabled { return apierrors.NewUnprocessableEntityError(apierrors.ErrorCodePhoneProviderDisabled, "Phone logins are disabled") } @@ -238,6 +240,7 @@ func (a *API) PKCE(ctx context.Context, w http.ResponseWriter, r *http.Request) } else if err != nil { return err } + grantParams.Provider = flowState.ProviderType if flowState.IsExpired(a.config.External.FlowStateExpiryDuration) { return apierrors.NewUnprocessableEntityError(apierrors.ErrorCodeFlowStateExpired, "invalid flow state, flow state has expired") } diff --git a/internal/api/verify.go b/internal/api/verify.go index 212d7388eb..6bec9d3a37 100644 --- a/internal/api/verify.go +++ b/internal/api/verify.go @@ -131,6 +131,7 @@ func (a *API) verifyGet(w http.ResponseWriter, r *http.Request, params *VerifyPa ) grantParams.FillGrantParams(r) + grantParams.Provider = "email" flowType := models.ImplicitFlow var authenticationMethod models.AuthenticationMethod @@ -238,6 +239,11 @@ func (a *API) verifyPost(w http.ResponseWriter, r *http.Request, params *VerifyP var isSingleConfirmationResponse = false grantParams.FillGrantParams(r) + if params.Type == smsVerification || params.Type == phoneChangeVerification { + grantParams.Provider = "phone" + } else { + grantParams.Provider = "email" + } err := db.Transaction(func(tx *storage.Connection) error { var terr error diff --git a/internal/models/refresh_token.go b/internal/models/refresh_token.go index 63e60fd140..0d3130583f 100644 --- a/internal/models/refresh_token.go +++ b/internal/models/refresh_token.go @@ -51,6 +51,7 @@ type GrantParams struct { UserAgent string IP string + Provider string } func (g *GrantParams) FillGrantParams(r *http.Request) { diff --git a/internal/tokens/service.go b/internal/tokens/service.go index 3a479d04c3..2f42842e18 100644 --- a/internal/tokens/service.go +++ b/internal/tokens/service.go @@ -873,6 +873,21 @@ func (s *Service) IssueRefreshToken(r *http.Request, responseHeaders http.Header err := conn.Transaction(func(tx *storage.Connection) error { var terr error + if grantParams.Provider != "" { + if terr = tx.Load(user, "Identities"); terr != nil { + return apierrors.NewInternalServerError("Error loading user identities").WithInternalError(terr) + } + for i := range user.Identities { + if user.Identities[i].Provider == grantParams.Provider { + user.Identities[i].LastSignInAt = &now + if terr = tx.UpdateOnly(&user.Identities[i], "last_sign_in_at"); terr != nil { + return apierrors.NewInternalServerError("Error updating identity last_sign_in_at").WithInternalError(terr) + } + break + } + } + } + if config.Security.RefreshTokenAlgorithmVersion == 2 { session, terr := models.NewSession(user.ID, grantParams.FactorID) if terr != nil {