Skip to content

feat(providers): add Feishu OAuth provider integration #12967

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions docs/public/img/providers/feishu.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
269 changes: 269 additions & 0 deletions packages/core/src/providers/feishu.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
/**
* <div class="provider" style={{backgroundColor: "#3370ff", display: "flex", justifyContent: "space-between", color: "#fff", padding: 16}}>
* <span>Built-in <b>Feishu</b> integration.</span>
* <a href="https://www.feishu.cn">
* <img style={{display: "block"}} src="https://authjs.dev/img/providers/feishu.svg" height="48" width="48"/>
* </a>
* </div>
*
* @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<FeishuProfile> {
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<FeishuProfile>{
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,
};
}