}
+
diff --git a/examples/nextjs/pages/middleware-protected/_middleware.ts b/examples/nextjs/pages/middleware-protected/_middleware.ts
new file mode 100644
index 00000000..e8bf7d2f
--- /dev/null
+++ b/examples/nextjs/pages/middleware-protected/_middleware.ts
@@ -0,0 +1,3 @@
+import { withMiddlewareAuthRequired } from '@supabase/supabase-auth-helpers/nextjs';
+
+export const middleware = withMiddlewareAuthRequired();
diff --git a/examples/nextjs/pages/middleware-protected/index.tsx b/examples/nextjs/pages/middleware-protected/index.tsx
new file mode 100644
index 00000000..c3a70b92
--- /dev/null
+++ b/examples/nextjs/pages/middleware-protected/index.tsx
@@ -0,0 +1,5 @@
+import type { NextPage } from 'next';
+
+const Page: NextPage = () =>
Authenticated
;
+
+export default Page;
diff --git a/src/nextjs/README.md b/src/nextjs/README.md
index 562f0cc0..38704d81 100644
--- a/src/nextjs/README.md
+++ b/src/nextjs/README.md
@@ -193,6 +193,52 @@ export const getServerSideProps = withAuthRequired({
});
```
+### Server-side data fetching to OAuth APIs using `provider_token`
+
+When using third-party auth providers, sessions are initiated with an additional `provider_token` field which is persisted as an HTTPOnly cookie upon logging in to enabled usage on the server side. The `provider_token` can be used to make API requests to the OAuth provider's API endpoints on behalf of the logged-in user. In the following example, we fetch the user's full profile from the third-party API during SSR using their id and auth token:
+
+```js
+import {
+ User,
+ withAuthRequired,
+ getUser
+} from '@supabase/supabase-auth-helpers/nextjs';
+
+interface Profile {
+ /* ... */
+}
+
+export default function ProtectedPage({
+ user,
+ data
+}: {
+ user: User,
+ profile: Profile
+}) {
+ return
Protected content
;
+}
+
+export const getServerSideProps = withAuthRequired({
+ redirectTo: '/',
+ async getServerSideProps(ctx) {
+ // Retrieve provider_token from cookies
+ const provider_token = ctx.req.cookies['sb-provider-token'];
+ // Get logged in user's third-party id from metadata
+ const { user } = await getUser(ctx);
+ const userId = user?.user_metadata.provider_id;
+ const profile: Profile = await (
+ await fetch(`https://api.example.com/users/${userId}`, {
+ method: 'GET',
+ headers: {
+ Authorization: `Bearer ${provider_token}`
+ }
+ })
+ ).json();
+ return { props: { profile } };
+ }
+});
+```
+
## Protecting API routes
Wrap an API Route to check that the user has a valid session. If they're not logged in the handler will return a
@@ -215,3 +261,14 @@ export default withAuthRequired(async function ProtectedRoute(req, res) {
```
If you visit `/api/protected-route` without a valid session cookie, you will get a 401 response.
+
+## Protecting routes with [Nextjs Middleware](https://nextjs.org/docs/middleware)
+
+As an alternative to protecting individual routes using `getServerSideProps` with `withAuthRequired`, `withMiddlewareAuthRequired` can be used from inside a `_middleware` file to protect an entire directory. In the following example, all requests to `/protected/*` will check whether a user is signed in, if successful the request will be forwarded to the destination route, otherwise the user will be redirected to `/login` (defaults to: `/`) with a 307 Temporary Redirect response status:
+
+```ts
+// pages/protected/_middleware.ts
+import { withMiddlewareAuthRequired } from '@supabase/supabase-auth-helpers/nextjs';
+
+export const middleware = withMiddlewareAuthRequired({ redirectTo: '/login' });
+```
diff --git a/src/nextjs/handlers/callback.ts b/src/nextjs/handlers/callback.ts
index 13cc9ee1..5fc1803a 100644
--- a/src/nextjs/handlers/callback.ts
+++ b/src/nextjs/handlers/callback.ts
@@ -11,6 +11,8 @@ export interface HandleCallbackOptions {
cookieOptions?: CookieOptions;
}
+type AuthCookies = Parameters[2];
+
export default function handelCallback(
req: NextApiRequest,
res: NextApiResponse,
@@ -31,22 +33,30 @@ export default function handelCallback(
new NextResponseAdapter(res),
[
{ key: 'access-token', value: session.access_token },
- { key: 'refresh-token', value: session.refresh_token }
- ].map((token) => ({
- name: `${cookieOptions.name}-${token.key}`,
- value: token.value,
- domain: cookieOptions.domain,
- maxAge: cookieOptions.lifetime ?? 0,
- path: cookieOptions.path,
- sameSite: cookieOptions.sameSite
- }))
+ { key: 'refresh-token', value: session.refresh_token },
+ session.provider_token
+ ? { key: 'provider-token', value: session.provider_token }
+ : null
+ ].reduce((acc, token) => {
+ if (token) {
+ acc.push({
+ name: `${cookieOptions.name}-${token.key}`,
+ value: token.value,
+ domain: cookieOptions.domain,
+ maxAge: cookieOptions.lifetime ?? 0,
+ path: cookieOptions.path,
+ sameSite: cookieOptions.sameSite
+ });
+ }
+ return acc;
+ }, [])
);
}
if (event === 'SIGNED_OUT') {
setCookies(
new NextRequestAdapter(req),
new NextResponseAdapter(res),
- ['access-token', 'refresh-token'].map((key) => ({
+ ['access-token', 'refresh-token', 'provider-token'].map((key) => ({
name: `${cookieOptions.name}-${key}`,
value: '',
maxAge: -1
diff --git a/src/nextjs/handlers/logout.ts b/src/nextjs/handlers/logout.ts
index b20761cb..d039aae8 100644
--- a/src/nextjs/handlers/logout.ts
+++ b/src/nextjs/handlers/logout.ts
@@ -32,7 +32,7 @@ export default function handleLogout(
setCookies(
new NextRequestAdapter(req),
new NextResponseAdapter(res),
- ['access-token', 'refresh-token'].map((key) => ({
+ ['access-token', 'refresh-token', 'provider-token'].map((key) => ({
name: `${cookieOptions.name}-${key}`,
value: '',
maxAge: -1
diff --git a/src/nextjs/index.ts b/src/nextjs/index.ts
index 9d5c024c..e418462d 100644
--- a/src/nextjs/index.ts
+++ b/src/nextjs/index.ts
@@ -3,6 +3,7 @@ export { User } from '@supabase/supabase-js';
// Methods
export * from './handlers';
+export { withMiddlewareAuthRequired } from './utils/withMiddlewareAuthRequired';
export { default as getUser } from './utils/getUser';
export { default as withAuthRequired } from './utils/withAuthRequired';
export { default as supabaseServerClient } from './utils/supabaseServerClient';
diff --git a/src/nextjs/utils/withMiddlewareAuthRequired.ts b/src/nextjs/utils/withMiddlewareAuthRequired.ts
new file mode 100644
index 00000000..3b84fb7f
--- /dev/null
+++ b/src/nextjs/utils/withMiddlewareAuthRequired.ts
@@ -0,0 +1,118 @@
+// import { NextResponse } from 'next/server'; TODO fix import
+import { NextResponse } from 'next/dist/server/web/spec-extension/response';
+import { NextMiddleware } from 'next/server';
+import { User, ApiError, createClient } from '@supabase/supabase-js';
+import { CookieOptions } from '../types';
+import { COOKIE_OPTIONS } from '../../shared/utils/constants';
+import { jwtDecoder } from '../../shared/utils/jwt';
+import { setCookies } from '../../shared/utils/cookies';
+import {
+ NextRequestAdapter,
+ NextResponseAdapter
+} from '../../shared/adapters/NextMiddlewareAdapter';
+
+export type WithMiddlewareAuthRequired = (options?: {
+ /**
+ * Path relative to the site root to redirect an
+ * unauthenticated visitor.
+ *
+ * The original request route will be appended via
+ * a `redirectedFrom` query parameter, ex: `?redirectedFrom=%2Fdashboard`
+ */
+ redirectTo?: string;
+ cookieOptions?: CookieOptions;
+}) => NextMiddleware;
+
+export const withMiddlewareAuthRequired: WithMiddlewareAuthRequired =
+ (options: { redirectTo?: string; cookieOptions?: CookieOptions } = {}) =>
+ async (req) => {
+ try {
+ if (
+ !process.env.NEXT_PUBLIC_SUPABASE_URL ||
+ !process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
+ ) {
+ throw new Error(
+ 'NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY env variables are required!'
+ );
+ }
+ if (!req.cookies) {
+ throw new Error('Not able to parse cookies!');
+ }
+ const supabase = createClient(
+ process.env.NEXT_PUBLIC_SUPABASE_URL,
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
+ { fetch }
+ );
+ const cookieOptions = { ...COOKIE_OPTIONS, ...options.cookieOptions };
+ const access_token = req.cookies[`${cookieOptions.name!}-access-token`];
+ const refresh_token = req.cookies[`${cookieOptions.name!}-refresh-token`];
+
+ const res = NextResponse.next();
+
+ const getUser = async (): Promise<{
+ user: User | null;
+ error: ApiError | null;
+ }> => {
+ if (!access_token) {
+ throw new Error('No cookie found!');
+ }
+ // Get payload from access token.
+ const jwtUser = jwtDecoder(access_token);
+ if (!jwtUser?.exp) {
+ throw new Error('Not able to parse JWT payload!');
+ }
+ const timeNow = Math.round(Date.now() / 1000);
+ if (jwtUser.exp < timeNow) {
+ if (!refresh_token) {
+ throw new Error('No refresh_token cookie found!');
+ }
+ const { data, error } = await supabase.auth.api.refreshAccessToken(
+ refresh_token
+ );
+ setCookies(
+ new NextRequestAdapter(req),
+ new NextResponseAdapter(res),
+ [
+ { key: 'access-token', value: data!.access_token },
+ { key: 'refresh-token', value: data!.refresh_token! }
+ ].map((token) => ({
+ name: `${cookieOptions.name}-${token.key}`,
+ value: token.value,
+ domain: cookieOptions.domain,
+ maxAge: cookieOptions.lifetime ?? 0,
+ path: cookieOptions.path,
+ sameSite: cookieOptions.sameSite
+ }))
+ );
+ return { user: data?.user ?? null, error };
+ }
+ return { user: jwtUser, error: null };
+ };
+
+ const authResult = await getUser();
+
+ if (authResult.error) {
+ throw new Error(
+ `Authorization error, redirecting to login page: ${authResult.error.message}`
+ );
+ } else if (!authResult.user) {
+ throw new Error('No auth user, redirecting');
+ }
+
+ // Authentication successful, forward request to protected route
+ return res;
+ } catch (err: unknown) {
+ const { redirectTo = '/' } = options;
+ if (err instanceof Error) {
+ console.log(
+ `Could not authenticate request, redirecting to ${redirectTo}:`,
+ err
+ );
+ }
+ const redirectUrl = req.nextUrl.clone();
+ redirectUrl.pathname = redirectTo;
+ redirectUrl.searchParams.set(`redirectedFrom`, req.nextUrl.pathname);
+ // Authentication failed, redirect request
+ return NextResponse.redirect(redirectUrl);
+ }
+ };
diff --git a/src/shared/adapters/NextMiddlewareAdapter.ts b/src/shared/adapters/NextMiddlewareAdapter.ts
new file mode 100644
index 00000000..258600f2
--- /dev/null
+++ b/src/shared/adapters/NextMiddlewareAdapter.ts
@@ -0,0 +1,34 @@
+import { NextRequest, NextResponse } from 'next/server';
+
+import { RequestAdapter, ResponseAdapter } from './types';
+
+export class NextRequestAdapter implements RequestAdapter {
+ private req: NextRequest;
+ constructor(request: NextRequest) {
+ this.req = request;
+ }
+
+ setRequestCookie(name: string, value: string) {
+ this.req.cookies[name] = value;
+ }
+
+ getHeader(name: string) {
+ return this.req.headers.get(name);
+ }
+}
+
+export class NextResponseAdapter implements ResponseAdapter {
+ private res: NextResponse;
+ constructor(response: NextResponse) {
+ this.res = response;
+ }
+
+ getHeader(name: string) {
+ return this.res.headers.get(name);
+ }
+
+ setHeader(name: string, value: string) {
+ this.res.headers.set(name, value);
+ return this;
+ }
+}