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 providersproviders: [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:3000JWT_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 providersproviders: [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:3000JWT_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><buttonclassName="bg-orange-300 px-20 py-3 rounded-sm"onClick={() => signOut()}>Sign Out</button></>) : (<><h1>Please login first</h1><buttontype="submit"// disabledclassName="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 pagespages: {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><inputclassName="block w-full py-2 mb-5"type="text"id="username"placeholder="entest"/><inputclassName="block w-full py-2 mb-5"type="password"id="password"placeholder="Nextjs@2023"/><buttonclassName="hover:bg-green-600 px-32 py-2 bg-green-500 rounded-sm w-full"onClick={() => {// get username, password from formsignIn("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=123abcCOGNITO_CLIENT_SECRET=123abcCOGNITO_ISSUER=https://cognito-idp.${region}.amazonaws.com/{cognito-user-pool-id}NEXTAUTH_URL=https://${amplify-domain}.comJWT_SECRET=123abcNEXTAUTH_SECRET=112abc
Update the Amplify Build setting as below
version: 1frontend:phases:preBuild:commands:- npm cibuild: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 buildartifacts:baseDirectory: .nextfiles:- '**/*'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
// deploymentconst sessionToken = cookies().get("__Secure-next-auth.session-token");// localconst 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 testNEXTAUTH_URL=http://localhost:3000// deploymentNEXTAUTH_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: 1frontend:phases:preBuild:commands:- npm cibuild: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 buildartifacts:baseDirectory: .nextfiles:- '**/*'cache:paths:- node_modules/**/*