Introduction#

GitHub this note shows

  • Setup NextJS with NextAuth
  • Add GitHub Provider
  • Add Cognito Provider
  • Serverside auth
  • Customer Provider UI pages

Setup NextAuth#

Create a new NextJS project with Tailwind as normal

npx create-next-app@latest

Add NextAuth library

npm install next-auth

Project structure with NextJS 13

|--app
|--globals.css
|--layout.css
|--page.tsx
|--pages
|--api
|--auth
|--[...nextauth].js
|--public
|--.env.local
|--next.config.js
|--package.json
|--postcss.config.js
|--tailwind.config.js
|--tsconfig.json
|--.eslintrc.json

Add GitHub Provider#

Create pages/api/auth/[...nextauth].js as the following

import NextAuth from "next-auth";
import GithubProvider from "next-auth/providers/github";
import CognitoProvider from "next-auth/providers/cognito";
export const authOptions = {
// Configure one or more authentication providers
providers: [
GithubProvider({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET,
}),
],
secret: process.env.JWT_SECRET,
};
export default NextAuth(authOptions);

The environment variables are configured in .env.local as

GITHUB_ID=
GITHUB_SECRET=
NEXTAUTH_URL=http://localhost:3000
JWT_SECRET=

Note that JWT_SECRET ca be generated from HERE

Add Cognito Provider#

Similarly, add Cognito Provider as

import NextAuth from "next-auth";
import GithubProvider from "next-auth/providers/github";
import CognitoProvider from "next-auth/providers/cognito";
export const authOptions = {
// Configure one or more authentication providers
providers: [
CognitoProvider({
clientId: process.env.COGNITO_CLIENT_ID,
clientSecret: process.env.COGNITO_CLIENT_SECRET,
issuer: process.env.COGNITO_ISSUER,
}),
],
secret: process.env.JWT_SECRET,
};

Please take note the cognito issuer format

https://cognito-idp.{region}.amazonaws.com/{PoolId}

then update the .env.local as

COGNITO_CLIENT_ID=
COGNITO_CLIENT_SECRET=
COGNITO_ISSUER=https://cognito-idp.{region}.amazonaws.com/{PoolId}
NEXTAUTH_URL=http://localhost:3000
JWT_SECRET=

Log In Page#

In the NextJS 13, we can setup it in the RootLayout, and use SessionProvider to pass session of user as the following

"use client";
import { SessionProvider } from "next-auth/react";
import "./globals.css";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<SessionProvider>
<body>{children}</body>
</SessionProvider>
</html>
);
}

The Log In page with login button which will trigger Provider as

"use client";
import { signIn, signOut, useSession } from "next-auth/react";
import Image from "next/image";
export default function Home() {
const { data: session } = useSession();
console.log(session);
return (
<main className="flex min-h-screen flex-col items-center justify-between p-24">
{session?.user ? (
<>
<h1>Hello {session.user.name}</h1>
<button
className="bg-orange-300 px-20 py-3 rounded-sm"
onClick={() => signOut()}
>
Sign Out
</button>
</>
) : (
<>
<h1>Please login first</h1>
<button
type="submit"
// disabled
className="bg-green-400 px-20 py-3 rounded-sm cursor-pointer"
onClick={() => signIn()}
>
Sign In
</button>
</>
)}
</main>
);
}

Server Side Auth#

  • Get cookies or token from header request
  • Verify the cookies or token, get user information
  • Server render the responding page
import { cookies, headers } from "next/headers";
import { decode } from "next-auth/jwt";
const getUser = async () => {
const authHeader = headers().get("authorization");
const sessionToken = cookies().get("next-auth.session-token");
const user = await decode({
token: sessionToken?.value,
secret: process.env.JWT_SECRET as string,
});
console.log("auth header ", authHeader);
console.log("session token from cookies ", sessionToken);
console.log("decode token ", user);
return user;
};
const ProfilePage = async () => {
const user = await getUser();
if (user) {
return (
<div>
<h1>
Hello {user.email} {user.name}{" "}
</h1>
</div>
);
}
return (
<div>
<h1>Please log in first</h1>
</div>
);
};
export default ProfilePage;

Custom Provider UI#

  • Custom provider UI
  • Initial login with provider
  • Refresh with only local

First, update the authOptions with pages

export const authOptions = {
// provider here
// then add pages
pages: {
signIn: "/auth",
},
};

Then create a custom login page at /app/auth/page.tsx as the below

"use client";
import { signIn } from "next-auth/react";
const LoginPage = async () => {
return (
<div className="mx-auto max-w-3xl min-h-screen flex justify-center items-center">
<div className="bg-green-100 px-10 py-10 rounded-md text-center flex-col space-y-5">
<h1 className="mb-5">Please Login First</h1>
<input
className="block w-full py-2 mb-5"
type="text"
id="username"
placeholder="entest"
/>
<input
className="block w-full py-2 mb-5"
type="password"
id="password"
placeholder="Nextjs@2023"
/>
<button
className="hover:bg-green-600 px-32 py-2 bg-green-500 rounded-sm w-full"
onClick={() => {
// get username, password from form
signIn("cognito", {
// username: username,
// email: username,
// password: password,
redirect: true,
callbackUrl: "/",
});
}}
>
<text className="font-bold text-white">Sign In with Cognito</text>
</button>
</div>
</div>
);
};
export default LoginPage;

The first time log in will receives the following items in cookies, and forwarded to UI of the identity provider.

  • next-auth.session-token
  • next-auth.callback-url
  • next-auth.csrf-token

Subsequence login look like a refresh and does not get forwarded to the identity provider provided that the next-auth.csrf-token still in the cookies ==> javascript client cached something.

Amplify Hosting#

  • Amplify support NextJS 13
  • Connect the GitHub repo to AWS Amplify hosting
  • Update some environment variables in Amplify Build Setting
  • Update cognito re-direct URL in AWS Cognito console
COGNITO_CLIENT_ID=123abc
COGNITO_CLIENT_SECRET=123abc
COGNITO_ISSUER=https://cognito-idp.${region}.amazonaws.com/{cognito-user-pool-id}
NEXTAUTH_URL=https://${amplify-domain}.com
JWT_SECRET=123abc
NEXTAUTH_SECRET=112abc

Update the Amplify Build setting as below

version: 1
frontend:
phases:
preBuild:
commands:
- npm ci
build:
commands:
- echo "NEXTAUTH_URL=$NEXTAUTH_URL" >> .env
- echo "NEXTAUTH_SECRET=$NEXTAUTH_SECRET" >> .env
- echo "COGNITO_ISSUER=$COGNITO_ISSUER" >> .env
- echo "COGNITO_CLIENT_SECRET=$COGNITO_CLIENT_SECRET" >> .env
- echo "COGNITO_CLIENT_ID=$COGNITO_CLIENT_ID" >> .env
- echo "JWT_SECRET=$JWT_SECRET" >> .env
- npm run build
artifacts:
baseDirectory: .next
files:
- '**/*'
cache:
paths:
- node_modules/**/*

Finally update the re-directl URL in AWS Cognito console

https://main.${amplify-domain}.amplifyapp.com/api/auth/callback/cognito

Cognito Id Token#

  • Option 1. Set the Id Token into the session => session token => server side decode
  • Option 2. Use setCookies from nookies to set Id Token into the browser cookie

First, we can augment the Next-Auth module or extend its Session iterface to contain id_token by creating a file nextauth.d.ts in the root folder as below

import { DefaultSession } from "next-auth";
declare module "next-auth" {
interface Session extends DefaultSession {
id_token?: string;
}
}

To get the Conigto Id Token we have to overwrite the callbacks in [...nextuath].js authOptions

callbacks: {
async signIn({ user, account, profile, email, credentials }) {
return true;
},
async redirect({ url, baseUrl }) {
return baseUrl;
},
async session({ session, user, token }) {
try {
session.id_token = token.id_token
} catch(error){
}
return session;
},
async jwt({ token, user, account, profile, isNewUser }) {
try {
token.id_token = account.id_token
} catch(error){
}
return token;
},
},

Then from client we can access the id Token

"use client";
import { useSession } from "next-auth/react";
const BookPage = () => {
const { data: session, status } = useSession();
return (
<main>
{session ? (
<>
id_token: {session.id_token} and email: {session.user?.email}
</>
) : (
<>Please log in first</>
)}
</main>
);
};
export default BookPage;

Server side now can access the id token which decoded from the session token

import { cookies, headers } from "next/headers";
import { decode } from "next-auth/jwt";
const getUser = async () => {
console.log(headers());
const sessionToken = cookies().get("next-auth.session-token");
const user = await decode({
token: sessionToken?.value,
secret: process.env.JWT_SECRET as string,
});
console.log("cookies ", cookies());
console.log("decode token ", user);
return user;
};
const ProfilePage = async () => {
const user = await getUser();
if (user) {
return (
<div>
<h1>
Hello {user.email} {user.name}{" "}
</h1>
</div>
);
}
return (
<div>
<h1>Please log in first</h1>
</div>
);
};
export default ProfilePage;

Trobleshooting#

Server side get header and token from cookies which set in the browser

// deployment
const sessionToken = cookies().get("__Secure-next-auth.session-token");
// local
const sessionToken = cookies().get("next-auth.session-token");

Setup cognito hosted ui, look like this

https://main.d22o1v6qly4z0s.amplifyapp.com/api/auth/callback/cognito

And NEXTAUTH_URL

// local test
NEXTAUTH_URL=http://localhost:3000
// deployment
NEXTAUTH_URL=https://main.d22o1v6qly4z0s.amplifyapp.com/

Amplify Hosting#

  • Goto Amplify console
  • Select the Git project to deploy
  • Update the environment variables

Then update the build setting

version: 1
frontend:
phases:
preBuild:
commands:
- npm ci
build:
commands:
- echo "NEXTAUTH_URL=$NEXTAUTH_URL" >> .env
- echo "NEXTAUTH_SECRET=$NEXTAUTH_SECRET" >> .env
- echo "COGNITO_ISSUER=$COGNITO_ISSUER" >> .env
- echo "COGNITO_CLIENT_SECRET=$COGNITO_CLIENT_SECRET" >> .env
- echo "COGNITO_CLIENT_ID=$COGNITO_CLIENT_ID" >> .env
- echo "COGNITO_USER_NAME=$COGNITO_USER_NAME" >> .env
- echo "COGNITO_POOL_ID=$COGNITO_POOL_ID" >> .env
- echo "COGNITO_IDENTITY_POOL_ID=$COGNITO_IDENTITY_POOL_ID" >> .env
- echo "JWT_SECRET=$JWT_SECRET" >> .env
- echo "REGION=$REGION" >> .env
- echo "BUCKET=$BUCKET" >> .env
- echo "JWT_SECRET=$JWT_SECRET" >> .env
- npm run build
artifacts:
baseDirectory: .next
files:
- '**/*'
cache:
paths:
- node_modules/**/*

Reference#