The final code for this chapter is available on github (branch 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:
// 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:
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.
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:
// 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.
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:
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:
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:
// 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:
// 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:
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
:
// 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:
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:
// 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.
{
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
:
token.provider = account.provider;
token.strapiUserId = strapiLoginResponse.user.id;
token.blocked = strapiLoginResponse.user.blocked;
and the session callbacks
:
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:
{
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:
// 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 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:
-
NextAuth
makes a request to Google OAuth. - Google OAuth returns data.
-
NextAuth
makes this data available in its callback functions. - Within the
jwt callback
we check if account is defined. (only on sign in will account be defined) - We call a
Strapi
Google provider endpoint with a Google token. -
Strapi
verifies the token and adds a user to the database. -
Strapi
sends this user data + aStrapi JWT token
back. - Inside the
jwt callback
inNextAuth
, we receive this data +Strapi JWT token
fromStrapi
. - We use the
jwt callback
to put theStrapi JWT token
inside ourNextAuth jwt token
. - We use the
session callback
to read out this token from the frontend usinguseSession
hook orgetServerSession
function. - The frontend can now make an api to the backend (
Strapi
), adding theStrapi token
.
A final note, this gist helped me a lot with figuring all of this out.
If you want to support my writing, you can donate with paypal.