8/ Using NextAuth GoogleProvider to put a user into the Strapi database
Tags: nextauth, strapi, nextjs, googleprovider
Published: 2024-04-01 16:20:31

The final code for this chapter is available on [github (branch callbacksForGoogleProvider)](https://github.com/peterlidee/NNAS/tree/callbacksForGoogleProvider).

The callback we need to place our user in `Strapi`'s database is the `jwt callback`. We also know when to do this. In the previous chapter we saw how the account argument of `jwt` only gets populated on a certain trigger: `signin`. So we will listen for these events:

```jsx
// frontend/src/api/auth/[...nextAuth]/authOptions.ts

async jwt({ token, trigger, account, user, session }) {
  if (account) {
    if (account.provider === 'google') {
      // we now know we are doing a sign in using GoogleProvider
      // do some strapi stuff here
    }
  }
  return token;
},
```

## Strapi authentication and authorization

`Strapi` handles the entire auth process via the [Users & Permissions plugin](https://docs.strapi.io/dev-docs/plugins/users-permissions):

> To authenticate users, Strapi provides two auth options: one social (Google, Twitter, Facebook, etc.) and another local auth (email and Password). Both of these options return a token (JWT token), which will be used to make further requests.
> (Source: <https://strapi.io/blog/strapi-authentication-with-react>)

The endpoint for local auth is `/api/auth/local` (careful, this is Strapi, so backend) and we will be using this later on when we setup our app with credentials. The endpoint for social auth is `/api/connect/[providername]`. In our case it will be `/api/connect/google`.

In theory, you don't need `NextAuth`. You can call the `Strapi` google endpoint (`/api/connect/google`) directly from your frontend and `Strapi` will start the entire auth process. All of it, including redirecting to Google on the first signing to ask for permission. This is quite a complex flow that `Strapi` explains well in the [docs](https://docs.strapi.io/dev-docs/plugins/users-permissions#understanding-the-login-flow).

Why don't we use this flow then and dump `NextAuth` ? (Oh, the temptation!) Couple of very good reasons:

- You would expose your backend directly to the user.
- UX confusion: the user would be redirected to a different url and then have to suffer a whole series of redirects.
- Absence of error handling. Any error in this process would expose the user to a dry json error.
- You would lose all the goodies from `NextAuth`: cookies, access to a session, security, updates, customization,...
- ...

## Integration between NextAuth and Strapi Users & Permissions plugin providers

As said, you can do an entire auth flow using `Strapi`. This is a multi step flow. What we're going to do is skip the first steps from this `Strapi` auth flow. Why? Because `NextAuth` handles these. We then hook into a later step in the `Strapi` auth flow from inside `NextAuth`.

In our `NextAuth`, we're at this point:

```jsx
// frontend/src/api/auth/[...nextAuth]/authOptions.ts

async jwt({ token, trigger, account, profile, user, session }) {
  if (account) {
    if (account.provider === 'google') {
      // we now know we are doing a sign in using GoogleProvider
      // do some strapi stuff here
    }
  }
  return token;
},
```

Google has approved our `OAuth` request and returned some data to `NextAuth`. This data corresponds to the account and profile arguments in our `jwt callback`. We covered this in the previous chapter. When we sign in with Google, account will be populated and account.provider will be 'google'.

Now, we move to the `Strapi` auth flow. We call this `Strapi` endpoint: `/api/auth/google/callback?access_token=[>>google access token here<<]` with a Google access token. Where does this access token come from? From the account argument in our `jwt callback`. Google sent it back and `NextAuth` makes it available for us.

When calling the `Strapi` endpoint with a valid token, `Strapi` will do some magic and will pop our google user into the `Strapi` database as a `Strapi User`. `Strapi` will then create a `jwt token` for this user and send it back to us in the response from the api call.

```jsx
async jwt({ token, trigger, account, profile, user, session }) {
  if (account) {
    if (account.provider === 'google') {
      // on success, we will receive the Strapi JWT token from this
      const strapiResponse = await fetch(
        `${process.env.STRAPI_BACKEND_URL}/api/auth/google/callback?access_token=${account.access_token}`,
      );
    }
  }
  return token;
},
```

We're back in `NextAuth` (frontend) and have a fresh `Strapi token`. What do we need to do with this `Strapi token`? We need to place it inside our `NextAuth` token. How do we customize the `NextAuth` token? By using the `jwt callback`. Where did we call this api endpoint from? From inside the `jwt callback`. And this will conclude our auth flow.

## Tokens for all

There's a lot of token-ing going on so I will go over it again. Our main token is the `NextAuth` token that gets saved as a cookie in the browser. This token has a payload. By default `NextAuth` puts some things inside it like name and email. But, we also need it to hold the `Strapi token`.

The `Strapi token` authorizes our frontend user with the backend (=`Strapi`). When making an api request to the backend we need to pass the `Strapi token`. So, we need access to the token in the frontend. How? We save it inside the payload of the `NextAuth token`. What's inside the payload of the `Strapi token`? Don't know, don't care.

Finally, there is the `Google OAuth` token. Google sends back this token after a succesfull authentication. This authentication is handled by `NextAuth` in the frontend. To create a `Strapi user` using the `Strapi` social provider endpoint (`api/auth/google`) we need to send along the `Google OAuth token` to the backend via this api endpoint. What is on the payload of the `Google token`? Don't know, don't care. How does `Strapi` handle this endpoint? Don't know, don't care.

## Setting up Strapi Users & Permissions plugin

Enough theory. Let's code. We start with setting up `Strapi`. We need to activate and configure the Google provider inside `Strapi`. So, run `Strapi` (strapi develop) and open the admin panel at `http://localhost:1337/admin`:

```
Setting > Users & Permissions plugin > providers > google
```

- Toggle to enable
- Fill in the client id and secret, they should be in your frontend env file
- Ignore the redirect url
- save

Verify that the correct roles are set for the Public and Authenticated roles:

```
Setting > Users & Permissions plugin > Roles > public > permissions > User-permissions > auth
Setting > Users & Permissions plugin > Roles > authenticated > permissions > User-permissions > auth
```

All the auth options should be allowed:

![Strapi authentication permissions](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c5qz3yox1qhlcm4nafjq.png)

Finally, in the frontend env file, add `STRAPI_BACKEND_URL=http://localhost:1337`.

## Add the api call to Strapi

Next, we write our api call. Since it's a fetch, we wrap it inside a try catch block:

```tsx
try {
  const strapiResponse = await fetch(
    `${process.env.STRAPI_BACKEND_URL}/api/auth/${account.provider}/callback?access_token=${account.access_token}`,
    { cache: 'no-cache' }
  );
  const data = await strapiResponse.json();
} catch (error) {
  throw error;
}
```

Notice how we added the `cache: 'no-cache'` option to the fetch. `Next` uses an aggressive caching policy so we avoid that, just in case. In the catch block, we rethrow the error. We will handle this later.

In the actual api call, the url should make sense. But what will `Strapi` return? On success: a `Strapi` user, on error: not an `Error` but an object with an error property. Let's make Types for these:

We create some Type files:

```ts
// frontend/src/types/strapi/User.d.ts

export type StrapiUserT = {
  id: number;
  username: string;
  email: string;
  blocked: boolean;
  provider: 'local' | 'google';
};

export type StrapiLoginResponseT = {
  jwt: string;
  user: StrapiUserT;
};
```

We also create a `Strapi` error type. `Strapi` errors for all api routes are the same so we will create a generic error type here:

```ts
// frontend/src/types/strapi/StrapiError.d.ts

export type StrapiErrorT = {
  data: null;
  error: {
    status: number;
    name: string;
    message: string;
  };
};
```

We can update our fetch with these types:

```ts
const strapiResponse = await fetch(
  `${process.env.STRAPI_BACKEND_URL}/api/auth/${account.provider}/callback?access_token=${account.access_token}`,
  { cache: 'no-cache' }
);
if (!strapiResponse.ok) {
  const strapiError: StrapiErrorT = await strapiResponse.json();
  throw new Error(strapiError.error.message);
}
const strapiLoginResponse: StrapiLoginResponseT = await strapiResponse.json();
// customize token
```

So, if strapiResponse is not ok (not code 200) then we know something went wrong and we throw an error. (We will handle these later). Else, we have data of type StrapiLoginResponseT. So a user and a jwt. We will now put these on our `NextAuth token`: `token.strapiToken = strapiLoginResponse.jwt;` and we're done. Remember, by default `NextAuth` already added the name and email on our token. We just added strapiToken to it. Here is the entire `jwt callback`:

```ts
// frontend/src/api/auth/[...nextAuth]/authOptions.ts

async jwt({ token, trigger, account, user, session }) {
  if (account) {
    if (account.provider === 'google') {
      // we now know we are doing a sign in using GoogleProvider
      try {
        const strapiResponse = await fetch(
          `${process.env.STRAPI_BACKEND_URL}/api/auth/${account.provider}/callback?access_token=${account.access_token}`,
          { cache: 'no-cache' }
        );
        if (!strapiResponse.ok) {
          const strapiError: StrapiErrorT = await strapiResponse.json();
          throw new Error(strapiError.error.message);
        }
        const strapiLoginResponse: StrapiLoginResponseT =
          await strapiResponse.json();
        // customize token
        // name and email will already be on here
        token.strapiToken = strapiLoginResponse.jwt;

      } catch (error) {
        throw error;
      }
    }
  }

  return token;
},
```

And let's run this! Start up the front- and backend in dev mode and do a sign in flow. Everything ran fine but the real prove is in the `Strapi` admin panel:

```
Content manager > collection types > user
```

And we see our user:

![Added user to Strapi with NextAuth](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lekd5u85ai7yod6r3rzo.png)

Great! We're mostly done. We finished our `jwt callback` but there is something missing. We haven't updated our session.

## Writing the NextAuth session callback

We know that the `jwt callback` runs before the `session callback` so, we can simply to this:

```ts
// frontend/src/api/auth/[...nextAuth]/authOptions.ts

async session({ token, session }) {
  session.strapiToken = token.strapiToken;
  return session;
},
```

This give us a Typescript error but we will solve this in a bit. Let's first test this out using our `<LoggedInClient />` and `<LoggedInServer />` components. We uncomment the session log and check our logs after signing out and in again. As expected, session now show an extra property: strapiToken.

```ts
{
  user: {
    name: 'Peter Jacxsens',
    email: string,
    image: string,
  },
  strapiToken: string,
  expires: Date,
}
```

## More data

While we are at it. Let's put some more info on our token. We will add provider (from account), `Strapi` user id and user.blocked (from StrapiLoginResponse). We update our `jwt callback`:

```ts
token.provider = account.provider;
token.strapiUserId = strapiLoginResponse.user.id;
token.blocked = strapiLoginResponse.user.blocked;
```

and the `session callbacks`:

```ts
session.provider = token.provider;
session.user.strapiUserId = token.strapiUserId;
session.user.blocked = token.blocked;
```

Testing this, our session from `useSession` or `getServerSession()` now looks as expected:

```ts
{
  user: {
    name: 'Peter Jacxsens',
    email: string,
    image: string,
    blocked: boolean,
    strapiUserId: number,
  },
  strapiToken: string,
  provider: 'google',
  expires: Date,
}
```

## Setting types on the NextAuth session and jwt callback arguments

The customizations we just did as well as the strapiToken we added earlier all trigger TypeScript errors both in the `jwt` as the `session callbacks`. The long list of callback arguments receive their props somewhere inside `NextAuth`. But `NextAuth` provides the opportunity to extend them.

Note: I'm not sure the following TypeScript definitions are correct. All the TypeScript errors resolve but be careful here.

We create a new TypeScript definitions file:

```ts
// frontend/src/types/nextauth/next-auth.d.ts

// https://next-auth.js.org/getting-started/typescript
// not sure about any of this but it throws no TS errors (anymore)

import NextAuth, { DefaultSession } from 'next-auth';
import { JWT } from 'next-auth/jwt';
import { StrapiUserT } from './strapi/StrapiLogin';

declare module 'next-auth' {
  // Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context

  interface Session {
    strapiToken?: string;
    provider?: 'google' | 'local';
    user: User;
  }

  /**
   * The shape of the user object returned in the OAuth providers' `profile` callback,
   * or the second parameter of the `session` callback, when using a database.
   */
  interface User extends DefaultSession['user'] {
    // not setting this will throw ts error in authorize function
    strapiUserId?: number;
    blocked?: boolean;
  }
}

declare module 'next-auth/jwt' {
  // Returned by the `jwt` callback and `getToken`, when using JWT sessions
  interface JWT {
    strapiUserId?: number;
    blocked?: boolean;
    strapiToken?: string;
    provider?: 'local' | 'google';
  }
}
```

You can read all about this in the [NextAuth docs](https://next-auth.js.org/getting-started/typescript#module-augmentation) but I will explain if briefly. The syntax we are using is called Module Augmentation. This is a TypeScript thing that allows to extend (and merge?) interfaces. That's pretty much all I know about it.

As for content. We extend 3 of the callback arguments: token, user and session. We know that session returns something like this: `{ strapiToken, provider, user: { ... }, ... }`. It seems that internally, `NextAuth` uses the typeof the `jwt callback` user argument to type session.user. Because we put extra data on there, we have to extend the DefaultUser (name?:, email?:, ...) with strapiUserId and blocked, also both optional.

We then update the Session interface with this extended user and with the 2 other optional properties strapiToken and provider. Finally, we also extend the token interface with all the properties we put on it: strapiToken, strapiUserId, provider and blocked, all optional.

Here is a tip: inside the callbacks, type for example `token.` and then TypeScript will suggest all possible properties. If you can't see the one you need or see an extra one, you did something wrong.

## Conclusion

We just learned how to put a user into the `Strapi` database using GoogleProvider. All and all, this is not very difficult. The tricky part is to know what, where and how `NextAuth` handles things.

The flow goes like this:

1. `NextAuth` makes a request to Google OAuth.
2. Google OAuth returns data.
3. `NextAuth` makes this data available in its callback functions.
4. Within the `jwt callback` we check if account is defined. (only on sign in will account be defined)
5. We call a `Strapi` Google provider endpoint with a Google token.
6. `Strapi` verifies the token and adds a user to the database.
7. `Strapi` sends this user data + a `Strapi JWT token` back.
8. Inside the `jwt callback` in `NextAuth`, we receive this data + `Strapi JWT token` from `Strapi`.
9. We use the `jwt callback` to put the `Strapi JWT token` inside our `NextAuth jwt token`.
10. We use the `session callback` to read out this token from the frontend using `useSession` hook or `getServerSession` function.
11. The frontend can now make an api to the backend (`Strapi`), adding the `Strapi token`.

A final note, this [gist](https://gist.github.com/thevisioner/91ea39fd1de1a99ea729fea8f7edabab) helped me a lot with figuring all of this out.

---

If you want to support my writing, you can [donate with paypal](https://www.paypal.com/donate/?hosted_button_id=4D78YQU4V5NEJ).