Skip to content

Commit

Permalink
🐛 fix: fix OAuth don't get user id from session (lobehub#1347)
Browse files Browse the repository at this point in the history
* ⬆️ chore: update next-auth to 5.0.0-beta.13

- ⬆️ chore: update @auth/core to ^0.27.0

* 🐛 fix: Could not get `user_id` from session

* ⬆️ chore: Unlock next-auth to `beta`

* 📝 docs: Add custom session guide

* 🐛 fix: use `user_id` from next-auth
  • Loading branch information
cy948 authored Feb 23, 2024
1 parent 6485db7 commit ce4d6ca
Show file tree
Hide file tree
Showing 4 changed files with 247 additions and 2 deletions.
108 changes: 108 additions & 0 deletions contributing/Basic/Add-New-Authentication-Providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ LobeChat uses [Auth.js v5](https://authjs.dev/) as the external authentication s
- [Step 2: Update Server Configuration Code](#step-2-update-server-configuration-code)
- [Step 3: Change Frontend Pages](#step-3-change-frontend-pages)
- [Step 4: Configure the Environment Variables](#step-4-configure-the-environment-variables)
- [Step 5: Modify server-side user information processing logic](#step-5-modify-server-side-user-information-processing-logic)

## Add New Authentication Provider

Expand Down Expand Up @@ -82,4 +83,111 @@ This value is the id of the Auth.js provider, and you can read the source code o

Add `OKTA_CLIENT_ID``OKTA_CLIENT_SECRET``OKTA_ISSUER` environment variables when you deploy.

### Step 5: Modify server-side user information processing logic

#### Get user information in the frontend

Use the `useOAuthSession()` method in the frontend page to get the user information `user` returned by the backend:

```ts
import { useOAuthSession } from '@/hooks/useOAuthSession';

const { user, isOAuthLoggedIn } = useOAuthSession();
```

The default type of `user` is `User`, and the type definition is:

```ts
interface User {
id?: string;
name?: string | null;
email?: string | null;
image?: string | null;
}
```

#### Modify user `id` handling logic

The `user.id` is used to identify users. When introducing a new OAuth identity provider, you need to handle the information carried in the OAuth callback in `src/app/api/auth/next-auth.ts`. You need to select the user's `id` from this information. Before that, we need to understand the data processing sequence of `Auth.js`:

```txt
authorize --> jwt --> session
```

By default, in the `jwt --> session` process, `Auth.js` will [automatically assign the user `id` to `account.providerAccountId` based on the login type](https://authjs.dev/reference/core/types#provideraccountid). If you need to select a different value as the user `id`, you need to implement the following handling logic:

```ts
callbacks: {
async jwt({ token, account, profile }) {
if (account) {
// You can select a different value from `account` or `profile`
token.userId = account.providerAccountId;
}
return token;
},
},
```

#### Customize `session` return

If you want to carry more information about `profile` and `account` in the `session`, according to the data processing order mentioned above in `Auth.js`, you must first copy this information to the `token`. For example, add the user avatar URL `profile.picture` to the `session`:

```diff
callbacks: {
async jwt({ token, profile, account }) {
if (profile && account) {
token.userId = account.providerAccountId;
+ token.avatar = profile.picture;
}
return token;
},
async session({ session, token }) {
if (session.user) {
session.user.id = token.userId ?? session.user.id;
+ session.user.avatar = token.avatar;
}
return session;
},
},
```

Then supplement the type definition for the new parameters:

```ts
declare module '@auth/core/jwt' {
interface JWT {
// ...
avatar?: string;
}
}

declare module 'next-auth' {
interface User {
avatar?: string;
}
}
```

> [More built-in type extensions in Auth.js](https://authjs.dev/getting-started/typescript#module-augmentation)
#### Differentiate multiple authentication providers in the processing logic

If you have configured multiple authentication providers and their `userId` mappings are different, you can use the `account.provider` parameter in the `jwt` method to get the default id of the identity provider and enter different processing logic.

```ts
callbacks: {
async jwt({ token, profile, account }) {
if (profile && account) {
if (account.provider === 'authing')
token.userId = account.providerAccountId ?? token.sub;
else if (acount.provider === 'auth0')
token.userId = profile.sub ?? token.sub;
else
// other providers
}
return token;
},
}
```

Now, you can use Okta as your provider to implement the authentication feature in LobeChat.
109 changes: 109 additions & 0 deletions contributing/Basic/Add-New-Authentication-Providers.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ LobeChat 使用 [Auth.js v5](https://authjs.dev/) 作为外部身份验证服务
- [步骤 2: 更新服务端配置代码](#步骤-2-更新服务端配置代码)
- [步骤 3: 修改前端页面](#步骤-3-修改前端页面)
- [步骤 4: 配置环境变量](#步骤-4-配置环境变量)
- [步骤 5: 修改服务端用户信息处理逻辑](#步骤-5-修改服务端用户信息处理逻辑)

## 添加新的身份验证提供者

Expand Down Expand Up @@ -81,3 +82,111 @@ export const getAppConfig = () => {
### 步骤 4: 配置环境变量

在部署时新增 Okta 相关的环境变量 `OKTA_CLIENT_ID``OKTA_CLIENT_SECRET``OKTA_ISSUER`,并填入相应的值,即可使用

### 步骤 5: 修改服务端用户信息处理逻辑

#### 在前端获取用户信息

在前端页面中使用 `useOAuthSession()` 方法获取后端返回的用户信息 `user`

```ts
import { useOAuthSession } from '@/hooks/useOAuthSession';

const { user, isOAuthLoggedIn } = useOAuthSession();
```

默认的 `user` 类型为 `User`,类型定义为:

```ts
interface User {
id?: string;
name?: string | null;
email?: string | null;
image?: string | null;
}
```

#### 修改用户 `id` 处理逻辑

`user.id` 用于标识用户。当引入新身份 OAuth 提供者后,您需要在 `src/app/api/auth/next-auth.ts` 中处理 OAuth 回调所携带的信息。您需要从中选取用户的 `id`。在此之前,我们需要了解 `Auth.js` 的数据处理顺序:

```txt
authorize --> jwt --> session
```

默认情况下,在 `jwt --> session` 过程中,`Auth.js`[自动根据登陆类型](https://authjs.dev/reference/core/types#provideraccountid)将用户 `id` 赋值到 `account.providerAccountId` 中。 如果您需要选取其他值作为用户 `id` ,您需要实现以下处理逻辑。

```ts
callbacks: {
async jwt({ token, account, profile }) {
if (account) {
// 您可以从 `account` 或 `profile` 中选取其他值
token.userId = account.providerAccountId;
}
return token;
},
},
```

#### 自定义 `session` 返回

如果您想在 `session` 中携带更多关于 `profile``account` 的信息,根据上面提到的 `Auth.js` 数据处理顺序,那必须先将该信息复制到 `token` 上。
示例:把用户头像 URL:`profile.picture` 添加到`session` 中:

```diff
callbacks: {
async jwt({ token, profile, account }) {
if (profile && account) {
token.userId = account.providerAccountId;
+ token.avatar = profile.picture;
}
return token;
},
async session({ session, token }) {
if (session.user) {
session.user.id = token.userId ?? session.user.id;
+ session.user.avatar = token.avatar;
}
return session;
},
},
```

然后补充对新增参数的类型定义:

```ts
declare module '@auth/core/jwt' {
interface JWT {
// ...
avatar?: string;
}
}

declare module 'next-auth' {
interface User {
avatar?: string;
}
}
```

> [更多`Auth.js`内置类型拓展](https://authjs.dev/getting-started/typescript#module-augmentation)
#### 在处理逻辑中区分多个身份验证提供者

如果您配置了多个身份验证提供者,并且他们的 `userId` 映射各不相同,可以在 `jwt` 方法中的 `account.provider` 参数获取身份提供者的默认 id ,从而进入不同的处理逻辑。

```ts
callbacks: {
async jwt({ token, profile, account }) {
if (profile && account) {
if (account.provider === 'Authing')
token.userId = account.providerAccountId ?? token.sub;
else if (acount.provider === 'Okta')
token.userId = profile.sub ?? token.sub;
else
// other providers
}
return token;
},
}
```
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
},
"dependencies": {
"@ant-design/icons": "^5",
"@auth/core": "^0.26.3",
"@auth/core": "^0.27.0",
"@aws-sdk/client-bedrock-runtime": "^3.503.1",
"@azure/openai": "^1.0.0-beta.11",
"@cfworker/json-schema": "^1",
Expand Down Expand Up @@ -107,7 +107,7 @@
"modern-screenshot": "^4",
"nanoid": "^5",
"next": "^14.1",
"next-auth": "5.0.0-beta.11",
"next-auth": "beta",
"next-sitemap": "^4.2.3",
"numeral": "^2.0.6",
"nuqs": "^1.15.4",
Expand Down
28 changes: 28 additions & 0 deletions src/app/api/auth/next-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,38 @@ import { getServerConfig } from '@/config/server';
const { AUTH0_CLIENT_ID, ENABLE_OAUTH_SSO, AUTH0_CLIENT_SECRET, AUTH0_ISSUER, NEXTAUTH_SECRET } =
getServerConfig();

declare module '@auth/core/jwt' {
// Returned by the `jwt` callback and `auth`, when using JWT sessions
interface JWT {
userId?: string;
}
}

const nextAuth = NextAuth({
callbacks: {
// Note: Data processing order of callback: authorize --> jwt --> session
async jwt({ token, account }) {
// Auth.js will process the `providerAccountId` automatically
// ref: https://authjs.dev/reference/core/types#provideraccountid
if (account) {
token.userId = account.providerAccountId;
}
return token;
},
async session({ session, token }) {
// Pick userid from token
if (session.user) {
session.user.id = token.userId ?? session.user.id;
}
return session;
},
},
providers: ENABLE_OAUTH_SSO
? [
Auth0({
// Specify auth scope, at least include 'openid email'
// all scopes in Auth0 ref: https://auth0.com/docs/get-started/apis/scopes/openid-connect-scopes#standard-claims
authorization: { params: { scope: 'openid email profile' } },
clientId: AUTH0_CLIENT_ID,
clientSecret: AUTH0_CLIENT_SECRET,
issuer: AUTH0_ISSUER,
Expand Down

0 comments on commit ce4d6ca

Please sign in to comment.