+ 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)