We've updated our Terms of Service. By continuing to use our services, you agree to the updated Terms.
guides
Guide

How to set up NextAuth with Magic

Magic Staff · May 21, 2024
How to set up NextAuth with Magic

NextAuth allows developers to integrate various identity providers for authentication. Users can log in using providers such as Google and then link their accounts to a Magic wallet.

Google provides a platform for creating mobile and web applications with tools and services that help developers build high-quality apps, including authentication.

In this guide, we'll walk through integrating Google authentication with Magic using NextAuth, enabling users to log in with Google and associate their accounts with a Magic wallet. While this example uses a Next.js web app, the principles apply to any JavaScript framework.

#Project prerequisites

To follow along with this guide, you’ll need four things:

  1. A Magic Publishable API Key
  2. A Magic Secret API Key
  3. A Google project
  4. A web client

You can get your Publishable and Secret API Key’s from your Magic Dashboard.

You can create your Google project from the Google console.

We’ll use the make-scoped-magic-app CLI tool to bootstrap a Next.js app with Magic authentication already baked into the client. You’re welcome to use your own client, but this tutorial and its accompanying code snippets assume the output of the make-scoped-magic-app CLI as the starting point.

The make-scoped-magic-app CLI tool is an easy way to bootstrap new projects with Magic. To generate your application, simply run the command below in the shell of your choice. Be sure to replace <YOUR_PUBLISHABLE_API_KEY> with the Publishable API Key from your Magic Dashboard.

Bash
01npx make-scoped-magic-app \\
02    --template nextjs-dedicated-wallet \\
03    --network ethereum-sepolia \\
04    --login-methods Google \\
05    --publishable-api-key <YOUR_PUBLISHABLE_API_KEY>

#Install project dependencies

For this project, we’ll need to install a few additional dependencies including the next-auth SDK and Magic OIDC extension package.

Install the following dependencies to your project:

NPM
Yarn
01⁠npm install next-auth @magic-ext/oidc

#Create and configure Google project

Head to the Google console and create new credentials for OAuth client ID. To do this, in your side navigation menu, click on “APIs & Services” and then “Credentials”. Next, click on the “Create Credentials” button and select “OAuth client ID”.

You will be asked to add a few things for the credentials. Add the following when prompted:

Now we can create the app. In the next step, a configuration file for the Google credentials will be displayed. We will need to add this to our app as we will be using this for our login/logout flow.

Take the Client ID and Client secret values and add them to the .env file:

.env
01NEXT_PUBLIC_GOOGLE_CLIENT_ID=<Client ID>
02NEXT_PUBLIC_GOOGLE_CLIENT_SECRET=<Client secret>

#Configure NextAuth

Inside of src/pages, create a directory api/auth and add a file named [...nextauth].ts. This is where we will create the NextAuth configuration. This is instructing NextAuth that when a user logs in using the signIn function provided by NextAuth, it will authenticate the user via the Google authentication provider.

Paste the following code:

Typescript
01import NextAuth from 'next-auth';
02import GoogleProvider from 'next-auth/providers/google';
03
04export default NextAuth({
05  providers: [
06    GoogleProvider({
07      clientId: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID ?? "",
08      clientSecret: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_SECRET ?? "",
09      authorization: {
10        params: {
11          prompt: 'consent',
12          access_type: 'offline',
13          scope: 'openid profile email',
14          session: {
15            strategy: 'jwt',
16          },
17        },
18      },
19    }),
20  ],
21  callbacks: {
22    async jwt({ token, account }) {
23      // Store the id_token in the token object
24      if (account) {
25        token.idToken = account.id_token;
26      }
27      return token;
28    },
29    async session({ session, token }) {
30      // Add the id_token to the session object
31      session.idToken = token.idToken;
32      return session;
33    },
34  },
35});

#Configure Magic with OIDC

When we generated our Magic application using the make-scoped-magic-app CLI, it defaults to using the OAuth module. We will need to replace that with the OIDC extension. We do this because we will need to expose the loginWithOIDC method provided by Magic so we can instruct Magic that the users will be logging in with custom auth provider.

In src/components/magic/MagicProvider.tsx, replace the current code with the following:

Typescript
01import { getChainId, getNetworkUrl } from '@/utils/network';
02import { OpenIdExtension } from '@magic-ext/oidc';
03
04import { Magic as MagicBase } from 'magic-sdk';
05import { ReactNode, createContext, useContext, useEffect, useMemo, useState } from 'react';
06const { Web3 } = require('web3');
07
08export type Magic = MagicBase<OpenIdExtension[]>;
09
10type MagicContextType = {
11  magic: Magic | null;
12  web3: typeof Web3 | null;
13};
14
15const MagicContext = createContext<MagicContextType>({
16  magic: null,
17  web3: null,
18});
19
20export const useMagic = () => useContext(MagicContext);
21
22const MagicProvider = ({ children }: { children: ReactNode }) => {
23  const [magic, setMagic] = useState<Magic | null>(null);
24  const [web3, setWeb3] = useState<typeof Web3 | null>(null);
25
26  useEffect(() => {
27    if (process.env.NEXT_PUBLIC_MAGIC_API_KEY) {
28      const magic = new MagicBase(process.env.NEXT_PUBLIC_MAGIC_API_KEY as string, {
29        network: {
30          rpcUrl: getNetworkUrl(),
31          chainId: getChainId(),
32        },
33        extensions: [new OpenIdExtension()],
34      });
35
36      setMagic(magic);
37      setWeb3(new Web3((magic as any).rpcProvider));
38    }
39  }, []);
40
41  const value = useMemo(() => {
42    return {
43      magic,
44      web3,
45    };
46  }, [magic, web3]);
47
48  return <MagicContext.Provider value={value}>{children}</MagicContext.Provider>;
49};
50
51export default MagicProvider;

#Get provider ID from Magic

We need a way to retrieve the provider ID for Google. Magic provides an endpoint for developers to send a POST request that returns a payload containing the provider ID. To do that, we’ll need to add our Magic secret key to the headers, along with a body containing the following values:

  • issuer: The URL of the token issuer
  • audience: The identifier of the audience, often the same as the IdP client ID to which the token is issued
  • display_name: A human-readable identifier for the entity
  • sandbox_mode: When true, provider does not enforce the expiry claim during ID token validation, which can be useful for testing environments. This defaults to false.

Attaining these values vary across different providers, but for Google, we will only need to get the client ID (which in this case is available as NEXT_PUBLIC_GOOGLE_CLIENT_ID) and the display name, which can be found in the project settings in the Google console.

Json
01{
02  "issuer": "<https://accounts.google.com>",
03  "audience": "<CLIENT ID>",
04  "display_name": "<NAME>",
05  "sandbox_mode": true
06}

We have everything we need to send the POST request to the Magic endpoint. In your terminal, paste the following code with your project’s values:

Bash
01curl --location '<https://api.magic.link/v1/api/magic_client/federated_idp>' \\\\
02--header 'X-Magic-Secret-Key: <MAGIC_SECRET_KEY>' \\\\
03--header 'Content-Type: application/json' \\\\
04--data '{
05  "issuer": "<https://accounts.google.com>",
06  "audience": "<CLIENT ID>",
07  "display_name": "<NAME>",
08  "sandbox_mode": true
09}'

Check the returned object for the id attribute. Save this in your .env file as NEXT_PUBLIC_PROVIDER_ID. We will be using that when we create our sign in flow using the loginWithOIDC method!

#Add to client

Everything should be correctly set up on the authentication side; now we just need to modify the existing code to allow for logging users in and out. We'll update the code to handle user authentication, linking Google authentication and Magic.

#Log in user

Navigate to src/components/magic/auth/Google.tsx. This is the file where will be handling the user log in flow. We will need to modify some of the useEffect hooks and add some additional functionality.

The first thing we need to do is import the helper functions provided by NextAuth (signIn, useSession, getSession). Next, we declare the session and status variables. We will be using these to ensure the user is authenticated by NextAuth and Google before logging them in to Magic.

We will need to replace the existing login code to use the signIn function provided by NextAuth:

Typescript
01const login = async () => {
02  setLoadingFlag(true);
03  await signIn('google', { redirect: false });
04};

Once a user logs in using the signIn function, they will not be redirected away from the sign in page, so we can use a hook to check the status of the user from the NextAuth side. If a user has been authenticated or there is an active session for the user, the loginWithMagic() function will be called and will start the loginWithOIDC process.

Here we attain the session so we have access to the idToken. We then pass the idToken along with the providerId we retrieved from the POST request earlier and set as NEXT_PUBLIC_PROVIDER_ID to invoke the loginWithOIDC method.

Typescript
01useEffect(() => {
02  if (status === 'authenticated' || session) {
03    loginWithMagic();
04  }
05}, [status]);
06
07const loginWithMagic = async () => {
08  const session = await getSession();
09  const DID = await magic?.openid.loginWithOIDC({
10    jwt: session?.idToken,
11    providerId: process.env.NEXT_PUBLIC_PROVIDER_ID!
12  });
13
14  const metadata = await magic?.user.getMetadata();
15  setToken(DID ?? '');
16  saveUserInfo(DID ?? '', 'SOCIAL', metadata?.publicAddress ?? '');
17  setLoadingFlag(false);
18};

Once we receive the DID token back from Magic, we can set the user state and should be redirected to the user’s dashboard.

Now that we’ve run through the new user log in flow, let’s replace the existing code with the following:

Typescript
01import { LoginProps } from '@/utils/types';
02import { useMagic } from '../MagicProvider';
03import { useEffect, useState } from 'react';
04import { saveUserInfo } from '@/utils/common';
05import Spinner from '../../ui/Spinner';
06import Image from 'next/image';
07import google from 'public/social/Google.svg';
08import Card from '../../ui/Card';
09import CardHeader from '../../ui/CardHeader';
10import { signIn, useSession, getSession } from "next-auth/react";
11
12const Google = ({ token, setToken }: LoginProps) => {
13  const { magic } = useMagic();
14  const { data: session, status } = useSession();
15  const [isAuthLoading, setIsAuthLoading] = useState<boolean>(false);
16
17  useEffect(() => {
18    const loadingFlag = localStorage.getItem('isAuthLoading');
19    setIsAuthLoading(loadingFlag === 'true');
20  }, []);
21
22  useEffect(() => {
23    if (status === 'authenticated' || session) {
24      loginWithMagic();
25    }
26  }, [status]);
27
28  const loginWithMagic = async () => {
29    const session = await getSession();
30    const DID = await magic?.openid.loginWithOIDC({
31      jwt: session?.idToken,
32      providerId: process.env.NEXT_PUBLIC_PROVIDER_ID!
33    });
34
35    const metadata = await magic?.user.getMetadata();
36    setToken(DID ?? '');
37    saveUserInfo(DID ?? '', 'SOCIAL', metadata?.publicAddress ?? '');
38    setLoadingFlag(false);
39  };
40
41  const login = async () => {
42    setLoadingFlag(true);
43    await signIn('google', { redirect: false });
44  };
45
46  const setLoadingFlag = (loading: boolean) => {
47    localStorage.setItem('isAuthLoading', loading.toString());
48    setIsAuthLoading(loading);
49  };
50
51  return (
52    <Card>
53      <CardHeader id="google">Google Login</CardHeader>
54      {isAuthLoading ? (
55        <Spinner />
56      ) : (
57        <div className="login-method-grid-item-container">
58          <button
59            className="social-login-button"
60            onClick={() => {
61              if (token.length === 0) login();
62            }}
63            disabled={false}
64          >
65            <Image src={google} alt="Google" height={24} width={24} className="mr-6" />
66            <div className="w-full text-xs font-semibold text-center">Continue with Google</div>
67          </button>
68        </div>
69      )}
70    </Card>
71  );
72};
73
74export default Google;

#Log out user

Logging out from both Magic and Google is essential for maintaining session security. Clearing a user's session from both the authentication provider and the wallet provider upon logout prevents unauthorized access and ensures the invalidation of authentication tokens, thereby safeguarding potentially sensitive user information.

Similarly to how NextAuth logs a user in using signIn, to log a user out we will just need to import and call the signOut function. For the sake of this guide we will be using { redirect: false }. If you don’t state to not redirect after logging out, it may not reach the rest of the code in the function.

With the generated Magic application, a user can sign out from two separate files:

  • src/components/magic/wallet-methods/Disconnect.tsx
  • src/components/magic/cards/UserInfoCard.tsx

Inside of Disconnect.tsx, import the signOut function from next-auth/react and replace the disconnect with the following code:

Typescript
01const disconnect = useCallback(async () => {
02  if (!magic) return;
03  try {
04    setDisabled(true);
05    await logout(setToken, magic); // Sign out of Magic
06    await signOut({ redirect: false }) // Sign out of NextAuth
07    setDisabled(false);
08  } catch (error) {
09    setDisabled(false);
10    console.error(error);
11  }
12}, [magic, setToken]);

Now navigate to the UserInfoCard.tsx file and import the signOut function. Replace the disconnect function with the following code:

Typescript
01const disconnect = useCallback(async () => {
02  try {
03    if (magic) {
04      await signOut({ redirect: false }); // Sign out of NextAuth
05      await logout(setToken, magic); // Sign out of Magic
06    }
07  } catch (error) {
08    console.error('Error disconnecting:', error);
09  }
10}, [magic, setToken]);

#Testing it all out

Start the local development server and open your browser to http://localhost:3000. You will see a button labeled “Continue with Google”. Click the button and complete the Google authentication process. After signing in successfully, a card labeled “Wallet” will appear, displaying a wallet address and balance. This wallet is linked to the authenticated user, confirming that you have successfully integrated Magic with Next Auth.

#Next steps

You now know how to integrate Magic with NextAuth and Google to include the following features:

  1. Simple authentication with Google
  2. Automatic wallet creation for first-time users
  3. Ability to have Magic users interact with their wallets
  4. Ability to log in and log out of NextAuth user sessions

Feel free to take a look at our final code solution. Take a look at the NextAuth documentation for more information on what is possible with Magic and NextAuth.

Let's make some magic!