Skip to content

Commit

Permalink
feat(next): allow unstable_getServerSession in Server Components (#…
Browse files Browse the repository at this point in the history
…5741)

* feat(next): support Server Components with `unstable_getServerSession`

* chore: remove `.entries`

* docs(next): add documentation for RSC

* update beta docs

* chore(dev): add app dir

* fix text

* only show second warning if using with RSC

* only delete expires for RSC case
  • Loading branch information
balazsorban44 committed Nov 6, 2022
1 parent 27a0b70 commit e90925b
Show file tree
Hide file tree
Showing 11 changed files with 136 additions and 21 deletions.
12 changes: 12 additions & 0 deletions apps/dev/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html>
<head></head>
<body>{children}</body>
</html>
)
}
7 changes: 7 additions & 0 deletions apps/dev/app/server-component/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { unstable_getServerSession } from "next-auth/next"
import { authOptions } from "pages/api/auth/[...nextauth]"

export default async function Page() {
const session = await unstable_getServerSession(authOptions)
return <pre>{JSON.stringify(session, null, 2)}</pre>
}
1 change: 1 addition & 0 deletions apps/dev/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ module.exports = {
config.experiments = { ...config.experiments, topLevelAwait: true }
return config
},
experimental: { appDir: true },
typescript: { ignoreBuildErrors: true },
}
6 changes: 3 additions & 3 deletions apps/dev/pages/api/examples/session.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// 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]';
import { authOptions } from "../auth/[...nextauth]"

export default async (req, res) => {
const session = await unstable_getServerSession(req, res, authOptions)
res.send(JSON.stringify(session, null, 2))
const session = await unstable_getServerSession(authOptions)
res.json(session)
}
23 changes: 20 additions & 3 deletions apps/dev/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
{
"compilerOptions": {
"target": "esnext",
"lib": ["dom", "dom.iterable", "esnext"],
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
Expand All @@ -15,7 +19,20 @@
"incremental": true,
"jsx": "preserve",
"baseUrl": ".",
"plugins": [
{
"name": "next"
}
]
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules", "jest.config.js"]
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
],
"exclude": [
"node_modules",
"jest.config.js"
]
}
7 changes: 3 additions & 4 deletions apps/example-nextjs/pages/server.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,12 @@ export default function ServerSidePage({ session }: { session: Session }) {
<h1>Server Side Rendering</h1>
<p>
This page uses the <strong>unstable_getServerSession()</strong> method
in <strong>unstable_getServerSideProps()</strong>.
in <strong>getServerSideProps()</strong>.
</p>
<p>
Using <strong>unstable_getServerSession()</strong> in{" "}
<strong>unstable_getServerSideProps()</strong> is the recommended
approach if you need to support Server Side Rendering with
authentication.
<strong>getServerSideProps()</strong> is the recommended approach if you
need to support Server Side Rendering with authentication.
</p>
<p>
The advantage of Server Side Rendering is this page does not require
Expand Down
23 changes: 21 additions & 2 deletions docs/docs/configuration/nextjs.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const authOptions: NextAuthOptions = {
export default NextAuth(authOptions);
```

In `getServerSideProps`:
### In `getServerSideProps`:
```js
import { authOptions } from 'pages/api/auth/[...nextauth]'
import { unstable_getServerSession } from "next-auth/next"
Expand All @@ -48,7 +48,8 @@ export async function getServerSideProps(context) {
}
}
```
In API routes:

### In API Routes:
```js
import { authOptions } from 'pages/api/auth/[...nextauth]'
import { unstable_getServerSession } from "next-auth/next"
Expand All @@ -68,6 +69,24 @@ export async function handler(req, res) {
}
```

### In `app/` directory:

You can also use `unstable_getServerSession` in Next.js' server components:

```tsx
import { unstable_getServerSession } from "next-auth/next"
import { authOptions } from "pages/api/auth/[...nextauth]"

export default async function Page() {
const session = await unstable_getServerSession(authOptions)
return <pre>{JSON.stringify(session, null, 2)}</pre>
}
```

:::warning
Currently, the underlying Next.js `cookies()` method does [only provides read access](https://beta.nextjs.org/docs/api-reference/cookies) to the request cookies. This means that the `expires` value is stripped away from `session` in Server Components. Furthermore, there is a hard expiry on sessions, after which the user will be required to sign in again. (The default expiry is 30 days).
:::

## Middleware

You can use a Next.js Middleware with NextAuth.js to protect your site.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const authOptions: NextAuthOptions = {
export default NextAuth(authOptions);
```

In `getServerSideProps`:
### In `getServerSideProps`:
```js
import { authOptions } from 'pages/api/[...nextauth]'
import { unstable_getServerSession } from "next-auth/next"
Expand All @@ -48,7 +48,8 @@ export async function getServerSideProps(context) {
}
}
```
In API routes:

### In API routes:
```js
import { authOptions } from 'pages/api/[...nextauth]'
import { unstable_getServerSession } from "next-auth/next"
Expand All @@ -68,6 +69,24 @@ export async function handler(req, res) {
}
```

### In `app/` directory:

You can also use `unstable_getServerSession` in Next.js' server components:

```tsx
import { unstable_getServerSession } from "next-auth/next"
import { authOptions } from "pages/api/auth/[...nextauth]"

export default async function Page() {
const session = await unstable_getServerSession(authOptions)
return <pre>{JSON.stringify(session, null, 2)}</pre>
}
```

:::warning
Currently, the underlying Next.js `cookies()` method does [only provides read access](https://beta.nextjs.org/docs/api-reference/cookies) to the request cookies. This means that the `expires` value is stripped away from `session` in Server Components. Furthermore, there is a hard expiry on sessions, after which the user will be required to sign in again. (The default expiry is 30 days).
:::

## Middleware

You can use a Next.js Middleware with NextAuth.js to protect your site.
Expand Down
6 changes: 2 additions & 4 deletions packages/next-auth/src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,8 @@ async function toInternalRequest(
const url = new URL(req.url)
// TODO: handle custom paths?
const nextauth = url.pathname.split("/").slice(3)
const headers = Object.fromEntries(req.headers.entries())
const query: Record<string, any> = Object.fromEntries(
url.searchParams.entries()
)
const headers = Object.fromEntries(req.headers)
const query: Record<string, any> = Object.fromEntries(url.searchParams)
query.nextauth = nextauth

return {
Expand Down
2 changes: 1 addition & 1 deletion packages/next-auth/src/core/lib/oauth/authorization-url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export default async function getAuthorizationUrl({

if (typeof provider.authorization === "string") {
const parsedUrl = new URL(provider.authorization)
const parsedParams = Object.fromEntries(parsedUrl.searchParams.entries())
const parsedParams = Object.fromEntries(parsedUrl.searchParams)
params = { ...params, ...parsedParams }
} else {
params = { ...params, ...provider.authorization?.params }
Expand Down
47 changes: 45 additions & 2 deletions packages/next-auth/src/next/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { NextAuthHandler } from "../core"
import { detectHost } from "../utils/detect-host"
import { setCookie } from "./utils"
import { cookies as nextCookies, headers } from "next/headers"

import type {
GetServerSidePropsContext,
Expand Down Expand Up @@ -84,6 +85,7 @@ function NextAuth(
export default NextAuth

let experimentalWarningShown = false
let experimentalRSCWarningShown = false
export async function unstable_getServerSession(
...args:
| [
Expand All @@ -92,6 +94,7 @@ export async function unstable_getServerSession(
NextAuthOptions
]
| [NextApiRequest, NextApiResponse, NextAuthOptions]
| [NextAuthOptions]
): Promise<Session | null> {
if (!experimentalWarningShown && process.env.NODE_ENV !== "production") {
console.warn(
Expand All @@ -103,7 +106,33 @@ export async function unstable_getServerSession(
experimentalWarningShown = true
}

const [req, res, options] = args
const isRSC = args.length === 1
if (
!experimentalRSCWarningShown &&
isRSC &&
process.env.NODE_ENV !== "production"
) {
console.warn(
"[next-auth][warn][EXPERIMENTAL_API]",
"\n`unstable_getServerSession` is used in a React Server Component.",
`\nhttps://next-auth.js.org/configuration/nextjs#unstable_getServerSession}`,
`\nhttps://next-auth.js.org/warnings#EXPERIMENTAL_API`
)
experimentalRSCWarningShown = true
}

const [req, res, options] = isRSC
? [
{
headers: ensureAvailable(headers),
cookies: ensureAvailable(nextCookies)
.getAll()
.reduce((acc, c) => ({ ...acc, [c.name]: c.value }), {}),
} as any,
{ getHeader() {}, setCookie() {}, setHeader() {} } as any,
args[0],
]
: args

options.secret = options.secret ?? process.env.NEXTAUTH_SECRET

Expand All @@ -123,7 +152,11 @@ export async function unstable_getServerSession(
cookies?.forEach((cookie) => setCookie(res, cookie))

if (body && typeof body !== "string" && Object.keys(body).length) {
if (status === 200) return body as Session
if (status === 200) {
// @ts-expect-error
if (isRSC) delete body.expires
return body as Session
}
throw new Error((body as any).message)
}

Expand All @@ -139,3 +172,13 @@ declare global {
}
}
}

function ensureAvailable(fn: () => any) {
try {
return fn()
} catch (error) {
throw new Error(
`Could not access ${fn.name}(). If you are not in a React Server Cmponent, pass the request and response.`
)
}
}

1 comment on commit e90925b

@vercel
Copy link

@vercel vercel bot commented on e90925b Nov 6, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.