From 00a2df442ddb9b2f5dc27b3ad423a86bd3598aa9 Mon Sep 17 00:00:00 2001 From: thorwebdev Date: Fri, 4 Nov 2022 11:55:41 +0800 Subject: [PATCH] chore: update nextjs auth helpers docs v0.5.0 --- .../docs/guides/auth/auth-helpers/nextjs.mdx | 671 ++++++++++++++---- 1 file changed, 537 insertions(+), 134 deletions(-) diff --git a/apps/docs/docs/guides/auth/auth-helpers/nextjs.mdx b/apps/docs/docs/guides/auth/auth-helpers/nextjs.mdx index 569770782e6..2f053040956 100644 --- a/apps/docs/docs/guides/auth/auth-helpers/nextjs.mdx +++ b/apps/docs/docs/guides/auth/auth-helpers/nextjs.mdx @@ -13,12 +13,14 @@ This submodule provides convenience helpers for implementing user authentication ## Install the Next.js helper library - + groupId="install" + defaultValue="npm" + values={[ + {label: 'npm', value: 'npm'}, + {label: 'Yarn', value: 'yarn'}, + ]}> + + ```sh npm install @supabase/auth-helpers-nextjs @@ -28,6 +30,7 @@ This library supports the following tooling versions: - Node.js: `^10.13.0 || >=12.0.0` - Next.js: `>=10` +- Note: Next.js 13 is supported except for the new `app` directory approach. We're working on adding support for this and you can follow along [here](https://github.com/supabase/auth-helpers/issues/341). Additionally, install the **React Auth Helpers** for components and hooks that can be used across all React-based frameworks. @@ -35,8 +38,8 @@ Additionally, install the **React Auth Helpers** for components and hooks that c npm install @supabase/auth-helpers-react ``` - - + + ```sh yarn add @supabase/auth-helpers-nextjs @@ -46,6 +49,7 @@ This library supports the following tooling versions: - Node.js: `^10.13.0 || >=12.0.0` - Next.js: `>=10` +- Note: Next.js 13 is supported except for the new `app` directory approach. We're working on adding support for this and you can follow along [here](https://github.com/supabase/auth-helpers/issues/341). Additionally, install the **React Auth Helpers** for components and hooks that can be used across all React-based frameworks. @@ -53,7 +57,7 @@ Additionally, install the **React Auth Helpers** for components and hooks that c yarn add @supabase/auth-helpers-react ``` - + ## Set up environment variables @@ -68,12 +72,13 @@ NEXT_PUBLIC_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY ## Basic Setup - + groupId="language" + defaultValue="js" + values={[ + {label: 'JavaScript', value: 'js'}, + {label: 'TypeScript', value: 'ts'}, + ]}> + Wrap your `pages/_app.js` component with the `SessionContextProvider` component: @@ -97,12 +102,12 @@ function MyApp({ Component, pageProps }) { } ``` - - + + Wrap your `pages/_app.tsx` component with the `SessionContextProvider` component: -```jsx title="pages/_app.tsx" +```tsx title="pages/_app.tsx" import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs' // highlight-next-line import { SessionContextProvider, Session } from '@supabase/auth-helpers-react' @@ -112,7 +117,7 @@ function MyApp({ pageProps, }: AppProps<{ // highlight-next-line - initialSession: Session, + initialSession: Session }>) { // Create a new supabase browser client on every first render. const [supabaseClient] = useState(() => createBrowserSupabaseClient()) @@ -128,7 +133,7 @@ function MyApp({ } ``` - + You can now determine if a user is authenticated by checking that the `user` object returned by the `useUser()` hook is defined. @@ -139,16 +144,18 @@ You can pass types that were [generated with the Supabase CLI](/docs/reference/j ### Browser client -```ts -// Creating a new supabase client object: +Creating a new supabase client object: + +```tsx import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs' import { Database } from '../database.types' const supabaseClient = createBrowserSupabaseClient() ``` -```ts -// Retrieving a supabase client object from the SessionContext: +Retrieving a supabase client object from the SessionContext: + +```tsx import { useSupabaseClient } from '@supabase/auth-helpers-react' import { Database } from '../database.types' @@ -157,7 +164,7 @@ const supabaseClient = useSupabaseClient() ### Server client -```ts +```tsx // Creating a new supabase server client object (e.g. in API route): import type { NextApiRequest, NextApiResponse } from 'next' import type { Database } from 'types_db' @@ -179,7 +186,7 @@ export default async (req: NextApiRequest, res: NextApiResponse) => { For [row level security](https://supabase.com/docs/learn/auth-deep-dive/auth-row-level-security) to work properly when fetching data client-side, you need to make sure to use the `supabaseClient` from the `useSupabaseClient` hook and only run your query once the user is defined client-side in the `useUser()` hook: -```js +```jsx import { Auth, ThemeSupa } from '@supabase/auth-ui-react' import { useUser, useSupabaseClient } from '@supabase/auth-helpers-react' import { useEffect, useState } from 'react' @@ -225,52 +232,59 @@ const LoginPage = () => { export default LoginPage ``` -## Server-side rendering (SSR) - withPageAuth +## Server-side rendering (SSR) -If you wrap your `getServerSideProps` with `withPageAuth` your props object will be augmented with the user object. +Create a server supabase client to retrieve the logged in user's session: -```js title="pages/profile.js" -import { withPageAuth } from '@supabase/auth-helpers-nextjs' +```jsx title="pages/profile.js" +import { createServerSupabaseClient } from '@supabase/auth-helpers-nextjs' export default function Profile({ user }) { return
Hello {user.name}
} -export const getServerSideProps = withPageAuth({ redirectTo: '/login' }) -``` +export const getServerSideProps = async (ctx) => { + // Create authenticated Supabase Client + const supabase = createServerSupabaseClient(ctx) + // Check if we have a session + const { + data: { session }, + } = await supabase.auth.getSession() -If there is no authenticated user, they will be redirect to your home page, unless you specify the `redirectTo` option. + if (!session) + return { + redirect: { + destination: '/', + permanent: false, + }, + } -You can pass in your own `getServerSideProps` method, the props returned from this will be merged with the -user props. You can also access the user session data by calling `supabase.auth.getUser()` inside of this method, eg: - -```js title="pages/protected-page.js" -import { withPageAuth } from '@supabase/auth-helpers-nextjs' - -export default function ProtectedPage({ user, customProp }) { - return
Protected content
+ return { + props: { + initialSession: session, + user: session.user, + }, + } } - -export const getServerSideProps = withPageAuth({ - redirectTo: '/foo', - async getServerSideProps(ctx, supabase) { - // Access the user object - const { - data: { user }, - } = await supabase.auth.getUser() - return { props: { email: user?.email } } - }, -}) ``` ## Server-side data fetching with RLS -Both `withApiAuth` and `withPageAuth` return a supabase client that you can use to run [row level security](https://supabase.com/docs/learn/auth-deep-dive/auth-row-level-security) authenticated queries server-side: +You can use the server supabase client to run [row level security](https://supabase.com/docs/learn/auth-deep-dive/auth-row-level-security) authenticated queries server-side: -```js -import { User, withPageAuth } from '@supabase/auth-helpers-nextjs' + + -export default function ProtectedPage({ user, data }: { user: User, data: any }) { +```jsx +import { createServerSupabaseClient } from '@supabase/auth-helpers-nextjs' + +export default function ProtectedPage({ user, data }) { return ( <>
Protected content for {user.email}
@@ -280,24 +294,101 @@ export default function ProtectedPage({ user, data }: { user: User, data: any }) ) } -export const getServerSideProps = withPageAuth({ - redirectTo: '/', - async getServerSideProps(ctx, supabase) { - // Run queries with RLS on the server - const { data } = await supabase.from('test').select('*') - return { props: { data } } - }, -}) +export const getServerSideProps = async (ctx) => { + // Create authenticated Supabase Client + const supabase = createServerSupabaseClient(ctx) + // Check if we have a session + const { + data: { session }, + } = await supabase.auth.getSession() + + if (!session) + return { + redirect: { + destination: '/', + permanent: false, + }, + } + + // Run queries with RLS on the server + const { data } = await supabase.from('users').select('*') + + return { + props: { + initialSession: session, + user: session.user, + data: data ?? [], + }, + } +} ``` +
+ + +```tsx +import { User, createServerSupabaseClient } from '@supabase/auth-helpers-nextjs' +import { GetServerSidePropsContext } from 'next' + +export default function ProtectedPage({ user, data }: { user: User; data: any }) { + return ( + <> +
Protected content for {user.email}
+
{JSON.stringify(data, null, 2)}
+
{JSON.stringify(user, null, 2)}
+ + ) +} + +export const getServerSideProps = async (ctx: GetServerSidePropsContext) => { + // Create authenticated Supabase Client + const supabase = createServerSupabaseClient(ctx) + // Check if we have a session + const { + data: { session }, + } = await supabase.auth.getSession() + + if (!session) + return { + redirect: { + destination: '/', + permanent: false, + }, + } + + // Run queries with RLS on the server + const { data } = await supabase.from('users').select('*') + + return { + props: { + initialSession: session, + user: session.user, + data: data ?? [], + }, + } +} +``` + +
+
+ ## 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 in the auth cookie and can be accessed within the session object. The `provider_token` can be used to make API requests to the OAuth provider's API endpoints on behalf of the logged-in user. -```js -import { User, withPageAuth } from '@supabase/auth-helpers-nextjs' + + -export default function ProtectedPage({ user, allRepos }: { user: User, allRepos: any }) { +```jsx +import { createServerSupabaseClient } from '@supabase/auth-helpers-nextjs' + +export default function ProtectedPage({ user, allRepos }) { return ( <>
Protected content for {user.email}
@@ -309,44 +400,224 @@ export default function ProtectedPage({ user, allRepos }: { user: User, allRepos ) } -export const getServerSideProps = withPageAuth({ - redirectTo: '/', - async getServerSideProps(ctx, supabase) { - const { - data: { session }, - error, - } = await supabase.auth.getSession() - if (error) { - throw error - } - if (!session) { - return { props: {} } +export const getServerSideProps = async (ctx) => { + // Create authenticated Supabase Client + const supabase = createServerSupabaseClient(ctx) + // Check if we have a session + const { + data: { session }, + } = await supabase.auth.getSession() + + if (!session) + return { + redirect: { + destination: '/', + permanent: false, + }, } - // Retrieve provider_token & logged in user's third-party id from metadata - const { provider_token, user } = session - const userId = user.user_metadata.user_name + // Retrieve provider_token & logged in user's third-party id from metadata + const { provider_token, user } = session + const userId = user.user_metadata.user_name - const allRepos = await ( - await fetch(`https://api.github.com/search/repositories?q=user:${userId}`, { - method: 'GET', - headers: { - Authorization: `token ${provider_token}`, - }, - }) - ).json() + const allRepos = await ( + await fetch(`https://api.github.com/search/repositories?q=user:${userId}`, { + method: 'GET', + headers: { + Authorization: `token ${provider_token}`, + }, + }) + ).json() - return { props: { allRepos, user } } - }, -}) + return { props: { user, allRepos } } +} ``` +
+ + +```tsx +import { User, createServerSupabaseClient } from '@supabase/auth-helpers-nextjs' +import { GetServerSidePropsContext } from 'next' + +export default function ProtectedPage({ user, allRepos }: { user: User; allRepos: any }) { + return ( + <> +
Protected content for {user.email}
+

Data fetched with provider token:

+
{JSON.stringify(allRepos, null, 2)}
+

user:

+
{JSON.stringify(user, null, 2)}
+ + ) +} + +export const getServerSideProps = async (ctx: GetServerSidePropsContext) => { + // Create authenticated Supabase Client + const supabase = createServerSupabaseClient(ctx) + // Check if we have a session + const { + data: { session }, + } = await supabase.auth.getSession() + + if (!session) + return { + redirect: { + destination: '/', + permanent: false, + }, + } + + // Retrieve provider_token & logged in user's third-party id from metadata + const { provider_token, user } = session + const userId = user.user_metadata.user_name + + const allRepos = await ( + await fetch(`https://api.github.com/search/repositories?q=user:${userId}`, { + method: 'GET', + headers: { + Authorization: `token ${provider_token}`, + }, + }) + ).json() + + return { props: { user, allRepos } } +} +``` + +
+
+ ## 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 -401 Unauthorized. +Create a server supabase client to retrieve the logged in user's session: -```js title="pages/api/protected-route.js" + + + +```jsx title="pages/api/protected-route.js" +import { createServerSupabaseClient } from '@supabase/auth-helpers-nextjs' + +const ProtectedRoute = async (req, res) => { + // Create authenticated Supabase Client + const supabase = createServerSupabaseClient({ req, res }) + // Check if we have a session + const { + data: { session }, + } = await supabase.auth.getSession() + + if (!session) + return res.status(401).json({ + error: 'not_authenticated', + description: 'The user does not have an active session or is not authenticated', + }) + + // Run queries with RLS on the server + const { data } = await supabase.from('test').select('*') + res.json(data) +} + +export default ProtectedRoute +``` + + + + +```tsx title="pages/api/protected-route.ts" +import { NextApiHandler } from 'next' +import { createServerSupabaseClient } from '@supabase/auth-helpers-nextjs' + +const ProtectedRoute: NextApiHandler = async (req, res) => { + // Create authenticated Supabase Client + const supabase = createServerSupabaseClient({ req, res }) + // Check if we have a session + const { + data: { session }, + } = await supabase.auth.getSession() + + if (!session) + return res.status(401).json({ + error: 'not_authenticated', + description: 'The user does not have an active session or is not authenticated', + }) + + // Run queries with RLS on the server + const { data } = await supabase.from('test').select('*') + res.json(data) +} + +export default ProtectedRoute +``` + + + + +## Protecting routes with [Nextjs Middleware](https://nextjs.org/docs/middleware) + +As an alternative to protecting individual pages you can use a `middleware` file to protect the entire directory or those that match the config object. In the following example, all requests to `/middleware-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: + +```ts title="middleware.ts" +import { createMiddlewareSupabaseClient } from '@supabase/auth-helpers-nextjs' +import { NextResponse } from 'next/server' +import type { NextRequest } from 'next/server' + +export async function middleware(req: NextRequest) { + // We need to create a response and hand it to the supabase client to be able to modify the response headers. + const res = NextResponse.next() + // Create authenticated Supabase Client. + const supabase = createMiddlewareSupabaseClient({ req, res }) + // Check if we have a session + const { + data: { session }, + } = await supabase.auth.getSession() + + // Check auth condition + if (session?.user.email?.endsWith('@gmail.com')) { + // Authentication successful, forward request to protected route. + return res + } + + // Auth condition not met, redirect to home page. + const redirectUrl = req.nextUrl.clone() + redirectUrl.pathname = '/' + redirectUrl.searchParams.set(`redirectedFrom`, req.nextUrl.pathname) + return NextResponse.redirect(redirectUrl) +} + +export const config = { + matcher: '/middleware-protected', +} +``` + +## Migration Guide {#migration} + +### Migrating to `0.5.X` + +To make these helpers more flexible as well as more maintainable and easier to upgrade for new versions of Next.js, we're stripping them down to the most useful part which is managing the cookies and giving you an authenticated supabase-js client in any environment (client, server, middleware/edge). + +Therefore we're marking the `withApiAuth`, `withPageAuth`, and `withMiddlewareAuth` higher order functions as deprectaed and they will be removed in the next **minor** release (v0.6.X). + +Please follow the steps below to update your API routes, pages, and middleware handlers. Thanks! + +#### `withApiAuth` deprecated! + +Use `createServerSupabaseClient` within your `NextApiHandler`: + + + + +```tsx title="pages/api/protected-route.ts" import { withApiAuth } from '@supabase/auth-helpers-nextjs' export default withApiAuth(async function ProtectedRoute(req, res, supabase) { @@ -356,45 +627,173 @@ export default withApiAuth(async function ProtectedRoute(req, res, supabase) { }) ``` -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) +```tsx title="pages/api/protected-route.ts" +import { NextApiHandler } from 'next' +import { createServerSupabaseClient } from '@supabase/auth-helpers-nextjs' -As an alternative to protecting individual pages using `getServerSideProps` with `withPageAuth`, `withMiddlewareAuth` can be used from inside a `middleware` file to protect the entire directory or those that match the config object. In the following example, all requests to `/middleware-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: +const ProtectedRoute: NextApiHandler = async (req, res) => { + // Create authenticated Supabase Client + const supabase = createServerSupabaseClient({ req, res }) + // Check if we have a session + const { + data: { session }, + } = await supabase.auth.getSession() -```ts title="middleware.ts" -import { withMiddlewareAuth } from '@supabase/auth-helpers-nextjs' + if (!session) + return res.status(401).json({ + error: 'not_authenticated', + description: 'The user does not have an active session or is not authenticated', + }) -export const middleware = withMiddlewareAuth({ redirectTo: '/login' }) + // Run queries with RLS on the server + const { data } = await supabase.from('test').select('*') + res.json(data) +} -export const config = { - matcher: ['/middleware-protected/:path*'], +export default ProtectedRoute +``` + + + + +#### `withPageAuth` deprecated! + +Use `createServerSupabaseClient` within `getServerSideProps`: + + + + +```tsx title="pages/profile.tsx" +import { withPageAuth, User } from '@supabase/auth-helpers-nextjs' + +export default function Profile({ user }: { user: User }) { + return
{JSON.stringify(user, null, 2)}
+} + +export const getServerSideProps = withPageAuth({ redirectTo: '/' }) +``` + +
+ + +```tsx title="pages/profile.js" +import { createServerSupabaseClient, User } from '@supabase/auth-helpers-nextjs' +import { GetServerSidePropsContext } from 'next' + +export default function Profile({ user }: { user: User }) { + return
{JSON.stringify(user, null, 2)}
+} + +export const getServerSideProps = async (ctx: GetServerSidePropsContext) => { + // Create authenticated Supabase Client + const supabase = createServerSupabaseClient(ctx) + // Check if we have a session + const { + data: { session }, + } = await supabase.auth.getSession() + + if (!session) + return { + redirect: { + destination: '/', + permanent: false, + }, + } + + return { + props: { + initialSession: session, + user: session.user, + }, + } } ``` -It is also possible to add finer granularity based on the user logged in. I.e. you can specify a promise to determine if a specific user has permission or not. +
+
-```ts title="middleware.ts" +#### `withMiddlewareAuth` deprecated! + + + + +```tsx title="middleware.ts" import { withMiddlewareAuth } from '@supabase/auth-helpers-nextjs' export const middleware = withMiddlewareAuth({ - redirectTo: '/login', + redirectTo: '/', authGuard: { - isPermitted: async (user) => user.email?.endsWith('@example.com') ?? false, + isPermitted: async (user) => { + return user.email?.endsWith('@gmail.com') ?? false + }, redirectTo: '/insufficient-permissions', }, }) export const config = { - matcher: ['/middleware-protected/:path*'], + matcher: '/middleware-protected', } ``` -## Migrating to version `0.4.X` {#migration} + + -- With `supabase-js` v2, the `auth` API routes are no longer required and you can delete the `auth` directory under the `/pages/api/` directory. -- The `/api/auth/logout` API route has been removed—use the `signout` method instead. - ```js +```tsx title="middleware.ts" +import { createMiddlewareSupabaseClient } from '@supabase/auth-helpers-nextjs' +import { NextResponse } from 'next/server' +import type { NextRequest } from 'next/server' + +export async function middleware(req: NextRequest) { + // We need to create a response and hand it to the supabase client to be able to modify the response headers. + const res = NextResponse.next() + // Create authenticated Supabase Client. + const supabase = createMiddlewareSupabaseClient({ req, res }) + // Check if we have a session + const { + data: { session }, + } = await supabase.auth.getSession() + + // Check auth condition + if (session?.user.email?.endsWith('@gmail.com')) { + // Authentication successful, forward request to protected route. + return res + } + + // Auth condition not met, redirect to home page. + const redirectUrl = req.nextUrl.clone() + redirectUrl.pathname = '/' + redirectUrl.searchParams.set(`redirectedFrom`, req.nextUrl.pathname) + return NextResponse.redirect(redirectUrl) +} + +export const config = { + matcher: '/middleware-protected', +} +``` + + + + +### Migrating to `0.4.X` and supabase-js v2 + +- With the update to `supabase-js` v2 the `auth` API routes are no longer required, therefore you can go ahead and delete your `auth` directory under the `/pages/api/` directory. Please refer to the [v2 migration guide](https://supabase.com/docs/reference/javascript/upgrade-guide) for the full set of changes within supabase-js. + +- The `/api/auth/logout` API route has been removed, please use the `signout` method instead: + + ```jsx ``` + - The `supabaseClient` and `supabaseServerClient` have been removed in favor of the `createBrowserSupabaseClient` and `createServerSupabaseClient` methods. This allows you to provide the CLI-generated types to the client: - ```js + ```tsx // client-side - import type { Database } from 'types_db'; - const [supabaseClient] = useState(() => - createBrowserSupabaseClient() - ); + import type { Database } from 'types_db' + const [supabaseClient] = useState(() => createBrowserSupabaseClient()) // server-side API route import type { NextApiRequest, NextApiResponse } from 'next' - import type { Database } from 'types_db'; + import type { Database } from 'types_db' export default async (req: NextApiRequest, res: NextApiResponse) => { - const supabaseServerClient = createServerSupabaseClient({ req, res }) - const { data:{ user } } = await supabaseServerClient.auth.getUser() + const supabaseServerClient = createServerSupabaseClient({ + req, + res, + }) + const { + data: { user }, + } = await supabaseServerClient.auth.getUser() res.status(200).json({ name: user?.name ?? '' }) } @@ -429,19 +832,19 @@ export const config = { - The `useUser` hook now returns the `user` object or `null`. - Usage with TypeScript: You can pass types that were [generated with the Supabase CLI](/docs/reference/javascript/typescript-support#generating-types) to the Supabase Client to get enhanced type safety and auto completion: - ```js - // Creating a new supabase client object: - import { Database } from '../database.types'; +Creating a new supabase client object: - const [supabaseClient] = useState(() => - createBrowserSupabaseClient() - ); - ``` +```tsx +import { Database } from '../database.types' - ```js - // Retrieving a supabase client object from the SessionContext: - import { useSupabaseClient } from '@supabase/auth-helpers-react'; - import { Database } from '../database.types'; +const [supabaseClient] = useState(() => createBrowserSupabaseClient()) +``` - const supabaseClient = useSupabaseClient(); - ``` +Retrieving a supabase client object from the SessionContext: + +```tsx +import { useSupabaseClient } from '@supabase/auth-helpers-react' +import { Database } from '../database.types' + +const supabaseClient = useSupabaseClient() +```