diff --git a/src/content/docs/en/guides/authentication.mdx b/src/content/docs/en/guides/authentication.mdx index 371066503a26d..25544d2ac4689 100644 --- a/src/content/docs/en/guides/authentication.mdx +++ b/src/content/docs/en/guides/authentication.mdx @@ -149,24 +149,39 @@ const session = await auth.api.getSession({

{session.user?.name}

``` -You can also use the `auth` object to protect your routes using middleware. The following example checks whether a user trying to access a logged-in dashboard route is authenticated, and redirects them to the home page if not. +You can also use the `auth` object to protect your routes. The following example uses [Astro's advanced routing](/en/guides/routing/#advanced-routing) with [Hono](https://hono.dev/) to require an authenticated session for every route under `/dashboard`, redirecting to the home page otherwise: -```ts title="src/middleware.ts" -import { auth } from "../../../auth"; // import your Better Auth instance -import { defineMiddleware } from "astro:middleware"; - -export const onRequest = defineMiddleware(async (context, next) => { - const isAuthed = await auth.api - .getSession({ - headers: context.request.headers, - }) - if (context.url.pathname === "/dashboard" && !isAuthed) { - return context.redirect("/"); +```ts title="src/fetch.ts" +import { Hono } from "hono"; +import { astro } from "astro/hono"; +import { auth } from "../auth"; // import your Better Auth instance + +const app = new Hono(); + +// Protect every route under /dashboard. +app.use("/dashboard", requireAuth); +app.use("/dashboard/*", requireAuth); + +// Run Astro's built-in pipeline for all other requests. +app.use(astro()); + +export default app; + +async function requireAuth(c, next) { + const session = await auth.api.getSession({ headers: c.req.raw.headers }); + if (!session) { + return c.redirect("/"); } return next(); -}); +} ``` +:::caution +The public pathname a middleware sees is not guaranteed to be the same as the route Astro matches internally: a configured `base`, URL encoding, and duplicate slashes can all cause them to differ. An attacker can exploit that gap to reach a protected route with a pathname your check does not recognize. + +Do not authorize requests by matching `context.url.pathname` against a string (e.g. `context.url.pathname === "/dashboard"` or `context.url.pathname.startsWith("/dashboard")`). Instead, restrict access on a router that matches routes for you. +::: + ### Next Steps - [Better Auth Astro Guide](https://www.better-auth.com/docs/integrations/astro) diff --git a/src/content/docs/en/guides/routing.mdx b/src/content/docs/en/guides/routing.mdx index 7fb54cc769552..df7b8d740762d 100644 --- a/src/content/docs/en/guides/routing.mdx +++ b/src/content/docs/en/guides/routing.mdx @@ -648,9 +648,9 @@ You can do this in two ways: #### Running the full pipeline with `astro()` -Use [`astro()`](/en/reference/modules/astro-fetch/#astro) when you want to keep Astro's built-in routing behavior, but need custom logic around it. This approach preserves the default pipeline order and lets you add pre-processing and post-processing in one place. For many use cases, such as adding auth guards, request logging, and custom headers, `astro()` is all you need. +Use [`astro()`](/en/reference/modules/astro-fetch/#astro) when you want to keep Astro's built-in routing behavior, but need custom logic around it. This approach preserves the default pipeline order and lets you add pre-processing and post-processing in one place. For many use cases, such as request logging and custom headers, `astro()` is all you need. -The following example checks if a user can access a dashboard before running the Astro pipeline, and adds a custom header to the response once Astro has finished running: +The following example logs each incoming request before running the Astro pipeline, and adds a custom header to the response once Astro has finished running: ```ts title="src/fetch.ts" import { FetchState, astro } from 'astro/fetch'; @@ -660,16 +660,7 @@ export default { const state = new FetchState(request); // Custom pre-processing, runs before any Astro handler - const url = new URL(request.url); - if (url.pathname.startsWith('/dashboard')) { - const cookie = request.headers.get('cookie') ?? ''; - if (!cookie.includes('session=')) { - return new Response(null, { - status: 302, - headers: { Location: '/login' }, - }); - } - } + console.log(`${request.method} ${new URL(request.url).pathname}`); const response = await astro(state); @@ -680,6 +671,12 @@ export default { }; ``` +:::caution +The pathname of the incoming request is not guaranteed to be the same as the route Astro matches internally, so a check such as `url.pathname.startsWith('/dashboard')` can be bypassed. + +Do not use pre-processing to authorize requests by matching the request pathname against a string. To guard routes, match them with a router that owns route matching, such as [Hono](#using-with-hono). +::: + #### Composing individual handlers When you need more control over the pipeline execution order, or want to omit certain features, you can compose individual handler functions from [`astro/fetch`](/en/reference/modules/astro-fetch/). Each handler operates on a [`FetchState` object](/en/reference/modules/astro-fetch/#fetchstate) that tracks per-request data, such as the matched route, cookies, and session. You can call handlers in any order and insert custom logic between stages. @@ -733,3 +730,31 @@ app.use(i18n()); export default app; ``` + +You can also guard protected routes by registering an authorization check on the Hono routes you want to protect: + +```ts title="src/fetch.ts" +import { Hono } from 'hono'; +import { actions, middleware, pages, i18n } from 'astro/hono'; +import { isLoggedIn } from './lib/auth'; + +const app = new Hono(); + +// Guard every route under /dashboard. +app.use('/dashboard', requireAuth); +app.use('/dashboard/*', requireAuth); + +app.use(actions()); +app.use(middleware()); +app.use(pages()); +app.use(i18n()); + +export default app; + +async function requireAuth(c, next) { + if (!(await isLoggedIn(c.req.raw))) { + return c.redirect('/login'); + } + return next(); +} +```