Skip to content

Add OAuth account linking and harden account safety - #41

Merged
mdfarhankc merged 1 commit into
mainfrom
feat/account-safety
Sep 21, 2026
Merged

mdfarhankc merged 1 commit into
mainfrom
feat/account-safety

Conversation

@mdfarhankc

Copy link
Copy Markdown
Owner

Summary

OAuth account linking. Signed-in users can link a provider through GET /oauth/{provider}/link/authorize and POST /oauth/{provider}/link/callback. The link state is bound to the user it was issued for, cannot be used as a sign-in state or a session token, and an identity that belongs to another user is never moved.

Re-authentication for destructive actions. DELETE /me and setting a first password need the current password or credentials checked within REAUTH_MAX_AGE_SECONDS (300). Tokens carry an auth_time claim that refresh rotation preserves, so rotating a stolen refresh token does not count as proving presence. The password check is rate limited in its own bucket.

No more account lockout from removing a sign-in method. Unlinking a provider and deleting a passkey share one rule, which counts only the methods the deployment can actually serve.

Also:

  • prune_expired_refresh_tokens() and the missing user_id / expires_at indexes
  • PASSWORD_MAX_LENGTH, enforced on every path that hashes or verifies, including login
  • password-reset links invalidated by a password change
  • Redis lockout fails open instead of raising a 500
  • Redis clients keyed per event-loop object
  • the passkey challenge store now evicts expired entries
  • sign-in no longer attaches a second identity from the same provider
  • several 500s turned into 401, 409 or 501

Breaking changes

  • DELETE /me and first-password sets from sessions older than REAUTH_MAX_AGE_SECONDS return 403. Clients should send current_password or re-authenticate.
  • Password-reset links issued before the upgrade stop working, for up to PASSWORD_RESET_EXPIRE_MINUTES after deploy.

Migration

Existing deployments need three new indexes:

CREATE INDEX ix_fullauth_refresh_tokens_user_id ON fullauth_refresh_tokens (user_id);
CREATE INDEX ix_fullauth_refresh_tokens_expires_at ON fullauth_refresh_tokens (expires_at);
CREATE INDEX ix_fullauth_oauth_accounts_user_id ON fullauth_oauth_accounts (user_id);

Checks

ruff, ruff format, mypy --strict, pytest (556 passed, 9 skipped on 3.14; also run on 3.10), mkdocs build --strict. Every new security guard was mutation-tested: removing it makes its test fail.

OAuth providers could only be attached as a side effect of signing in, on a
provider-verified email that matched an existing account. Signed-in users
can now link a provider explicitly through /oauth/{provider}/link/authorize
and /link/callback. A link state is bound to the user it was issued for and
cannot be used as a sign-in state or a session token, and an identity that
belongs to another user is never moved.

Deleting an account and setting a first password now require recent
authentication: the current password, or credentials checked within
REAUTH_MAX_AGE_SECONDS. Tokens carry an auth_time claim that refresh
rotation preserves, so rotating a stolen refresh token does not count as
proving presence. The password check is rate limited in its own bucket.

Removing a sign-in method can no longer lock an account out. Unlinking a
provider and deleting a passkey share one rule, which counts only the
methods the deployment can actually serve.

Also: refresh-token pruning and the missing user_id indexes; a
PASSWORD_MAX_LENGTH enforced on every path that hashes or verifies,
including login; password-reset links invalidated by a password change;
the Redis lockout manager failing open instead of raising; Redis clients
keyed per event loop object; an unevicted passkey challenge store; a
second provider identity attached on sign-in; and several 500s turned into
401, 409 or 501.

Breaking: DELETE /me and first-password sets from sessions older than
REAUTH_MAX_AGE_SECONDS now return 403. Existing deployments need the new
indexes; the changelog carries the SQL.
@coderabbitai

coderabbitai Bot commented Sep 21, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: fe739149-3aff-42bb-a2e7-c8a6c46e2756


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@mdfarhankc
mdfarhankc merged commit b2f4915 into main Sep 21, 2026
7 checks passed
@mdfarhankc
mdfarhankc deleted the feat/account-safety branch September 21, 2026 17:22
@mdfarhankc mdfarhankc mentioned this pull request Sep 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant