diff --git a/app/.env.local.example b/app/.env.local.example new file mode 100644 index 00000000..8f97301d --- /dev/null +++ b/app/.env.local.example @@ -0,0 +1,17 @@ +####################################### +# Authentication +####################################### + +# A random string used for encrypting tokens and verification hashes. +# Generate its value by running `openssl rand -base64 32` +# or via https://generate-secret.vercel.app/32 +NEXTAUTH_SECRET="Replace me" + +# The template provides a Cognito authentication example by default. To make it work, +# you'll need to set these. Using Cognito is not required! See the setup-authentication.md +# documentation for more details. +COGNITO_CLIENT_ID= +COGNITO_CLIENT_SECRET= +COGNITO_DOMAIN= +COGNITO_ISSUER=https://cognito-idp.{PUT REGION HERE}.amazonaws.com/{PUT USER POOL ID HERE} +COGNITO_DOMAIN=https://{PUT SUBDOMAIN HERE}.auth.{PUT REGION HERE}.amazoncognito.com diff --git a/app/.gitignore b/app/.gitignore index 32056e09..596c07a1 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -26,6 +26,7 @@ npm-debug.log* # local env files .env*.local +!.env.local.example # vercel .vercel diff --git a/app/next-env.d.ts b/app/next-env.d.ts index 4f11a03d..fd36f949 100644 --- a/app/next-env.d.ts +++ b/app/next-env.d.ts @@ -1,5 +1,6 @@ /// /// +/// // NOTE: This file should not be edited // see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/app/package-lock.json b/app/package-lock.json index a34ad12f..6bfcd58a 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -13,6 +13,7 @@ "@uswds/uswds": "3.1.0", "i18next": "^23.0.0", "next": "^14.0.0", + "next-auth": "^5.0.0-beta.3", "next-i18next": "^14.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -88,6 +89,27 @@ "node": ">=6.0.0" } }, + "node_modules/@auth/core": { + "version": "0.0.0-manual.e9863699", + "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.0.0-manual.e9863699.tgz", + "integrity": "sha512-/hVzGuFw7nAZimliD8kpuKnNjvkRu+jpaVhYB/FaIXLNJFNwhbO2MgXBnr5tvLIHgRJnR5C9UN5RNpQXiFHuSA==", + "dependencies": { + "@panva/hkdf": "^1.0.4", + "cookie": "0.5.0", + "jose": "^4.11.1", + "oauth4webapi": "^2.0.6", + "preact": "10.11.3", + "preact-render-to-string": "5.2.3" + }, + "peerDependencies": { + "nodemailer": "^6.8.0" + }, + "peerDependenciesMeta": { + "nodemailer": { + "optional": true + } + } + }, "node_modules/@aw-web-design/x-default-browser": { "version": "1.4.126", "resolved": "https://registry.npmjs.org/@aw-web-design/x-default-browser/-/x-default-browser-1.4.126.tgz", @@ -4700,6 +4722,14 @@ "node": ">= 8" } }, + "node_modules/@panva/hkdf": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.1.1.tgz", + "integrity": "sha512-dhPeilub1NuIG0X5Kvhh9lH4iW3ZsHlnzwgwbOlgwQ2wG1IqFzsgHqmKPk3WzsdWAeaxKJxgM0+W433RmN45GA==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -10424,7 +10454,6 @@ "version": "0.5.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", - "dev": true, "engines": { "node": ">= 0.6" } @@ -16941,6 +16970,14 @@ "jiti": "bin/jiti.js" } }, + "node_modules/jose": { + "version": "4.15.4", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.4.tgz", + "integrity": "sha512-W+oqK4H+r5sITxfxpSU+MMdr/YSWGvgZMQDIsNoBDGGy4i7GBPTtvFKibQzW06n3U3TqHjhvBJsirShsEJ6eeQ==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -17890,6 +17927,24 @@ } } }, + "node_modules/next-auth": { + "version": "5.0.0-beta.3", + "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-5.0.0-beta.3.tgz", + "integrity": "sha512-WOKhATBFGeONV+29HzFmspNmL7NXxrsCWLfaDKmAd/4DD1nqXE0BzNFH8t3SJBx7PUDMnB6F7xB76LM/AaV1MQ==", + "dependencies": { + "@auth/core": "experimental" + }, + "peerDependencies": { + "next": "^14", + "nodemailer": "^6.6.5", + "react": "^18.2.0" + }, + "peerDependenciesMeta": { + "nodemailer": { + "optional": true + } + } + }, "node_modules/next-i18next": { "version": "14.0.3", "resolved": "https://registry.npmjs.org/next-i18next/-/next-i18next-14.0.3.tgz", @@ -18228,6 +18283,14 @@ "integrity": "sha512-6xpotnECFy/og7tKSBVmUNft7J3jyXAka4XvG6AUhFWRz+Q/Ljus7znJAA3bxColfQLdS+XsjoodtJfCgeTEFQ==", "dev": true }, + "node_modules/oauth4webapi": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-2.3.0.tgz", + "integrity": "sha512-JGkb5doGrwzVDuHwgrR4nHJayzN4h59VCed6EW8Tql6iHDfZIabCJvg6wtbn5q6pyB2hZruI3b77Nudvq7NmvA==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -19700,6 +19763,31 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "dev": true }, + "node_modules/preact": { + "version": "10.11.3", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.11.3.tgz", + "integrity": "sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/preact-render-to-string": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.3.tgz", + "integrity": "sha512-aPDxUn5o3GhWdtJtW0svRC2SS/l8D9MAgo2+AWml+BhDImb27ALf04Q2d+AHqUUOc6RdSXFIBVa2gxzgMKgtZA==", + "dependencies": { + "pretty-format": "^3.8.0" + }, + "peerDependencies": { + "preact": ">=10" + } + }, + "node_modules/preact-render-to-string/node_modules/pretty-format": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz", + "integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==" + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -23574,6 +23662,19 @@ "@jridgewell/trace-mapping": "^0.3.9" } }, + "@auth/core": { + "version": "0.0.0-manual.e9863699", + "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.0.0-manual.e9863699.tgz", + "integrity": "sha512-/hVzGuFw7nAZimliD8kpuKnNjvkRu+jpaVhYB/FaIXLNJFNwhbO2MgXBnr5tvLIHgRJnR5C9UN5RNpQXiFHuSA==", + "requires": { + "@panva/hkdf": "^1.0.4", + "cookie": "0.5.0", + "jose": "^4.11.1", + "oauth4webapi": "^2.0.6", + "preact": "10.11.3", + "preact-render-to-string": "5.2.3" + } + }, "@aw-web-design/x-default-browser": { "version": "1.4.126", "resolved": "https://registry.npmjs.org/@aw-web-design/x-default-browser/-/x-default-browser-1.4.126.tgz", @@ -26453,6 +26554,11 @@ "fastq": "^1.6.0" } }, + "@panva/hkdf": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.1.1.tgz", + "integrity": "sha512-dhPeilub1NuIG0X5Kvhh9lH4iW3ZsHlnzwgwbOlgwQ2wG1IqFzsgHqmKPk3WzsdWAeaxKJxgM0+W433RmN45GA==" + }, "@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -30583,8 +30689,7 @@ "cookie": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", - "dev": true + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==" }, "cookie-signature": { "version": "1.0.6", @@ -35377,6 +35482,11 @@ "integrity": "sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg==", "dev": true }, + "jose": { + "version": "4.15.4", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.4.tgz", + "integrity": "sha512-W+oqK4H+r5sITxfxpSU+MMdr/YSWGvgZMQDIsNoBDGGy4i7GBPTtvFKibQzW06n3U3TqHjhvBJsirShsEJ6eeQ==" + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -36093,6 +36203,14 @@ "watchpack": "2.4.0" } }, + "next-auth": { + "version": "5.0.0-beta.3", + "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-5.0.0-beta.3.tgz", + "integrity": "sha512-WOKhATBFGeONV+29HzFmspNmL7NXxrsCWLfaDKmAd/4DD1nqXE0BzNFH8t3SJBx7PUDMnB6F7xB76LM/AaV1MQ==", + "requires": { + "@auth/core": "experimental" + } + }, "next-i18next": { "version": "14.0.3", "resolved": "https://registry.npmjs.org/next-i18next/-/next-i18next-14.0.3.tgz", @@ -36354,6 +36472,11 @@ "integrity": "sha512-6xpotnECFy/og7tKSBVmUNft7J3jyXAka4XvG6AUhFWRz+Q/Ljus7znJAA3bxColfQLdS+XsjoodtJfCgeTEFQ==", "dev": true }, + "oauth4webapi": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-2.3.0.tgz", + "integrity": "sha512-JGkb5doGrwzVDuHwgrR4nHJayzN4h59VCed6EW8Tql6iHDfZIabCJvg6wtbn5q6pyB2hZruI3b77Nudvq7NmvA==" + }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -37248,6 +37371,26 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "dev": true }, + "preact": { + "version": "10.11.3", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.11.3.tgz", + "integrity": "sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg==" + }, + "preact-render-to-string": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.3.tgz", + "integrity": "sha512-aPDxUn5o3GhWdtJtW0svRC2SS/l8D9MAgo2+AWml+BhDImb27ALf04Q2d+AHqUUOc6RdSXFIBVa2gxzgMKgtZA==", + "requires": { + "pretty-format": "^3.8.0" + }, + "dependencies": { + "pretty-format": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz", + "integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==" + } + } + }, "prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", diff --git a/app/package.json b/app/package.json index b195dcf7..ecf2e134 100644 --- a/app/package.json +++ b/app/package.json @@ -26,6 +26,7 @@ "@uswds/uswds": "3.1.0", "i18next": "^23.0.0", "next": "^14.0.0", + "next-auth": "^5.0.0-beta.3", "next-i18next": "^14.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/app/src/app/api/auth/[...nextauth]/route.ts b/app/src/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 00000000..ba252504 --- /dev/null +++ b/app/src/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,7 @@ +/** + * @file Behind the scenes, this creates all the relevant OAuth API routes within /api/auth/* + * such as GET /api/auth/signin and POST /api/auth/signin/:provider + * + * @see https://authjs.dev/getting-started/providers/oauth-tutorial + */ +export { GET, POST } from "src/auth"; diff --git a/app/src/app/auth/signout/page.tsx b/app/src/app/auth/signout/page.tsx new file mode 100644 index 00000000..28894036 --- /dev/null +++ b/app/src/app/auth/signout/page.tsx @@ -0,0 +1,17 @@ +"use client"; + +import { federateSignOut } from "src/auth"; + +import { useEffect } from "react"; + +export default function Page() { + /** + * Not super sure why this needs to be called on the client-side, but at the + * moment the Auth.js signOut method has a dependency on `window`, at a minimum. + */ + useEffect(() => { + federateSignOut().catch(console.error); + }, []); + + return
Signing out...
; +} diff --git a/app/src/app/layout.tsx b/app/src/app/layout.tsx new file mode 100644 index 00000000..9682b154 --- /dev/null +++ b/app/src/app/layout.tsx @@ -0,0 +1,15 @@ +export const metadata = { + title: "Next.js template", +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + {children} + + ); +} diff --git a/app/src/app/user/page.tsx b/app/src/app/user/page.tsx new file mode 100644 index 00000000..9b164512 --- /dev/null +++ b/app/src/app/user/page.tsx @@ -0,0 +1,21 @@ +import { auth } from "src/auth"; + +import Link from "next/link"; + +export default async function Page() { + const session = await auth(); + + if (session) { + return ( + <> +

+ Sign out +

+ User ID: {session.sub} + {JSON.stringify(session, null, 2)} + + ); + } + + return Sign in; +} diff --git a/app/src/auth.ts b/app/src/auth.ts new file mode 100644 index 00000000..780a13eb --- /dev/null +++ b/app/src/auth.ts @@ -0,0 +1,102 @@ +/** + * @file Auth configuration and exports for accessing the session, + * API route handlers, and a federated sign out method. + */ +import NextAuth, { NextAuthConfig } from "next-auth"; +import Cognito from "next-auth/providers/cognito"; +import { signOut } from "next-auth/react"; + +type NextAuthCallbacks = NonNullable; + +/** + * The OAuth provider for authentication. + * @see https://authjs.dev/reference/core/providers + */ +const AUTH_PROVIDER = Cognito({ + clientId: process.env.COGNITO_CLIENT_ID, + clientSecret: process.env.COGNITO_CLIENT_SECRET, + issuer: process.env.COGNITO_ISSUER, +}); + +/** + * Not a real page path. It's caught by the redirect callback + * in order to identify when to redirect the user through a + * federated sign out flow. + */ +const FEDERATED_SIGN_OUT_CALLBACK_URL = "/federated-sign-out"; + +/** + * By default, NextAuth only removes the session from the app, + * but this doesn't log the user out from the identity provider. + * Using this method will log the user out from both. See + * the Auth redirect callback for how that works. + */ +export async function federateSignOut() { + await signOut({ + callbackUrl: FEDERATED_SIGN_OUT_CALLBACK_URL, + }); +} + +/** + * Called anytime the user is redirected to a callback URL (e.g. on sign in or sign out). + * The redirect callback may be invoked more than once in the same flow. + * @see https://authjs.dev/guides/basics/callbacks#redirect-callback + */ +const handleRedirect: NextAuthCallbacks["redirect"] = ({ url, baseUrl }) => { + if (url === FEDERATED_SIGN_OUT_CALLBACK_URL) { + const urlParams = new URLSearchParams({ + client_id: process.env.COGNITO_CLIENT_ID as string, + // Where Cognito will redirect the user after signing them out. + // This URL needs safelisted in Cognito as an "Allowed sign-out URL". + // https://docs.aws.amazon.com/cognito/latest/developerguide/logout-endpoint.html + logout_uri: baseUrl, + }).toString(); + + return new URL( + `/logout?${urlParams}`, + process.env.COGNITO_DOMAIN as string + ).toString(); + } + + // Allows relative callback URLs + if (url.startsWith("/")) return `${baseUrl}${url}`; + // Allows callback URLs on the same origin + else if (new URL(url).origin === baseUrl) return url; + return baseUrl; +}; + +/** + * Called whenever a session is checked. + * + * By default, the "sub" (AKA "User ID") isn't included, but you + * can expose it by extending the session here. + * + * By default, the session is persisted as a JWT cookie, so avoid + * storing sensitive information if possible. + */ +const handleSession: NextAuthCallbacks["session"] = ({ token, session }) => { + return { + ...session, + // Any custom properties added below should have a corresponding + // type defined in types/next-auth.d.ts so consuming components + // don't get a type error when referencing them. + sub: token.sub, + }; +}; + +export const { + /** API route handlers */ + handlers: { GET, POST }, + /** Method for accessing a session */ + auth, +} = NextAuth({ + debug: false, + providers: [AUTH_PROVIDER], + callbacks: { + redirect: handleRedirect, + session: handleSession, + }, + pages: { + signOut: "/auth/signout", + }, +}); diff --git a/app/src/middleware.ts b/app/src/middleware.ts new file mode 100644 index 00000000..ad985c2f --- /dev/null +++ b/app/src/middleware.ts @@ -0,0 +1,40 @@ +/** + * @file Middleware allows you to run code before a request is completed. Then, + * based on the incoming request, you can modify the response by rewriting, + * redirecting, modifying the request or response headers, or responding + * directly. Middleware runs before cached content and routes are matched. + * @see https://nextjs.org/docs/app/building-your-application/routing/middleware + */ +import { NextResponse } from "next/server"; + +import { auth } from "./auth"; + +export const config = { + matcher: [ + /* + * Run middleware on all request paths except for the ones starting with: + * - api (API routes) + * - _next/static (static files) + * - _next/image (image optimization files) + * - favicon.ico (favicon file) + */ + "/((?!api|_next/static|_next/image|favicon.ico).*)", + ], +}; + +/** + * Patterns for which routes authentication is enforced on. + */ +const authenticatedPaths: Array = ["/user"]; + +export default auth((req) => { + const session = req.auth; + const path = req.nextUrl.pathname; + const requiresAuth = authenticatedPaths.some((matcher) => + path.match(matcher) + ); + + if (requiresAuth && !session) { + return NextResponse.redirect(new URL("/api/auth/signin", req.url)); + } +}); diff --git a/app/src/types/next-auth.d.ts b/app/src/types/next-auth.d.ts new file mode 100644 index 00000000..890a0e2c --- /dev/null +++ b/app/src/types/next-auth.d.ts @@ -0,0 +1,11 @@ +import { DefaultSession, Profile } from "next-auth/types"; + +/** + * Extend the Session type to include any custom properties that are + * added as part of the session callback in auth.ts. + */ +declare module "next-auth" { + interface Session extends DefaultSession { + sub: Profile["sub"]; + } +} diff --git a/app/tests/middleware.test.ts b/app/tests/middleware.test.ts new file mode 100644 index 00000000..288641e7 --- /dev/null +++ b/app/tests/middleware.test.ts @@ -0,0 +1,5 @@ +describe("middleware", () => { + it.todo( + "redirects to signin page if user isn't authenticated and visits an authenticated page" + ); +}); diff --git a/app/tsconfig.json b/app/tsconfig.json index 0fb8cb02..095a9c63 100644 --- a/app/tsconfig.json +++ b/app/tsconfig.json @@ -18,8 +18,19 @@ "allowJs": false, "checkJs": false, // Help speed up type checking in larger applications - "incremental": true + "incremental": true, + "plugins": [ + { + "name": "next" + } + ] }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "__mocks__/styleMock.js"], + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + "__mocks__/styleMock.js", + ".next/types/**/*.ts" + ], "exclude": ["node_modules"] } diff --git a/template-only-docs/setup-authentication.md b/template-only-docs/setup-authentication.md new file mode 100644 index 00000000..2e4c8fce --- /dev/null +++ b/template-only-docs/setup-authentication.md @@ -0,0 +1,17 @@ +# Authentication + +Authentication support is provided by [Auth.js](https://authjs.dev/). Any OAuth/OIDC service can be used. + +## Setup + +1. Copy the `.env.local.example` file to `.env.local` and fill in the values +1. Configure the authentication provider in `auth.ts`. A provider exists for common services, such as [Cognito](https://authjs.dev/reference/core/providers/cognito), [Azure AD](https://authjs.dev/reference/core/providers/azure-ad), or any [custom provider](https://authjs.dev/guides/providers/custom-provider) that implements the OAuth/OIDC specs, such as Login.gov. + +### Configuration + +Customizing the following features is currently the responsibility of the project: + +- [ ] [Configure the middleware](../app/src/middleware.ts) to enforce auth on pages +- [ ] Setup federated sign out. The `signOut` method provided by Auth.js only removes the session from the application, but the user remains logged into the identity provider. This is likely not what you intend. Some of this is specific to the auth provider your project uses, for instance [Cognito has a specific Logout endpoint](https://docs.aws.amazon.com/cognito/latest/developerguide/logout-endpoint.html) you send a request to. See the `federateSignOut` and `handleRedirect` in [`auth.ts`](../app/src/auth.ts) for a starting point. +- [ ] Recommended: [Setup refresh token rotation](https://authjs.dev/guides/basics/refresh-token-rotation). Some of this is specific to the auth provider your project uses. +- [ ] Optional: [Override the default authentication pages](https://authjs.dev/guides/basics/pages)