Advanced: Details
Account linking
Using multiple authentication methods poses an additional challenge: What should happen when the same email address is used with a different method?
Sign-ups via each authentication method are tracked in the
authAccounts
table. Account linking
determines whether two accounts are linked to the same user document.
Convex Auth follows this logic:
-
Each provider is determined "trusted" or not, based on whether the email address ownership is verified.
-
Email-based authentication methods (magic links and OTPs) are trusted.
-
OAuth providers are trusted by default, since most popular OAuth providers today enforce email verification before the user can use them.
You can make any OAuth provider untrusted by setting the
allowDangerousEmailAccountLinking
option tofalse
:convex/auth.tsimport Resend from "@auth/core/providers/resend"; import GitHub from "@auth/core/providers/github"; import { convexAuth } from "@convex-dev/auth/server"; export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({ providers: [ Resend, GitHub({ allowDangerousEmailAccountLinking: false }), ], });
-
Password-based accounts without required email verification are untrusted
-
-
If a trusted method is used, and there is a single existing user document with a given email address verified, signing in via the trusted method will link the new account to the same user document.
- This means that there is always guaranteed to be only a single user document linked to accounts from trusted methods.
-
If an untrusted method is used, the new account will not be linked to any existing one.
Verified phone numbers are used for account linking just like email addresses.
Preventing duplicate user accounts
We recommend to not mix trusted and untrusted authentication methods. If you don't want to have multiple user documents for the same email address, you must either:
- Use only trusted methods.
- Use only a single untrusted method.
Controlling user creation and account linking behavior
You can implement your own user creation and account linking behavior by
providing the
createOrUpdateUser
callback:
import GitHub from "@auth/core/providers/github";
import Password from "@convex-dev/auth/providers/Password";
import { MutationCtx } from "./_generated/server";
export const { auth, signIn, signOut, store, isAuthenticated } = {
providers: [GitHub, Password],
callbacks: {
// `args.type` is one of "oauth" | "email" | "phone" | "credentials" | "verification"
// `args.provider` is the currently used provider config
async createOrUpdateUser(ctx: MutationCtx, args) {
if (args.existingUserId) {
// Optionally merge updated fields into the existing user object here
return args.existingUserId;
}
// Implement your own account linking logic:
const existingUser = await findUserByEmail(ctx, args.profile.email);
if (existingUser) return existingUser._id;
// Implement your own user creation:
return ctx.db.insert("users", {
/* ... */
});
},
},
};
When you provide this callback, the library doesn't create or update users at all. It is up to you to implement all the necessary logic for all providers you use.
Writing additional data during authentication
If you don't specify the
createOrUpdateUser
callback
you can still perform additional writes to the database with the
afterUserCreatedOrUpdated
callback:
import GitHub from "@auth/core/providers/github";
import Password from "@convex-dev/auth/providers/Password";
import { MutationCtx } from "./_generated/server";
export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({
providers: [GitHub, Password],
callbacks: {
// `args` are the same the as for `createOrUpdateUser` but include `userId`
async afterUserCreatedOrUpdated(ctx: MutationCtx, { userId }) {
await ctx.db.insert("someTable", { userId, data: "some data" });
},
},
});
This is helpful when the default user creation implementation in the library satisfies your app's needs.
Session validity
Convex Auth issues JWTs which allow your client to authenticate.
As far as the ConvexReactClient
and the Convex backend with
ctx.auth.getUserIdentity()
are concerned, the JWT is all that's needed for
valid authentication.
This means that when an existing session is invalidated (deleted), the user is not automically signed out until the JWT expires.
If you want session validity to be reflected immediately, you need to actually load the current session in your queries/mutations/actions, and you need to make sure your client can handle the state where the JWT is valid but the session is not.
Either way, for critical operations (changing account details) you should always
require either direct reauthentication or recent authentication. The current
session's _creationTime
can be used to determine how recently the user has
signed in.
Reauthentication
To reauthenticate the user, you need to have a way to show the sign-in UI even when the user is already signed in (via routing or app state for example).
Using any authentication method while the user is logged-in will invalidate their existing session and create a new one.
Session document lifecycle
The session documents are created and deleted by Convex Auth.
For this reason if you're tying other documents to sessions, and you don't want to lose information when the session expires, you should store both the session ID and the user ID in your other document.
Custom callback and sign-in URLs
For ease of configuration, Convex Auth defaults to using the builtin
CONVEX_SITE_URL
value in callback and sign-in URLs. For some OAuth providers
(Google, for example) this can lead to something like
happy-animal-123.convex.site
showing up to users on the OAuth consent screen.
If you're using a custom domain (opens in a new tab) for your production Convex deployment, you can configure Convex Auth to use that domain for the sign-in and callback URLs.
With your Production deployment selected in the Convex dashboard, navigate
to Settings -> Environment Variables. Add a CUSTOM_AUTH_SITE_URL
environment variable pointing to the custom domain you configured for the
deployment. For example, if your custom HTTP Actions domain is
convex.example.com
then you'd set CUSTOM_AUTH_SITE_URL
to
https://convex.example.com
(no trailing slash).
Make sure to update your OAuth provider config for your production deployment with your new callback URL. Otherwise it will refuse to redirect users to your application.