diff --git a/docs/public/img/providers/feishu.svg b/docs/public/img/providers/feishu.svg new file mode 100644 index 0000000000..75ef59420b --- /dev/null +++ b/docs/public/img/providers/feishu.svg @@ -0,0 +1,121 @@ + + + + + + + + + \ No newline at end of file diff --git a/packages/core/src/providers/feishu.ts b/packages/core/src/providers/feishu.ts new file mode 100644 index 0000000000..d021df073a --- /dev/null +++ b/packages/core/src/providers/feishu.ts @@ -0,0 +1,269 @@ +/** + *
+ * Built-in Feishu integration. + * + * + * + *
+ * + * @module providers/feishu + */ +import type { OAuthConfig, OAuthUserConfig } from "./index.js"; +/** + * The Feishu profile returned from the API + * + * @see https://open.feishu.cn/document/server-docs/authentication-management/login-state-management/get + */ +export interface FeishuProfile { + /** The user's display name */ + name: string; + /** The user's English name */ + en_name: string; + /** The user's avatar URLs */ + avatar_url: string; + avatar_thumb: string; + avatar_middle: string; + avatar_big: string; + /** The user's Feishu IDs */ + open_id: string; + union_id: string; + /** The user's email address */ + email: string; + /** The user's enterprise email address */ + enterprise_email: string; + /** The user's unique ID */ + user_id: string; + /** The user's mobile phone number */ + mobile: string; + /** The tenant key */ + tenant_key: string; + /** The user's employee number */ + employee_no: string; +} + +export interface FeishuOptions extends OAuthUserConfig { + callbackUrl: string; +} + +/** + * The parameters for the token request + * + * @see https://open.feishu.cn/document/authentication-management/access-token/obtain-oauth-code + */ +interface TokenRequestParams { + /** The authorization code received from the authorization endpoint */ + code: string; + /** The client ID of the OAuth application */ + client_id: string; + /** The client secret of the OAuth application */ + client_secret: string; + /** The redirect URI used in the authorization request */ + redirect_uri: string; + /** The code verifier used for PKCE (Proof Key for Code Exchange) */ + code_verifier: string; + /** The grant type, typically "authorization_code" */ + grant_type: string; + /** The scope of the OAuth application */ + scope: string; +} + +/** + * The response from the token endpoint + * + * @see https://open.feishu.cn/document/authentication-management/access-token/get-user-access-token + */ +interface TokenResponse { + /** Error code, 0 indicates success, non-zero indicates failure */ + code?: number; + /** User access token, only returned on success */ + access_token?: string; + /** Access token expiration time in seconds, only returned on success */ + expires_in?: number; + /** Refresh token, only returned on success and when offline_access is authorized */ + refresh_token?: string; + /** Refresh token expiration time in seconds, only returned with refresh_token */ + refresh_token_expires_in?: number; + /** Token type, always "Bearer" on success */ + token_type?: string; + /** List of permissions granted to the access token, only returned on success */ + scope?: string; + /** Error type, only returned on failure */ + error?: string; + /** Detailed error message, only returned on failure */ + error_description?: string; +} + +/** + * Add Feishu login to your page and make requests to [Feishu APIs](https://open.feishu.cn/document/sso/web-application-sso/login-overview). + * + * ### Setup + * + * #### Callback URL + * ``` + * https://example.com/api/auth/callback/feishu + * ``` + * + * #### Configuration + * ```ts + * import NextAuth from "next-auth"; + * import Feishu from "@auth/core/providers/feishu"; + * + * declare module "next-auth" { + * interface Session { + * accessToken?: string; + * } + * } + * + * export const { handlers, signIn, signOut, auth } = NextAuth({ + * providers: [ + * Feishu({ + * clientId: process.env.FEISHU_CLIENT_ID!, + * clientSecret: process.env.FEISHU_CLIENT_SECRET!, + * callbackUrl: `${process.env.NEXTAUTH_URL}/api/auth/callback/feishu`, + * }), + * ], + * }); + * ``` + * + * ### Resources + * + * - [Feishu - Creating an OAuth App](https://open.feishu.cn/document/sso/web-application-sso/login-overview) + * - [Feishu - Authorizing OAuth Apps](https://open.feishu.cn/document/authentication-management/access-token/obtain-oauth-code) + * - [Feishu - Configure your Feishu OAuth Apps](https://open.feishu.cn/app) + * - [Learn more about OAuth](https://authjs.dev/concepts/oauth) + * - [Source code](https://github.com/nextauthjs/next-auth/blob/main/packages/core/src/providers/feishu.ts) + * + * ### Notes + * + * By default, Auth.js assumes that the Feishu provider is + * based on the [OAuth 2](https://www.rfc-editor.org/rfc/rfc6749.html) specification. + * + * :::tip + * + * The Feishu provider comes with a [default configuration](https://github.com/nextauthjs/next-auth/blob/main/packages/core/src/providers/feishu.ts). + * To override the defaults for your use case, check out [customizing a built-in OAuth provider](https://authjs.dev/guides/configuring-oauth-providers). + * + * ::: + * + * :::info **Disclaimer** + * + * If you think you found a bug in the default configuration, you can [open an issue](https://authjs.dev/new/provider-issue). + * + * Auth.js strictly adheres to the specification and it cannot take responsibility for any deviation from + * the spec by the provider. You can open an issue, but if the problem is non-compliance with the spec, + * we might not pursue a resolution. You can ask for more help in [Discussions](https://authjs.dev/new/github-discussions). + * + * ::: + */ + +export default function Feishu(options: FeishuOptions): OAuthConfig{ + return { + id: "feishu", + name: "Feishu", + type: "oauth", + authorization: { + url: "https://accounts.feishu.cn/open-apis/authen/v1/authorize", + params: { + client_id: options.clientId, + response_type: "code", + state: "RANDOMSTRING", + scope: "", + }, + }, + token: { + url: "https://open.feishu.cn/open-apis/authen/v2/oauth/token", + async request({ params }: { params: TokenRequestParams }) { + const payload = { + grant_type: "authorization_code", + code: params.code, + client_id: options.clientId, + client_secret: options.clientSecret, + redirect_uri: options.callbackUrl, + code_verifier: params.code_verifier, + scope: params.scope, + }; + + const response = await fetch("https://open.feishu.cn/open-apis/authen/v2/oauth/token", { + method: "POST", + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + body: JSON.stringify(payload), + }); + + const data = await response.json(); + + if (data.code !== 0 || data.error) { + throw new Error( + data.error_description || + data.error || + `Failed to get access token: ${JSON.stringify(data)}`, + ); + } + + return { + tokens: { + access_token: data.access_token, + token_type: data.token_type || "Bearer", + expires_in: data.expires_in, + refresh_token: data.refresh_token, + scope: data.scope, + }, + }; + }, + }, + userinfo: { + url: "https://open.feishu.cn/open-apis/authen/v1/user_info", + async request({ tokens }: { tokens: TokenResponse }) { + const response = await fetch("https://open.feishu.cn/open-apis/authen/v1/user_info", { + method: "GET", + headers: { + Authorization: `Bearer ${tokens.access_token}`, + }, + }); + + const data = await response.json(); + + if (data.code !== 0) { + throw new Error(`Failed to get user info: ${data.msg || JSON.stringify(data)}`); + } + + return { + name: data.data.name, + en_name: data.data.en_name, + avatar_url: data.data.avatar_url, + avatar_thumb: data.data.avatar_thumb, + avatar_middle: data.data.avatar_middle, + avatar_big: data.data.avatar_big, + open_id: data.data.open_id, + union_id: data.data.union_id, + email: data.data.email, + enterprise_email: data.data.enterprise_email, + user_id: data.data.user_id, + mobile: data.data.mobile, + tenant_key: data.data.tenant_key, + employee_no: data.data.employee_no, + } satisfies FeishuProfile; + }, + }, + profile(profile: FeishuProfile) { + return { + name: profile.name, + en_name: profile.en_name, + avatar_url: profile.avatar_url, + avatar_thumb: profile.avatar_thumb, + avatar_middle: profile.avatar_middle, + avatar_big: profile.avatar_big, + open_id: profile.open_id, + union_id: profile.union_id, + email: profile.email, + enterprise_email: profile.enterprise_email, + user_id: profile.user_id, + mobile: profile.mobile, + tenant_key: profile.tenant_key, + employee_no: profile.employee_no, + }; + }, + options, + }; +}