From 5064f20e6174a91fc2b96c5960aeb3b635a793ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Mon, 2 Jan 2023 11:15:42 +0100 Subject: [PATCH 1/3] feat(core): introduce `AuthRequest` and `AuthResponse` web extensions --- .prettierrc.js | 1 + packages/core/src/index.ts | 63 +++++++++-------- packages/core/src/lib/web-extension.ts | 95 ++++++++++++++++++++++++++ packages/core/src/lib/web.ts | 6 +- 4 files changed, 132 insertions(+), 33 deletions(-) create mode 100644 packages/core/src/lib/web-extension.ts diff --git a/.prettierrc.js b/.prettierrc.js index 44f0e9a0b1..dc0bd2a946 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -9,6 +9,7 @@ module.exports = { files: [ "apps/dev/pages/api/auth/[...nextauth].ts", "docs/{sidebars,docusaurus.config}.js", + "packages/core/src/index.ts", ], options: { printWidth: 150 }, }, diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 27c5709dcf..10ac9f6375 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -40,17 +40,17 @@ import { logger, setLogger, type LoggerInstance } from "./lib/utils/logger.js" import { toInternalRequest, toResponse } from "./lib/web.js" import type { Adapter } from "./adapters.js" -import type { - CallbacksOptions, - CookiesOptions, - EventCallbacks, - PagesOptions, - SessionOptions, - Theme, -} from "./types.js" +import type { CallbacksOptions, CookiesOptions, EventCallbacks, PagesOptions, SessionOptions, Theme } from "./types.js" import type { Provider } from "./providers/index.js" import { JWTOptions } from "./jwt.js" +import { ProvidersRequest, ProvidersResponse, SessionRequest, SessionResponse } from "./lib/web-extension.js" +export * from "./lib/web-extension.js" + +/** Returns a special {@link SessionResponse} instance to read the session from the request. */ +export function Auth(request: SessionRequest, config: AuthConfig): Promise +/** Returns a special {@link ProvidersResponse} instance to read the list of providers in a client-safe way. */ +export function Auth(request: ProvidersRequest, config: AuthConfig): Promise /** * Core functionality provided by Auth.js. * @@ -69,19 +69,18 @@ import { JWTOptions } from "./jwt.js" *``` * @see [Documentation](https://authjs.dev) */ -export async function Auth( - request: Request, - config: AuthConfig -): Promise { +export function Auth(request: Request, config: AuthConfig): Promise +export async function Auth(request: Request, config: AuthConfig): Promise { setLogger(config.logger, config.debug) + const isAuthRequest = request instanceof SessionRequest || request instanceof ProvidersRequest + + if (isAuthRequest) config.trustHost = true + const internalRequest = await toInternalRequest(request) if (internalRequest instanceof Error) { logger.error(internalRequest) - return new Response( - `Error: This action with HTTP ${request.method} is not supported.`, - { status: 400 } - ) + return new Response(`Error: This action with HTTP ${request.method} is not supported.`, { status: 400 }) } const assertionResult = assertConfig(internalRequest, config) @@ -92,14 +91,10 @@ export async function Auth( // Bail out early if there's an error in the user config logger.error(assertionResult) const htmlPages = ["signin", "signout", "error", "verify-request"] - if ( - !htmlPages.includes(internalRequest.action) || - internalRequest.method !== "GET" - ) { + if (!htmlPages.includes(internalRequest.action) || internalRequest.method !== "GET") { return new Response( JSON.stringify({ - message: - "There was a problem with the server configuration. Check the server logs for more information.", + message: "There was a problem with the server configuration. Check the server logs for more information.", code: assertionResult.name, }), { status: 500, headers: { "Content-Type": "application/json" } } @@ -108,19 +103,11 @@ export async function Auth( const { pages, theme } = config - const authOnErrorPage = - pages?.error && - internalRequest.url.searchParams - .get("callbackUrl") - ?.startsWith(pages.error) + const authOnErrorPage = pages?.error && internalRequest.url.searchParams.get("callbackUrl")?.startsWith(pages.error) if (!pages?.error || authOnErrorPage) { if (authOnErrorPage) { - logger.error( - new ErrorPageLoop( - `The error page ${pages?.error} should not require authentication` - ) - ) + logger.error(new ErrorPageLoop(`The error page ${pages?.error} should not require authentication`)) } const render = renderPage({ theme }) const page = render.error({ error: "Configuration" }) @@ -144,6 +131,18 @@ export async function Auth( headers: response.headers, }) } + + if (isAuthRequest) { + switch (request.action) { + case "session": + return new SessionResponse(response.body, response) + case "providers": + return new ProvidersResponse(response.body, response) + default: + return response + } + } + return response } diff --git a/packages/core/src/lib/web-extension.ts b/packages/core/src/lib/web-extension.ts new file mode 100644 index 0000000000..738e417d9b --- /dev/null +++ b/packages/core/src/lib/web-extension.ts @@ -0,0 +1,95 @@ +import type { AuthAction, Session } from "../types.js" +import { PublicProvider } from "./routes/providers.js" + +/** @internal */ +export abstract class AuthRequest extends Request { + abstract action: AuthAction +} + +/** + * Extends the standard {@link Request} to add a `session()` method on the response + * for retrieving the {@link Session} object. + */ +export class SessionRequest extends AuthRequest { + action = "session" as const + constructor(req: Request) { + super(req.url, req) + } +} + +export class SessionResponse extends Response { + action = "session" as const + + /** + * Returns the {@link Session} object from the response, or `null` + * if the session is unavailable (config error, not authenticated, etc.). + * + * @example + * ```ts + * export default async function handle(req: Request) { + * const response = await Auth(new SessionRequest(req), authConfig) + * const session = await response.session() + * + * if (!session) { + * return new Response("Not authenticated", { status: 401 }) + * } + * + * console.log(session.user) // Do something with the session + * return response // or return whatever you want. + * } + * ``` + */ + async session(): Promise { + try { + const data = await this.clone().json() + if (!this.ok || !data || !Object.keys(data).length) { + return null + } + return data + } catch { + return null + } + } +} + +/** + * Extends the standard {@link Request} to add a `providers()` method on the response + * for retrieving a list of client-safe provider configuration. Useful for + * rendering a list of sign-in options. + */ +export class ProvidersRequest extends AuthRequest { + action = "providers" as const + constructor(req: Request) { + super(req.url, req) + } +} + +export class ProvidersResponse extends Response { + action = "providers" as const + + /** + * Returns the list of providers from the response, or `null` + * if the providers are unavailable (config error, etc.). + * @example + * ```ts + * export default async function handle(req: Request) { + * const response = await Auth(new ProvidersRequest(req), authConfig) + * const providers = await response.providers() + * if (!providers) { + * return new Response("Providers unavailable", { status: 500 }) + * + * + * console.log(providers) // Do something with the providers + * return response // or return whatever you want. + * } + * ``` + */ + async providers(): Promise { + try { + if (!this.ok) return [] + return Object.values(await this.clone().json()) + } catch { + return [] + } + } +} diff --git a/packages/core/src/lib/web.ts b/packages/core/src/lib/web.ts index 4c3395bf57..f679d0e552 100644 --- a/packages/core/src/lib/web.ts +++ b/packages/core/src/lib/web.ts @@ -2,6 +2,7 @@ import { parse as parseCookie, serialize } from "cookie" import { AuthError, UnknownAction } from "../errors.js" import type { AuthAction, RequestInternal, ResponseInternal } from "../types.js" +import { ProvidersRequest, SessionRequest } from "./web-extension.js" async function getBody(req: Request): Promise | undefined> { if (!("body" in req) || !req.body || req.method !== "POST") return @@ -34,8 +35,11 @@ export async function toInternalRequest( // see init.ts const url = new URL(req.url.replace(/\/$/, "")) const { pathname } = url + let action: AuthAction | undefined + if (req instanceof SessionRequest || req instanceof ProvidersRequest) { + action = req.action + } else action = actions.find((a) => pathname.includes(a)) - const action = actions.find((a) => pathname.includes(a)) if (!action) { throw new UnknownAction("Cannot detect action.") } From cb2da4dd9c0cc94f1f62f6be9f5e4fc8ee5825df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Mon, 2 Jan 2023 11:15:58 +0100 Subject: [PATCH 2/3] chore(dev): update session example endpoint --- apps/dev/nextjs/pages/api/examples/session.js | 8 ------- apps/dev/nextjs/pages/api/examples/session.ts | 21 +++++++++++++++++++ 2 files changed, 21 insertions(+), 8 deletions(-) delete mode 100644 apps/dev/nextjs/pages/api/examples/session.js create mode 100644 apps/dev/nextjs/pages/api/examples/session.ts diff --git a/apps/dev/nextjs/pages/api/examples/session.js b/apps/dev/nextjs/pages/api/examples/session.js deleted file mode 100644 index d92d1afe20..0000000000 --- a/apps/dev/nextjs/pages/api/examples/session.js +++ /dev/null @@ -1,8 +0,0 @@ -// This is an example of how to access a session from an API route -import { unstable_getServerSession } from "next-auth/next" -import { authOptions } from "../auth/[...nextauth]" - -export default async (req, res) => { - const session = await unstable_getServerSession(req, res, authOptions) - res.json(session) -} diff --git a/apps/dev/nextjs/pages/api/examples/session.ts b/apps/dev/nextjs/pages/api/examples/session.ts new file mode 100644 index 0000000000..5b922dd56a --- /dev/null +++ b/apps/dev/nextjs/pages/api/examples/session.ts @@ -0,0 +1,21 @@ +import { Auth, SessionRequest } from "@auth/core" +import { authConfig } from "../auth/[...nextauth]" + +export default async function handle(req: Request) { + authConfig.secret = process.env.AUTH_SECRET + + const response = await Auth(new SessionRequest(req), authConfig) + const session = await response.session() + if (!session) { + return new Response("Not authenticated", { status: 401 }) + } + + console.log(session.user) // Do something with the session + // Pass the original headers to set cookies (eg.: updating the session expiry) + response.headers.set("content-type", "text/plain") + return new Response("Authenticated", { headers: response.headers }) +} + +export const config = { + runtime: "experimental-edge", +} From fd80f10625ffe71496c71505f815ce5a6c62fd4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Mon, 2 Jan 2023 11:16:11 +0100 Subject: [PATCH 3/3] docs: don't hide classes header --- docs/src/css/index.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/css/index.css b/docs/src/css/index.css index f5f096a45f..aed88512bf 100644 --- a/docs/src/css/index.css +++ b/docs/src/css/index.css @@ -293,6 +293,6 @@ html[data-theme="dark"] #carbonads .carbon-poweredby { See: https://github.com/TypeStrong/typedoc/issues/2006 */ /* h3.anchor + p:has(code, strong), */ /** hack did not work as it hides property types elsewhere */ -#classes { +/* #classes { display: none; -} +} */