From 9c6ab80c0fd18f1850dcab319573f2e303392228 Mon Sep 17 00:00:00 2001 From: wangsijie Date: Mon, 6 Jan 2025 16:56:44 +0800 Subject: [PATCH] refactor: rewrite next-auth quick start --- ...ion.md => _scopes-claims-introduction.mdx} | 2 +- .../framework/next-auth/README.mdx | 43 ++++-- .../next-auth/_get-user-information.mdx | 128 ++++++++++++++++++ .../framework/next-auth/_installation.mdx | 32 +++++ ...{_config-provider.mdx => _integration.mdx} | 112 +++++++++++---- .../next-auth/_scopes-and-claims-code.md | 13 -- .../next-auth/_scopes-and-claims.mdx | 5 - .../api-resources/_config-api-resources.mdx | 11 ++ .../_fetch-access-token-for-api-resources.mdx | 3 + .../_fetch-organization-token-for-user.mdx | 10 ++ .../code/_config-organization-code.md | 19 +++ .../code/_config-resources-code.md | 20 +++ .../_config-resources-with-scopes-code.md | 20 +++ ...onfig-resources-with-shared-scopes-code.md | 20 +++ .../code/_get-access-token-code.mdx | 83 ++++++++++++ .../_get-organization-access-token-code.mdx | 36 +++++ 16 files changed, 502 insertions(+), 55 deletions(-) rename docs/quick-starts/fragments/{_scopes-claims-introduction.md => _scopes-claims-introduction.mdx} (85%) create mode 100644 docs/quick-starts/framework/next-auth/_get-user-information.mdx create mode 100644 docs/quick-starts/framework/next-auth/_installation.mdx rename docs/quick-starts/framework/next-auth/{_config-provider.mdx => _integration.mdx} (58%) delete mode 100644 docs/quick-starts/framework/next-auth/_scopes-and-claims-code.md delete mode 100644 docs/quick-starts/framework/next-auth/_scopes-and-claims.mdx create mode 100644 docs/quick-starts/framework/next-auth/api-resources/_config-api-resources.mdx create mode 100644 docs/quick-starts/framework/next-auth/api-resources/_fetch-access-token-for-api-resources.mdx create mode 100644 docs/quick-starts/framework/next-auth/api-resources/_fetch-organization-token-for-user.mdx create mode 100644 docs/quick-starts/framework/next-auth/api-resources/code/_config-organization-code.md create mode 100644 docs/quick-starts/framework/next-auth/api-resources/code/_config-resources-code.md create mode 100644 docs/quick-starts/framework/next-auth/api-resources/code/_config-resources-with-scopes-code.md create mode 100644 docs/quick-starts/framework/next-auth/api-resources/code/_config-resources-with-shared-scopes-code.md create mode 100644 docs/quick-starts/framework/next-auth/api-resources/code/_get-access-token-code.mdx create mode 100644 docs/quick-starts/framework/next-auth/api-resources/code/_get-organization-access-token-code.mdx diff --git a/docs/quick-starts/fragments/_scopes-claims-introduction.md b/docs/quick-starts/fragments/_scopes-claims-introduction.mdx similarity index 85% rename from docs/quick-starts/fragments/_scopes-claims-introduction.md rename to docs/quick-starts/fragments/_scopes-claims-introduction.mdx index dd42558261b..5b03d2e2994 100644 --- a/docs/quick-starts/fragments/_scopes-claims-introduction.md +++ b/docs/quick-starts/fragments/_scopes-claims-introduction.mdx @@ -34,4 +34,4 @@ classDiagram The "sub" claim means "subject", which is the unique identifier of the user (i.e. user ID). ::: -Logto SDK will always request three scopes: `openid`, `profile`, and `offline_access`. +{!props.hideDefaultScopes &&

Logto SDK will always request three scopes: `openid`, `profile`, and `offline_access`.

} diff --git a/docs/quick-starts/framework/next-auth/README.mdx b/docs/quick-starts/framework/next-auth/README.mdx index 565d7ef008e..7d78f4e394e 100644 --- a/docs/quick-starts/framework/next-auth/README.mdx +++ b/docs/quick-starts/framework/next-auth/README.mdx @@ -1,20 +1,25 @@ --- slug: /quick-starts/next-auth -sidebar_label: Next Auth +sidebar_label: Auth.js (Next Auth) sidebar_custom_props: logoFilename: 'next-auth.svg' description: Authentication for Next.js. --- +import ApiResourcesDescription from '../../fragments/_api-resources-description.md'; import FurtherReadings from '../../fragments/_further-readings.md'; -import ConfigProvider from './_config-provider.mdx'; +import GetUserInformation from './_get-user-information.mdx'; import GuideTip from './_guide-tip.mdx'; -import ScopesAndClaims from './_scopes-and-claims.mdx'; +import Installation from './_installation.mdx'; +import Integration from './_integration.mdx'; +import ConfigApiResources from './api-resources/_config-api-resources.mdx'; +import FetchAccessTokenForApiResources from './api-resources/_fetch-access-token-for-api-resources.mdx'; +import FetchOrganizationTokenForUser from './api-resources/_fetch-organization-token-for-user.mdx'; -# Add authentication to your Next Auth application +# Add authentication to your Auth.js (Next Auth) application -This guide will show you how to integrate Logto into your Next.js application with [Next Auth](https://next-auth.js.org/). +This guide will show you how to integrate Logto into your Next.js application with [Auth.js](https://authjs.dev/), previously known as Next Auth. @@ -22,21 +27,35 @@ This guide will show you how to integrate Logto into your Next.js application wi - A [Logto Cloud](https://cloud.logto.io) account or a [self-hosted Logto](/introduction/set-up-logto-oss). - A Logto traditional application created. -- A Next.js project with Next Auth, check out the [Next Auth documentation](https://next-auth.js.org/getting-started/introduction). +- A Next.js project with Auth.js, check out the [Auth.js documentation](https://authjs.dev/getting-started/installation). + +## Installation \{#installation} + + ## Integration \{#integration} -### Config Next Auth provider \{#config-next-auth-provider} + + +## Fetch user information \{#fetch-user-information} + + + +## API resources \{#api-resources} + + + +### Configure Logto provider \{#configure-logto-provider} - + -### Checkpoint \{#checkpoint} +### Fetch access token for the API resource \{#fetch-access-token-for-the-api-resource} -Now, you can test your application to see if the authentication works as expected. + -## Scopes and claims \{#scopes-and-claims} +### Fetch organization tokens \{#fetch-organization-tokens} - + ## Further readings \{#further-readings} diff --git a/docs/quick-starts/framework/next-auth/_get-user-information.mdx b/docs/quick-starts/framework/next-auth/_get-user-information.mdx new file mode 100644 index 00000000000..25f6e7a4ee0 --- /dev/null +++ b/docs/quick-starts/framework/next-auth/_get-user-information.mdx @@ -0,0 +1,128 @@ +import FindUserInfoMissing from '../../fragments/_find-user-info-missing.mdx'; +import ScopesAndClaims from '../../fragments/_scopes-and-claims.mdx'; +import ScopesAndClaimsIntroduction from '../../fragments/_scopes-claims-introduction.md'; + +### Display user information \{#display-user-information} + +When user is signed in, the return value of `auth()` will be an object containing the user's information. You can display this information in your app: + +```tsx title="app/page.tsx" +import { auth } from '@/auth'; + +export default async function Home() { + const session = await auth(); + + return ( +
+ {session?.user && ( +
+

Claims:

+ + + + + + + + + {Object.entries(session.user).map(([key, value]) => ( + + + + + ))} + +
NameValue
{key}{String(value)}
+
+ )} +
+ ); +} +``` + +### Request additional claims \{#request-additional-claims} + + + + + +To request additional scopes, you can configure the params of Logto provider: + +```ts title="./auth.ts" +import NextAuth from 'next-auth'; + +export const { handlers, signIn, signOut, auth } = NextAuth({ + providers: [ + { + id: 'logto',, + // ... + authorization: { + params: { + // highlight-next-line + scope: 'openid offline_access profile email', + }, + }, + // ... + }, + ], +}); +``` + +### Claims that need network requests \{#claims-that-need-network-requests} + +To prevent bloating the ID token, some claims require network requests to fetch. For example, the `custom_data` claim is not included in the user object even if it's requested in the scopes. To access these claims, you need to make a network request to fetch the user info. + +#### Get access token \{#get-access-token} + +Update the `NextAuth` config so that we can get the access token: + +```ts title="./auth.ts" +export const { handlers, signIn, signOut, auth } = NextAuth({ + // ... + callbacks: { + async jwt({ token, account }) { + if (account) { + token.accessToken = account.access_token; + } + return token; + }, + async session({ session, token }) { + // Inject the access token into the session object + session.accessToken = token.accessToken; + return session; + }, + }, +}); +``` + +#### Fetch user info \{#fetch-user-info} + +Now access the OIDC user info endpoint with the access token: + +```ts title="./app/page.tsx" +// ... + +export default async function Home() { + const session = await auth(); + // Replace the URL with your Logto endpoint, should ends with `/oidc/me` + const response = await fetch('https://xxx.logto.app/oidc/me', { + headers: { + Authorization: `Bearer ${session?.accessToken}`, + }, + }); + const user = await response.json(); + console.log(user); + + // ... +} +``` + +Above is a simple example. Remember to handle the error cases. + +#### Access token refresh \{#access-token-refresh} + +An access token is valid for a short period of time. By defualt, Next.js will only fetch one when the session is created. To implement auto access token refresh, see [Refresh token rotation](https://next-auth.js.org/v3/tutorials/refresh-token-rotation). + +### Scopes and claims \{#scopes-and-claims} + + diff --git a/docs/quick-starts/framework/next-auth/_installation.mdx b/docs/quick-starts/framework/next-auth/_installation.mdx new file mode 100644 index 00000000000..447e251dd0e --- /dev/null +++ b/docs/quick-starts/framework/next-auth/_installation.mdx @@ -0,0 +1,32 @@ +import TabItem from '@theme/TabItem'; +import Tabs from '@theme/Tabs'; + +Install Auth.js via your favorite package manager: + + + + + +
+  npm i next-auth@beta
+
+ +
+ + +
+  pnpm add next-auth@beta
+
+ +
+ + +
+  yarn add next-auth@beta
+
+ +
+ +
+ +See [Auth.js documentation](https://authjs.dev/getting-started/installation) for more details. diff --git a/docs/quick-starts/framework/next-auth/_config-provider.mdx b/docs/quick-starts/framework/next-auth/_integration.mdx similarity index 58% rename from docs/quick-starts/framework/next-auth/_config-provider.mdx rename to docs/quick-starts/framework/next-auth/_integration.mdx index 0a3273d4151..5c07f2336b5 100644 --- a/docs/quick-starts/framework/next-auth/_config-provider.mdx +++ b/docs/quick-starts/framework/next-auth/_integration.mdx @@ -7,38 +7,25 @@ import GetAppSecret from '../../fragments/_get-app-secret.mdx'; import AssumingUrl from '../../fragments/_web-assuming-url.mdx'; import SignInFlowSummary from '../../fragments/_web-sign-in-flow-summary.mdx'; - - - - -#### Configure sign-in redirect URI \{#configure-sign-in-redirect-uri} - - - -#### Set up Next Auth provider \{#set-up-next-auth-provider} +### Set up Auth.js provider \{#set-up-authjs-provider} -Modify your API route config of Next Auth, if you are using Pages Router, the file is in `pages/api/auth/[...nextauth].js`, if you are using App Router, the file is in `app/api/auth/[...nextauth]/route.ts`. - -The following is an example of App Router: +Modify your API route config of Auth.js, add Logto as an OIDC provider: - + + +```ts title="./app/api/auth/[...nextauth]/route.ts" +import { handlers } from '@/auth'; +export const { GET, POST } = handlers; +``` -```ts +```ts title="./auth.ts" import NextAuth from 'next-auth'; -export const { - handlers: { GET, POST }, - signIn, - signOut, - auth, -} = NextAuth({ +export const { handlers, signIn, signOut, auth } = NextAuth({ providers: [ { id: 'logto', @@ -70,11 +57,17 @@ export const { 2. Replace the `clientId` and `clientSecret` with your Logto application's ID and secret. 3. Customize the `profile` function to map the user profile to the Next Auth user object, the default mapping is shown in the example. +Then you can also add an optional Middleware to keep the session alive: + +```ts title="./middleware.ts" +export { auth as middleware } from '@/auth'; +``` + -```ts +```ts title="app/api/auth/[...nextauth]/route.ts" import NextAuth from 'next-auth'; const handler = NextAuth({ @@ -116,3 +109,74 @@ export { handler as GET, handler as POST }; + +You can find more details in the [Auth.js documentation](https://authjs.dev/getting-started/installation). + +### Configure sign-in redirect URI \{#configure-sign-in-redirect-uri} + + + + + + + +### Implement sign-in and sign-out \{#implement-sign-in-and-sign-out} + +#### Implement sign-in and sign-out button \{#implement-sign-in-and-sign-out-button} + +```tsx title="app/components/sign-in.tsx" +import { signIn } from '@/auth'; + +export default function SignIn() { + return ( +
{ + 'use server'; + await signIn('logto'); + }} + > + +
+ ); +} +``` + +```tsx title="app/components/sign-out.tsx" +import { signOut } from '@/auth'; + +export function SignOut() { + return ( +
{ + 'use server'; + await signOut(); + }} + > + +
+ ); +} +``` + +#### Show sign-in and sign-out button in the page \{#show-sign-in-and-sign-out-button-in-the-page} + +```tsx title="app/page.tsx" +import SignIn from './components/sign-in'; +import SignOut from './components/sign-out'; +import { auth } from '@/auth'; + +export default function Home() { + const session = await auth(); + + return
{session?.user ? : }
; +} +``` + +Above is a simple example, you can check the [Auth.js documentation](https://authjs.dev/getting-started/session-management/login) for more details. + +### Checkpoint \{#checkpoint} + +Now, you can test your application to see if the authentication works as expected. diff --git a/docs/quick-starts/framework/next-auth/_scopes-and-claims-code.md b/docs/quick-starts/framework/next-auth/_scopes-and-claims-code.md deleted file mode 100644 index ddf7ae41225..00000000000 --- a/docs/quick-starts/framework/next-auth/_scopes-and-claims-code.md +++ /dev/null @@ -1,13 +0,0 @@ -```ts -const handler = NextAuth({ - providers: [ - { - id: 'logto', - name: 'Logto', - // ... other options - authorization: { params: { scope: 'openid offline_access profile email' } }, - // ... other options - }, - ], -}); -``` diff --git a/docs/quick-starts/framework/next-auth/_scopes-and-claims.mdx b/docs/quick-starts/framework/next-auth/_scopes-and-claims.mdx deleted file mode 100644 index 7c4ba9d1799..00000000000 --- a/docs/quick-starts/framework/next-auth/_scopes-and-claims.mdx +++ /dev/null @@ -1,5 +0,0 @@ -import ScopesAndClaims from '../../fragments/_scopes-and-claims.mdx'; - -import ScopesAndClaimsCode from './_scopes-and-claims-code.md'; - -} /> diff --git a/docs/quick-starts/framework/next-auth/api-resources/_config-api-resources.mdx b/docs/quick-starts/framework/next-auth/api-resources/_config-api-resources.mdx new file mode 100644 index 00000000000..9591c7c1c66 --- /dev/null +++ b/docs/quick-starts/framework/next-auth/api-resources/_config-api-resources.mdx @@ -0,0 +1,11 @@ +import ConfigApiResources from '../../../fragments/_config-api-resources.mdx'; + +import ConfigResourcesCode from './code/_config-resources-code.md'; +import ConfigResourcesWithScopesCode from './code/_config-resources-with-scopes-code.md'; +import ConfigResourcesWithSharedScopesCode from './code/_config-resources-with-shared-scopes-code.md'; + +} + configResourcesWithScopesCode={} + configResourcesWithSharedScopesCode={} +/> diff --git a/docs/quick-starts/framework/next-auth/api-resources/_fetch-access-token-for-api-resources.mdx b/docs/quick-starts/framework/next-auth/api-resources/_fetch-access-token-for-api-resources.mdx new file mode 100644 index 00000000000..6423448a2ee --- /dev/null +++ b/docs/quick-starts/framework/next-auth/api-resources/_fetch-access-token-for-api-resources.mdx @@ -0,0 +1,3 @@ +import GetAccessTokenCode from './code/_get-access-token-code.mdx'; + + diff --git a/docs/quick-starts/framework/next-auth/api-resources/_fetch-organization-token-for-user.mdx b/docs/quick-starts/framework/next-auth/api-resources/_fetch-organization-token-for-user.mdx new file mode 100644 index 00000000000..651c813f14b --- /dev/null +++ b/docs/quick-starts/framework/next-auth/api-resources/_fetch-organization-token-for-user.mdx @@ -0,0 +1,10 @@ +import FetchOrganizationTokenForUser from '../../../fragments/_fetch-organization-token-for-user.mdx'; + +import ConfigOrganizationCode from './code/_config-organization-code.md'; +import GetOrganizationAccessTokenCode from './code/_get-organization-access-token-code.mdx'; + +} + getOrganizationAccessTokenCode={} +/> diff --git a/docs/quick-starts/framework/next-auth/api-resources/code/_config-organization-code.md b/docs/quick-starts/framework/next-auth/api-resources/code/_config-organization-code.md new file mode 100644 index 00000000000..c62993a44d4 --- /dev/null +++ b/docs/quick-starts/framework/next-auth/api-resources/code/_config-organization-code.md @@ -0,0 +1,19 @@ +```ts title="./auth.ts" +import NextAuth from 'next-auth'; + +export const { handlers, signIn, signOut, auth } = NextAuth({ + providers: [ + { + id: 'logto',, + // ... + authorization: { + params: { + // highlight-next-line + scope: 'openid offline_access urn:logto:scope:organizations', + }, + }, + // ... + }, + ], +}); +``` diff --git a/docs/quick-starts/framework/next-auth/api-resources/code/_config-resources-code.md b/docs/quick-starts/framework/next-auth/api-resources/code/_config-resources-code.md new file mode 100644 index 00000000000..ddde5aecbb0 --- /dev/null +++ b/docs/quick-starts/framework/next-auth/api-resources/code/_config-resources-code.md @@ -0,0 +1,20 @@ +```ts title="./auth.ts" +import NextAuth from 'next-auth'; + +export const { handlers, signIn, signOut, auth } = NextAuth({ + providers: [ + { + id: 'logto',, + // ... + authorization: { + params: { + scope: 'openid offline_access profile email', + // highlight-next-line + resource: 'https://shopping.your-app.com/api', + }, + }, + // ... + }, + ], +}); +``` diff --git a/docs/quick-starts/framework/next-auth/api-resources/code/_config-resources-with-scopes-code.md b/docs/quick-starts/framework/next-auth/api-resources/code/_config-resources-with-scopes-code.md new file mode 100644 index 00000000000..81fa1a08452 --- /dev/null +++ b/docs/quick-starts/framework/next-auth/api-resources/code/_config-resources-with-scopes-code.md @@ -0,0 +1,20 @@ +```ts title="./auth.ts" +import NextAuth from 'next-auth'; + +export const { handlers, signIn, signOut, auth } = NextAuth({ + providers: [ + { + id: 'logto',, + // ... + authorization: { + params: { + // highlight-next-line + scope: 'openid offline_access profile email shopping:read shopping:write', + resource: 'https://shopping.your-app.com/api', + }, + }, + // ... + }, + ], +}); +``` diff --git a/docs/quick-starts/framework/next-auth/api-resources/code/_config-resources-with-shared-scopes-code.md b/docs/quick-starts/framework/next-auth/api-resources/code/_config-resources-with-shared-scopes-code.md new file mode 100644 index 00000000000..c9c349eea02 --- /dev/null +++ b/docs/quick-starts/framework/next-auth/api-resources/code/_config-resources-with-shared-scopes-code.md @@ -0,0 +1,20 @@ +```ts title="./auth.ts" +import NextAuth from 'next-auth'; + +export const { handlers, signIn, signOut, auth } = NextAuth({ + providers: [ + { + id: 'logto',, + // ... + authorization: { + params: { + // highlight-next-line + scope: 'openid offline_access profile read write', + resource: 'https://shopping.your-app.com/api', + }, + }, + // ... + }, + ], +}); +``` diff --git a/docs/quick-starts/framework/next-auth/api-resources/code/_get-access-token-code.mdx b/docs/quick-starts/framework/next-auth/api-resources/code/_get-access-token-code.mdx new file mode 100644 index 00000000000..34443cb87e4 --- /dev/null +++ b/docs/quick-starts/framework/next-auth/api-resources/code/_get-access-token-code.mdx @@ -0,0 +1,83 @@ +Auth.js will only fetch the access token once without resource parameter. We need to implement the access token fetching by ourselves. + +#### Get refresh token \{#get-refresh-token} + +Update Logto provider config, add "prompt" parameter and set it to `consent`, and ensure `offline_access` scope is included: + +```ts title="./auth.ts" +import NextAuth from 'next-auth'; + +export const { handlers, signIn, signOut, auth } = NextAuth({ + // ... + authorization: { + params: { + // highlight-next-line + prompt: 'consent', + scope: 'openid offline_access shopping:read shopping:write', + resource: 'https://shopping.your-app.com/api', + // ... + }, + }, + // ... +}); +``` + +Then add a callback to save the `refresh_token` to the session: + +```ts title="./auth.ts" +export const { handlers, signIn, signOut, auth } = NextAuth({ + // ... + callbacks: { + async jwt({ token, account }) { + if (account) { + // ... + // highlight-next-line + token.refreshToken = account.refresh_token; + } + return token; + }, + async session({ session, token }) { + // ... + // highlight-next-line + session.refreshToken = token.refreshToken; + return session; + }, + }, +}); +``` + +#### Fetch access token \{#fetch-access-token} + +With the `refresh_token`, we can fetch the access token from Logto's OIDC token endpoint. + +```ts title="./app/page.tsx" +// ... + +export default async function Home() { + const session = await auth(); + + if (session?.refreshToken) { + // Replace the app ID and secret with your own, you can check the "Integration" section. + const basicAuth = Buffer.from(':').toString('base64'); + + // Replace the URL with your Logto endpoint, should ends with `/oidc/token` + const response = await fetch('https://xxx.logto.app/oidc/token', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Authorization: `Basic ${basicAuth}`, + }, + body: new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token: session.refreshToken, + resource: 'https://shopping.your-app.com/api', + }).toString(), + }); + + const data = await response.json(); + console.log(data.access_token); + } + + // ... +} +``` diff --git a/docs/quick-starts/framework/next-auth/api-resources/code/_get-organization-access-token-code.mdx b/docs/quick-starts/framework/next-auth/api-resources/code/_get-organization-access-token-code.mdx new file mode 100644 index 00000000000..163ed67cdf6 --- /dev/null +++ b/docs/quick-starts/framework/next-auth/api-resources/code/_get-organization-access-token-code.mdx @@ -0,0 +1,36 @@ +Similar to the access token for API resources, we can use the refresh token to fetch the organization access token. + +```ts title="./app/page.tsx" +// ... + +export default async function Home() { + const session = await auth(); + + if (session?.refreshToken) { + // Replace the app ID and secret with your own, you can check the "Integration" section. + const basicAuth = Buffer.from(':').toString('base64'); + + // Replace the URL with your Logto endpoint, should ends with `/oidc/token` + const response = await fetch('https://xxx.logto.app/oidc/token', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Authorization: `Basic ${basicAuth}`, + }, + body: new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token: session.refreshToken, + // highlight-next-line + resource: 'urn:logto:scope:organizations', + // highlight-next-line + organization_id: 'organization-id', + }).toString(), + }); + + const data = await response.json(); + console.log(data.access_token); + } + + // ... +} +```