Invalidate sessions on password change and reset
Session validity was decoupled from credential rotation: changing a password (logged-in flow) or completing a forgot-password reset left every existing iam_sessions row valid until its idle TTL. A user who saw their account compromised on another device had no way to actually evict that device by rotating the password. Inside the same DB transaction as the password update, expire the identity's other active sessions: - ChangePassword keeps the caller's current session and revokes every other session for the identity, so the user is not logged out of the browser they just used. - ResetPassword has no caller session (the user is anonymous, authenticated only by a stateless token) and revokes all of the identity's active sessions. The session middleware already rejects rows with expire_reason set, so revoked sessions are kicked out on the next request without any middleware change. Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
@@ -343,6 +343,30 @@ WHERE
|
||||
return result.RowsAffected(), nil
|
||||
}
|
||||
|
||||
func (s *Sessions) ExpireAllForIdentity(ctx context.Context, conn pg.Querier, identityID gid.GID) (int64, error) {
|
||||
q := `
|
||||
UPDATE iam_sessions
|
||||
SET
|
||||
expired_at = NOW(),
|
||||
updated_at = NOW(),
|
||||
expire_reason = 'revoked'
|
||||
WHERE
|
||||
identity_id = @identity_id
|
||||
AND expire_reason IS NULL
|
||||
`
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"identity_id": identityID,
|
||||
}
|
||||
|
||||
result, err := conn.Exec(ctx, q, args)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("cannot query sessions: %w", err)
|
||||
}
|
||||
|
||||
return result.RowsAffected(), nil
|
||||
}
|
||||
|
||||
func (s *Session) LoadByRootSessionIDAndMembershipID(
|
||||
ctx context.Context,
|
||||
conn pg.Querier,
|
||||
|
||||
@@ -235,7 +235,7 @@ func (s *AccountService) ListPendingInvitations(
|
||||
return page.NewPage(invitations, cursor), nil
|
||||
}
|
||||
|
||||
func (s AccountService) ChangePassword(ctx context.Context, identityID gid.GID, req *ChangePasswordRequest) error {
|
||||
func (s AccountService) ChangePassword(ctx context.Context, identityID gid.GID, currentSessionID gid.GID, req *ChangePasswordRequest) error {
|
||||
if err := req.Validate(); err != nil {
|
||||
return fmt.Errorf("invalid request: %w", err)
|
||||
}
|
||||
@@ -275,6 +275,11 @@ func (s AccountService) ChangePassword(ctx context.Context, identityID gid.GID,
|
||||
return fmt.Errorf("cannot update identity: %w", err)
|
||||
}
|
||||
|
||||
sessions := coredata.Sessions{}
|
||||
if _, err := sessions.ExpireAllForIdentityExceptOneSession(ctx, tx, identity.ID, currentSessionID); err != nil {
|
||||
return fmt.Errorf("cannot expire other sessions: %w", err)
|
||||
}
|
||||
|
||||
// TODO: email to notify identity that their password has been changed
|
||||
|
||||
return nil
|
||||
|
||||
@@ -279,6 +279,11 @@ func (s AuthService) ResetPassword(
|
||||
return fmt.Errorf("cannot update identity: %w", err)
|
||||
}
|
||||
|
||||
sessions := coredata.Sessions{}
|
||||
if _, err := sessions.ExpireAllForIdentity(ctx, tx, identity.ID); err != nil {
|
||||
return fmt.Errorf("cannot expire sessions: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
@@ -318,10 +318,12 @@ func (r *mutationResolver) VerifyEmail(ctx context.Context, input types.VerifyEm
|
||||
// ChangePassword is the resolver for the changePassword field.
|
||||
func (r *mutationResolver) ChangePassword(ctx context.Context, input types.ChangePasswordInput) (*types.ChangePasswordPayload, error) {
|
||||
identity := authn.IdentityFromContext(ctx)
|
||||
session := authn.SessionFromContext(ctx)
|
||||
|
||||
err := r.iam.AccountService.ChangePassword(
|
||||
ctx,
|
||||
identity.ID,
|
||||
session.ID,
|
||||
&iam.ChangePasswordRequest{
|
||||
CurrentPassword: input.CurrentPassword,
|
||||
NewPassword: input.NewPassword,
|
||||
|
||||
Reference in New Issue
Block a user