GetProviderToken for reading a user’s upstream OAuth token.
Everything here is opt in. Existing deployments behave as before until you set the config field for a feature.
Turn it on
AccountState is a separate switch on purpose. The state lives in new tables, so a Postgres or MySQL deployment that upgrades the library without running the migration would otherwise fail every sign-in. Run the migration first, then set the field. The built-in memory and SQLite storages implement all four capabilities. Custom storage adapters are not affected until they opt in.
Force a password change
CodePasswordChangeRequired (HTTP 403) when they sign in with any method that issues a session: password, magic link, OAuth, passkey, SAML. Setting the flag also revokes their existing sessions. The flag clears when the user completes a password reset or changes their password.
The user recovers through the existing password reset email. This library does not issue a limited, change-password-only session. If you need an in-place change flow, build it on top of the reset endpoint.
Imported users can carry the flag as well: set ImportedUser.MustChangePassword, and theauth-migrate passes it through when a source reader marks the account as needing a reset.
Disable and delete accounts
DisableUser also revokes the user’s sessions, API tokens and trusted devices. EnableUser lifts the block but does not bring those back.
DeleteUser removes sessions, tokens, linked identities, passkeys, TOTP, API tokens the user owns or delegated, and organization memberships. Audit rows keep their data but lose the actor id. It returns ErrLastOwner when the user is the only owner of an organization, so transfer or delete the organization first. LifecycleHooks.OnAccountDeleted runs after the deletion commits. It is observe only, like the other lifecycle hooks, and cannot veto it.
HTTP routes:
On Postgres and MySQL the cascade relies on the existing foreign keys plus explicit deletes of API tokens and magic links. The memory and SQLite paths are covered by the contract suite. The Postgres and MySQL paths have not been exercised by the live contract suites yet, so run your own check before relying on them in production.
Cap concurrent sessions
SessionEvictOldest, a new sign-in revokes the user’s oldest sessions to make room. With SessionReject, it fails with CodeSessionLimit. Sessions waiting on a second factor do not count, and session rotation is exempt. In reject mode the check runs before a recovery code is spent, so a refused sign-in does not burn one.
The cap is best effort. Two sign-ins that start at the same moment can both pass the check.
Trusted devices for TOTP
WhenTrustedDevices is set, the TOTP verify and recovery code endpoints accept {"rememberDevice": true}. The library then sets a cookie of the form token.hmac, keyed from EncryptionKey, and stores only the SHA-256 of the token. Later password sign-ins from that browser skip the TOTP prompt until the window ends.
Trust is revoked when the password changes, when TOTP is removed, and on request:
GET /auth/trusted-devices, DELETE /auth/trusted-devices/{id} and DELETE /auth/trusted-devices. Each device carries a label and an IP prefix taken from the same helper the session list uses.
Passkey sign-in is not covered. In this library a passkey is a standalone login, so there is no second factor step to remember.
Read a user’s provider token
GetProviderToken decrypts the stored access token and, when it has expired, refreshes it with the stored refresh token. Concurrent calls for the same user and provider share one refresh. A rotated refresh token is persisted, and a refresh that does not return a new one keeps the old one.
If the provider answers invalid_grant, the stored tokens are cleared and later calls fail immediately with ErrProviderGrantRevoked instead of hitting the provider again.
Providers refresh through the optional RefreshProvider interface (RefreshToken(ctx, refreshToken) (*ProviderToken, error)). Implemented today for Google, Microsoft, GitHub, the generic OAuth provider and generic OIDC. For Google, set google.Config.Offline so the first consent returns a refresh token. The other built-in providers return ErrProviderRefreshUnsupported. The shared provider/internal/refresh helper is the intended place to add them.
One thing to know: a provider that rotates refresh tokens can race when two processes refresh at once. One of them may see the grant as revoked. Run provider token reads through one place if that matters for your provider.
Limits
- Forced change is an error and a password reset, not a limited session.
- The session cap is best effort under concurrent sign-ins.
- Trusted devices cover TOTP and recovery codes only.
- Postgres and MySQL cascade deletion and the new storage methods compile and pass
go vet, but their live contract suites are not part of this change.