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 } = 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 } = {
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 } = 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.