Chuyển đến nội dung
Tài liệu · Next.js

Bắt đầu nhanh: Next.js

Thêm đăng nhập vào một ứng dụng Next.js dùng App Router. SDK @usher/nextjs sắp ra mắt; trong lúc chờ, hướng dẫn này dùng luồng OpenID Connect chuẩn, cũng chính là những gì SDK sẽ làm thay bạn.

Trước khi bắt đầu

  • Một ứng dụng Next.js 15 hoặc 16 dùng App Router.
  • Một môi trường Usher. Trong Usher Console, hãy mở dự án của bạn, chọn một môi trường, và ghi lại Bên phát hành của môi trường đó.

Đăng ký ứng dụng

Trong Console, vào Ứng dụng và tạo một Ứng dụng web. Thêm chính xác URI chuyển hướng này:

http://localhost:3000/auth/callback

Sao chép Client ID và Client secret. Client secret chỉ hiển thị một lần.

Thêm biến môi trường

.env.local
USHER_ISSUER=https://acme.usherid.com
USHER_CLIENT_ID=your_client_id
USHER_CLIENT_SECRET=your_client_secret
APP_URL=http://localhost:3000

Cài đặt một client OpenID Connect

Mọi client đã được chứng nhận đều dùng được. Hướng dẫn này dùng openid-client, thư viện xử lý khám phá, PKCE và xác thực token.

Terminal
pnpm add openid-client

Khám phá bên phát hành

Bước khám phá đọc các endpoint và khóa của bên phát hành một lần, nên không có gì phải viết cứng trong mã.

lib/usher.ts
import * as client from 'openid-client'

let config: Promise<client.Configuration> | undefined

export function usher() {
  config ??= client.discovery(
    new URL(process.env.USHER_ISSUER!),
    process.env.USHER_CLIENT_ID!,
    process.env.USHER_CLIENT_SECRET!,
  )
  return config
}

export const redirectUri = `${process.env.APP_URL}/auth/callback`

Đưa người dùng đến Lobby

Một route handler tạo PKCE verifier và giá trị state, lưu chúng trong một cookie ngắn hạn, rồi chuyển hướng đến Lobby.

app/auth/sign-in/route.ts
import * as client from 'openid-client'
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
import { redirectUri, usher } from '@/lib/usher'

export async function GET() {
  const config = await usher()
  const codeVerifier = client.randomPKCECodeVerifier()
  const state = client.randomState()

  const jar = await cookies()
  jar.set('usher_flow', JSON.stringify({ codeVerifier, state }), {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    path: '/auth',
    maxAge: 600,
  })

  const url = client.buildAuthorizationUrl(config, {
    redirect_uri: redirectUri,
    scope: 'openid profile email offline_access',
    code_challenge: await client.calculatePKCECodeChallenge(codeVerifier),
    code_challenge_method: 'S256',
    state,
  })
  redirect(url.href)
}

Xử lý callback

Usher đưa người dùng trở lại cùng một mã. Hãy đổi mã đó lấy token; openid-client sẽ kiểm tra state, PKCE verifier và chữ ký của token ID giúp bạn.

app/auth/callback/route.ts
import * as client from 'openid-client'
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
import { usher } from '@/lib/usher'

export async function GET(request: Request) {
  const config = await usher()
  const jar = await cookies()
  const flow = JSON.parse(jar.get('usher_flow')?.value ?? '{}')
  jar.delete('usher_flow')

  const tokens = await client.authorizationCodeGrant(config, new URL(request.url), {
    pkceCodeVerifier: flow.codeVerifier,
    expectedState: flow.state,
  })
  const claims = tokens.claims()! // sub, email, name, …

  // start your own session here: store claims.sub, and keep the refresh token server-side
  redirect('/')
}

Đăng xuất

Kết thúc phiên của người dùng trong ứng dụng của bạn, rồi đưa họ đến endpoint kết thúc phiên của Usher để Lobby cũng quên họ.

app/auth/sign-out/route.ts
import * as client from 'openid-client'
import { redirect } from 'next/navigation'
import { usher } from '@/lib/usher'

export async function GET() {
  const config = await usher()
  // clear your own session first
  const url = client.buildEndSessionUrl(config, {
    post_logout_redirect_uri: process.env.APP_URL!,
  })
  redirect(url.href)
}

Với SDK

Khi @usher/nextjs được phát hành, cùng ứng dụng đó sẽ trông như thế này. Bản xem trước dưới đây có thể thay đổi trước khi phát hành.

app/layout.tsx · app/page.tsx (xem trước)
// app/layout.tsx
import { UsherProvider } from '@usher/nextjs'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <UsherProvider>{children}</UsherProvider>
      </body>
    </html>
  )
}

// app/page.tsx
import { SignIn, UserButton, currentUser } from '@usher/nextjs'

export default async function Page() {
  const user = await currentUser()
  return user ? <UserButton /> : <SignIn />
}

Bước tiếp theo