This project demonstrates an approach for supporting "one user, multiple account providers" with NextAuth.js.
Note: You can find the App Router implementation of this on this branch:
app-router
The primary logic for how this is achieved is inside of the src/pages/api/auth/[...nextauth].ts file.
The logic in this file should be adaptable to different application needs without requiring having the exact same database schema, ORM, authentication providers, or UI as used in this demo!
- First, extend the types of the
Session
andJWT
objects from NextAuth to include auserId
property which will be used to link accounts together. - Wrap the
NextAuth
handler one level deep in a custom handler to get direct access toreq
andres
objects on the request. - In the
signIn
callback ofNextAuth
auth options, check if a user is already signed in by making a call togetServerSession
from NextAuth.- If they are signed in, treat the new sign in as an attempt to link an account and perform the linking using the
userId
property from the session object. - If they are not signed in, let the sign in continue as normal.
- If they are signed in, treat the new sign in as an attempt to link an account and perform the linking using the
- Next, in the
jwt
callback, and for sign-in's fetch an existing account from the database for the provider and provider's account identifier.- If there is one, add the
userId
property to the JWT object. - If there is no existing account, create a new user account first, and then add the
userId
property of the newly created user to the JWT object.
- If there is one, add the
- Lastly, in the
session
callback, ensure that thetoken.userId
that was previously set is also set on thesession.userId
property. This will ensure that it is available in thesignIn
callback (Step 3) for linking!
This project specifically uses the following tools and technologies:
- Framework: Next.js
- Authentication: NextAuth.js
- Database: Postgres
- ORM: Drizzle
- Environment Variables: T3-Env
- Styling: Tailwind CSS
- Deployment: Vercel
To use the application as is, you will need to setup environment variables as described in .env.example
. Also see NextAuth documentation on providers for setup information for each provider's secrets (e.g GitHub)
This project uses Postgres as its database. You will need to create a database to run the application as is!
I have found Neon to be quick and easy to setup. Alternatively, you can setup postgres through Docker.
For postgres database clients, I recommend Postico or pgAdmin.
Once your database is setup and you have added it's URL to .env.local
, you can generate and run migrations as follows:
# Generate migrations
npm run migrations:generate
# Run migrations
npm run migrations:run
NB: There is a bug here where the migration run script looks like it is taking long to complete, but the migrations are actually successful. Verify that they completed from your database.
First, ensure that you have the necessary environment variables in a .env.local
file corresponding with those described in .env.example
.
Next, run the development server:
npm run dev
# or
yarn dev
# or
pnpm dev
Once the application is started, you may go to the demo and attempt the one-user multiple functionality! 🎉
⚠️ Potential security risks ahead! See: nextauthjs/next-auth#1002 (comment)- This is an external work-around/approach to the problem of account linking. NextAuth itself does support internal (and MORE secure though limited) approach to account linking which should be considered strongly to this approach! The conditions for linking are controlled by next-auth, vs. externally in this approach and should be more secure/locked down compared to this approach.
- Such conditions include checks for same email accross providers, and
allowDangerousEmailLinking
flag to only link accounts from providers that are trusted to verify email addresses (to prevent hijacking accounts with less secure oauth providers) and naturally more/better conditions as the library evolves!
- You may also want to distinguish between "primary" and "secondary" accounts. For example there can be use-cases where sign-in's are only allowed through a single provider, but other accounts from other providers may be linked to the primary account for verification purposes. You may keep track of the primary provider (provider at first sign in/up) and block the
signIn
if it is not through that provider! - There is a big assumption here that a sign-in attempt with a currently signed in user is an attempt to link accounts. This may not always be the case for certain applications. This is not an assumption that always makes sense, and care should be taken to mitigate this for different applications.
- For example, this could be the case if your sign in page/endpoints accessible when a user is already signed in, allowing for the possibility of a second user signing in on the same device (e.g. a shared computer). See the discussion linked below for ideas/solutions for this!
- The adapter approach mentioned above introduces extra conditions on top of this assumption.
- I have not thought too much about what the migration strategy would look like for already existing applications that seek to add this account linking functionality and this should be approached carefully! For now, some thoughts I have are as follows:
- First, add the logic to start including an application-level unique
userId
property in the JWT object, by invalidating/revoking existing sessions to force users to sign in again and get a new JWT withuserId
property. - After this, roll in the logic to start linking accounts on sign-in's.
- Again, here the adapter approach by NextAuth should help with automatically making sure that sessions are in the right state to support account linking by attaching the
userId
on creation! A migration strategy might still be necessary especially for already existing applications that seek to use existing adapaters.
- First, add the logic to start including an application-level unique
- This demo has also not been tested beyond the providers used in this demo, but the solution looks to be provider agnostic and relies heavily on the
account.providerAccountId
+account.provider
properties in identifying existing accounts on sign-in's. It works to the degree that providers/or next-auth supplies a uniquely identifiable id (account.providerAccountId
) for each user on the provider's platform. The logic for determining existing accounts may be modified to rely on other properties if necessary. Coincidentally, a similar approach is used by NextAuth to identify existing accounts in their codebase. See here for more. - The demo is very minimal, but supports features such as ensuring an account (as identified by
account.provider
+account.providerAccountId
) can only be linked to one user. It does not support features such as unlinking or removing connected accounts. - In conclusion, browse through this callback handler logic from
nextauth
code-base to know what offerings there are following the adapter-based approach.
See this discussion for more context and ideas on this!
All contributions to this demo are welcome! Issues are especially welcome to bring light to bugs or improvements that can be made to the approach adopted by this demo!