In this tutorial, we will learn how to build a Next.js frontend application with GitHub authentication using Strapi. We'll cover Next.js route handlers and middleware to create a secure, user-friendly authentication system.
By the end of this guide, we'll have a working example ready to integrate other auth providers supported by Strapi. Let's get started.
See the project overview and demo of this video on YouTube.
To set up Next.js GitHub OAuth with Strapi, we will first start by setting up our Next.js frontend. We have to first navigate to our preferred project folder. This is where we will create our Strapi and Next.js projects.
Start by running the following command in the terminal:
npx create-next-app@latest
The command above will install the lastest Next.js application.
We will call our project frontend.
We will be asked the following question during installation; and we will select "Yes" for everything except using ESLint.
➜ auth-provider-blog npx create-next-app@latest
✔ What is your project named? … frontend
✔ Would you like to use TypeScript? … Yes
✔ Would you like to use ESLint? … No
✔ Would you like to use Tailwind CSS? … Yes
✔ Would you like to use `src/` directory? … Yes
✔ Would you like to use App Router? (recommended) … Yes
✔ Would you like to customize the default import alias (@/*)? … Yes
Creating a new Next.js app in /Users/paulbratslavsky/temp/auth-provider-blog/frontend.
Now that we have our Next.js project set up, we can start it with the following command.
cd frontend
yarn dev
In the command above, we CD into our frontend
folder, where our Next.js application resides and start the development server.
Once our project starts, we will see the following displayed on our screen.
Now, let's create our form for signing in. We are going to replace the code on our page.tsx
file with the following code.
1// ./src/app/page.tsx
2
3import Link from "next/link";
4
5export default function Home() {
6 const backendUrl =
7 process.env.NEXT_PUBLIC_BACKEND_URL ?? "http://localhost:1337";
8 const path = "/api/connect/github";
9 const url = new URL(backendUrl + path);
10
11 return (
12 <div className="flex min-h-screen flex-1 flex-col justify-center py-12 sm:px-6 lg:px-8">
13 <div className="sm:mx-auto sm:w-full sm:max-w-md">
14 <img
15 className="mx-auto h-10 w-auto"
16 src="https://tailwindui.com/img/logos/mark.svg?color=indigo&shade=600"
17 alt="Your Company"
18 />
19 <h2 className="mt-6 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">
20 Sign in to your account
21 </h2>
22 </div>
23
24 <div className="mt-10 sm:mx-auto sm:w-full sm:max-w-[480px]">
25 <div className="bg-white px-6 py-12 shadow sm:rounded-lg sm:px-12">
26 <div>
27 <div className="grid grid-cols-1 gap-4">
28 <form>
29 <Link
30 href={url.href}
31 className="flex w-full items-center justify-center gap-3 rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus-visible:ring-transparent"
32 >
33 <svg
34 className="h-5 w-5 fill-[#24292F]"
35 aria-hidden="true"
36 fill="currentColor"
37 viewBox="0 0 20 20"
38 >
39 <path
40 fillRule="evenodd"
41 d="M10 0C4.477 0 0 4.484 0 10.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0110 4.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.203 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.942.359.31.678.921.678 1.856 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0020 10.017C20 4.484 15.522 0 10 0z"
42 clipRule="evenodd"
43 />
44 </svg>
45 <span className="text-sm font-semibold leading-6">
46 GitHub
47 </span>
48 </Link>
49 </form>
50 </div>
51 </div>
52 </div>
53 </div>
54 </div>
55 );
56}
All we are doing in the code above is creating a button link that will redirect us to the http://localhost:1337/api/connect/github Strapi API endpoint, which is responsible for initiating authentication using the GitHub provider.
We will cover this in more detail, but first, let's build out our front end, which will handle this process.
Once we have added the code above, we can restart our application, and should now see the following view.
So why do we need a route to handle the auth callback? This is because we need to authenticate our Strapi user and set the JWT token.
But before we proceed to write the code, here are the steps that will be in our auth flow.
We will use our local dev as an example.
access_token
from Github, which can be used for a period of time to make authorized requests to Github to get the user information.access_token.
This will be our callback, and we will be responsible for logging in or creating our first user in Strapi via GitHub credentials.To accomplish Next.js GitHub Authentication, we will create an Route Handler in Next.js, you can learn more about them here.
In our project, inside the app
folder, create the following folders connect/[provider]/redirect
. After that, inside the new redirect
folder, create a route.ts
file with the following code:
1// ./src/app/connect/[provider]/[redirect]/route.ts
2
3import { NextResponse } from "next/server";
4import { cookies } from "next/headers";
5
6const config = {
7 maxAge: 60 * 60 * 24 * 7, // 1 week
8 path: "/",
9 domain: process.env.HOST ?? "localhost",
10 httpOnly: true,
11 secure: process.env.NODE_ENV === "production",
12};
13
14export const dynamic = "force-dynamic"; // defaults to auto
15export async function GET(
16 request: Request,
17 params: { params: { provider: string } }
18) {
19 const { searchParams } = new URL(request.url);
20 const token = searchParams.get("access_token");
21
22 if (!token) return NextResponse.redirect(new URL("/", request.url));
23
24 const provider = params.params.provider;
25 const backendUrl =
26 process.env.NEXT_PUBLIC_BACKEND_URL ?? "http://localhost:1337";
27 const path = `/api/auth/${provider}/callback`;
28
29 const url = new URL(backendUrl + path);
30 url.searchParams.append("access_token", token);
31
32 const res = await fetch(url.href);
33 const data = await res.json();
34
35 cookies().set("jwt", data.jwt, config);
36
37 return NextResponse.redirect(new URL("/dashboard", request.url));
38}
Let's break down what the above code does.
1import { NextResponse } from "next/server";
2import { cookies } from "next/headers";
NextResponse
: A utility from Next.js for generating server responses.cookies
: A utility for handling cookies in server-side requests and responses.The configuration for our JWT token:
1const config = {
2 maxAge: 60 * 60 * 24 * 7, // 1 week
3 path: "/",
4 domain: process.env.HOST ?? "localhost",
5 httpOnly: true,
6 secure: process.env.NODE_ENV === "production",
7};
maxAge
: The cookie's lifespan is set to 1 week.path
: The path scope of the cookie is set to the root directory.domain
: The domain scope of the cookie, taken from an environment variable or defaulting to localhost
.httpOnly
: The cookie is accessible only through HTTP, not JavaScript.secure
: The cookie is sent only over HTTPS if in production.Extracts the query parameters from the request URL to get the access_token.
1const { searchParams } = new URL(request.url);
2const token = searchParams.get("access_token");
If the access_token
is not present, redirects the user to the homepage.
1if (!token) return NextResponse.redirect(new URL("/", request.url));
Constructs the URL for the backend or Strapi authentication callback, appending the access_token as a query parameter.
1const provider = params.params.provider;
2const backendUrl =
3 process.env.NEXT_PUBLIC_BACKEND_URL ?? "http://localhost:1337";
4const path = "/api/auth/github/callback";
5const url = new URL(backendUrl + path);
6url.searchParams.append("access_token", token);
Sends a request to the backend or Strapi authentication endpoint and waits for the response, then parses the response as JSON.
1const res = await fetch(url.href);
2const data = await res.json();
1cookies().set("jwt", data.jwt, config);
2Sets a cookie named jwt with the JWT obtained from the backend response, using the previously defined configuration.
Redirects the user to the /dashboard
page after setting the cookie.
1return NextResponse.redirect(new URL("/dashboard", request.url));
The code above will authenticate our user and set our cookie.
Now that we have this in place let's create our Dashboard page, which will be our protected route. Later on, using Next.js protected routes ideology, we will secure this page.
With the route handler configured, we need to create a dashboard page that a user will be redirected to.
Inside the app
folder, create a new folder called dashboard
. Within that folder, create a page.tsx
file and paste it into the following code.
1// ./src/app/dashboard/page.tsx
2
3import { LogoutButton } from "@/components/Logout";
4import React from "react";
5
6export default function Dashboard() {
7 return (
8 <div className="container mx-auto px-4 flex justify-between items-center my-4">
9 <h2 className="text-xl">Dashboard</h2>
10 <LogoutButton />
11 </div>
12 );
13}
As we can see that this code above uses a LogoutButton
component. So let's create it.
Inside the src
folder, create a new folder called components
. Within the components
folder, create the LogoutButton.tsx
file and paste in the following code:
1// ./src/components/LogoutButton.tsx
2
3import { cookies } from "next/headers";
4import { redirect } from "next/navigation";
5
6const config = {
7 maxAge: 60 * 60 * 24 * 7, // 1 week
8 path: "/",
9 domain: process.env.HOST ?? "localhost",
10 httpOnly: true,
11 secure: process.env.NODE_ENV === "production",
12};
13
14async function logoutAction() {
15 "use server";
16 cookies().set("jwt", "", { ...config, maxAge: 0 });
17 redirect("/");
18}
19
20export function LogoutButton() {
21 return (
22 <form action={logoutAction}>
23 <button
24 type="submit"
25 className="bg-white hover:bg-gray-100 text-gray-800 font-semibold py-2 px-4 border border-gray-400 rounded shadow"
26 >
27 Logout
28 </button>
29 </form>
30 );
31}
The above code will handle the logout functionality by removing our cookie
and redirecting the user to the /
home route.
This is accomplished by our logoutAction
server action, which will be triggered upon form submission.
Now that our dashboard
page is ready, let's see if it works. We can view it by typing in the dashboard
path inside the url bar. So, let's restart our app and navigate our the dashboard route http://localhost:3000/dashboard.
Excellent, we now have our Dashboard page with our LogoutButton
component, which is really cool.
We will revisit how to make this route private via Next.js Auth Middleware, but let's focus on setting up our Strapi backend.
Let's get started by creating our Strapi project. We just released the Strapi 5 release candidate, so let's take it out for a spin. This example would work the same with Strapi 4, too.
To get started, at the root of our project, run the following command:
npx create-strapi-app@rc backend --quickstart
Once all the dependencies get installed, you will be greeted with the following screen, go ahead and create our first Strapi Admin user.
Once done, you will be greeted by the admin panel.
Now that we have our Strapi project, we can test our login with the GitHub button on our frontend app's Home page.
Clicking the button will show us the message above: This is a good sign; it means that our redirect link is working correctly, and we need to set up our provider in Strapi Admin.
Let's navigate to our Providers page and select GitHub, which we will use in this tutorial.
We will need to fill out the following fields: Enabled
, which we will set to true
. Then, we will need to get Client ID
and Client Secret
from GitHub and fill them in.
Finally, we must reference our The redirect URL to your front-end app, which we created earlier via our Next.js Route Handler. This URL will be accessed with the following URL path: http://localhost:3000/connect/github/redirect
.
Let's see where we can find these credentials. We must log into our GitHub account and navigate to the OAuthAPP page.
Once on the page, we will have access to the Client ID
and the ability to create Client Secret
. Let's go ahead and do that now.
Now, we navigate to our Strapi Admin area and inside our GitHub Provider menu and update the Client ID
and Client Secret
with the credentials.
We will also need to fill out the redirect URL for our frontend; this references our Route Handler we created earlier. The value for this should be: http://localhost:3000/connect/github/redirect
And finally, we need to make one more change on our GitHub OAuth settings page.
HomePage URL
: This references our Next.js frontend. This will be http://localhost:3000
.Authorization callback URL
: This references our Strapi call back handler. And this will be http://localhost:1337/api/connect/github/callback
.Remember, we have to save our changes.
After Github OAuth is set up, we need to make sure that both our Next.js and Strapi apps are running. We navigate to our project's home page, and let's try our GitHub login button.
Nice, our create account or login with GitHub is working. We can confirm that a new user was created in our Strapi Admin panel.
Even though we redirected to our Dashboard route, it is still not protected; you can test it by logging out and trying to navigate to it via the browser URL bar.
Notice that we can navigate to our Dashboard without logging in. Let's change that.
Let's look at how we can create Next.js middleware to make the route private and check whether a user is logged in. By the way, you can learn more about Next.js Auth Middleware here.
Inside the src
folder in our Next.js project, we create a file named middleware.ts
and add the following code.
1// ./src/middleware.ts
2
3import { NextResponse } from "next/server";
4import type { NextRequest } from "next/server";
5import { getUserMeLoader } from "@/services/user-me-loader";
6
7export async function middleware(request: NextRequest) {
8 const user = await getUserMeLoader();
9 const currentPath = request.nextUrl.pathname;
10
11 if (currentPath.startsWith("/dashboard") && user.ok === false) {
12 return NextResponse.redirect(new URL("/", request.url));
13 }
14
15 return NextResponse.next();
16}
In the code above, we use the getUserMeLoader
function to get the actual user from Strapi who is logged in.
Then, we check to see if we don't have the logged-in user and redirect them to the home page.
We may wonder why we are looking to get the user info from Strapi instead of using our own JWT token.
That is because we want our source of truth to be the server where our user is actually logged in. So we will make a call to a Strapi endpoint users/me
with the provided JWT token. If the token is valid and the user is logged in, we will get the user's data in the response.
So, let's create our getUserMeLoader
function.
Let's create a folder named services
in the src
folder. Create a file called user-me-loader.ts
and add the following code.
1import { cookies } from "next/headers";
2
3export async function getAuthToken() {
4 const authToken = cookies().get("jwt")?.value;
5 return authToken;
6}
7
8export async function getUserMeLoader() {
9 const baseUrl =
10 process.env.NEXT_PUBLIC_BACKEND_URL ?? "http://localhost:1337";
11 const path = "/api/users/me";
12
13 const url = new URL(path, baseUrl);
14
15 const authToken = await getAuthToken();
16 if (!authToken) return { ok: false, data: null, error: null };
17
18 try {
19 const response = await fetch(url.href, {
20 method: "GET",
21 headers: {
22 "Content-Type": "application/json",
23 Authorization: `Bearer ${authToken}`,
24 },
25 cache: "no-cache",
26 });
27 const data = await response.json();
28 if (data.error) return { ok: false, data: null, error: data.error };
29 return { ok: true, data: data, error: null };
30 } catch (error) {
31 console.log(error);
32 return { ok: false, data: null, error: error };
33 }
34}
The above code fetches our logged-in user from Strapi.
Now that we have all these components let's test our auth and redirect to see if our Dashboard page is protected. We can secure more pages using the Next.js protected routes ideology.
In the image above, we can see that when we try to directly navigate to the /dashboard
route, we cannot access it. Our middleware is working.
In this tutorial, we walked through the process of implementing social authentication in a Strapi and Next.js application using the GitHub provider. Next.js GitHub Authentication is one of many ways to implement social login in an application.
We created a dynamic route handle to allow us to add additional providers easily in the future.
We have covered each step in detail, from setting up our Next.js frontend, creating a user-friendly sign-in form to configuring Strapi, and handling the authentication flow with Next.js middleware for route protection.
By following these steps, we have a secure and efficient authentication system that enhances user experience by leveraging GitHub OAuth with Strapi for social login.
This setup simplifies the login process for users and ensures that our application remains secure and scalable.
Thank you for checking out this tutorial, and happy coding!
And remember, if you ever have questions, you can always join us on our Discord during the "Open Office Hours."
Join us at 4 AM CST (9:00 AM GMT) for our new early bird session. Perfect for our global community members!
Remember our regular session at 12:30 PM CST (6:30 PM GMT). It's a great time for an afternoon break and chat!