Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 28 additions & 13 deletions src/content/docs/en/guides/authentication.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -149,24 +149,39 @@ const session = await auth.api.getSession({
<p>{session.user?.name}</p>
```

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)
Expand Down
49 changes: 37 additions & 12 deletions src/content/docs/en/guides/routing.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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);

Expand All @@ -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.
Expand Down Expand Up @@ -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();
}
```
Loading