Advanced

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:

  1. 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 to false:

      convex/auth.ts
      import 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

  2. 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.
  3. 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:

  1. Use only trusted methods.
  2. 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.