Skip to main content
Glama
larksuite

Feishu/Lark OpenAPI MCP

Official
by larksuite
oidc.ts9.89 kB
import { Response } from 'express'; import { OAuthRegisteredClientsStore } from '@modelcontextprotocol/sdk/server/auth/clients.js'; import { OAuthClientInformationFull, OAuthTokens } from '@modelcontextprotocol/sdk/shared/auth.js'; import { AuthorizationParams, OAuthServerProvider } from '@modelcontextprotocol/sdk/server/auth/provider.js'; import { AuthInfo } from '@modelcontextprotocol/sdk/server/auth/types.js'; import { authStore } from '../store'; import { isTokenValid } from '../utils/is-token-valid'; import { generateCodeChallenge } from '../utils/pkce'; import { z } from 'zod'; import { LarkProxyOAuthServerProviderOptions } from './types'; import { commonHttpInstance } from '../../utils/http-instance'; import { logger } from '../../utils/logger'; const LarkOIDCTokenSchema = z.object({ code: z.number(), msg: z.string().optional(), data: z.object({ access_token: z.string(), token_type: z.string(), refresh_token: z.string().optional(), expires_in: z.number().optional(), refresh_expires_in: z.number().optional(), scope: z.string().optional(), }), }); interface OAuth2OAuthEndpoints { appAccessTokenUrl: string; authorizationUrl: string; tokenUrl: string; refreshTokenUrl: string; registrationUrl: string; } export class LarkOIDC2OAuthServerProvider implements OAuthServerProvider { private readonly _endpoints: OAuth2OAuthEndpoints; private readonly _options: LarkProxyOAuthServerProviderOptions; skipLocalPkceValidation = true; constructor(options: LarkProxyOAuthServerProviderOptions) { const { domain } = options; this._endpoints = { appAccessTokenUrl: `${domain}/open-apis/auth/v3/app_access_token/internal`, authorizationUrl: `${domain}/open-apis/authen/v1/index`, tokenUrl: `${domain}/open-apis/authen/v1/oidc/access_token`, refreshTokenUrl: `${domain}/open-apis/authen/v1/oidc/refresh_access_token`, registrationUrl: `${domain}/open-apis/authen/v1/index`, }; this._options = options; } get clientsStore(): OAuthRegisteredClientsStore { return authStore; } async authorize(client: OAuthClientInformationFull, params: AuthorizationParams, res: Response): Promise<void> { const targetUrl = new URL(this._endpoints.authorizationUrl); const searchParams = new URLSearchParams({ app_id: this._options.appId, redirect_uri: this._options.callbackUrl + '?redirect_uri=' + client.redirect_uris[0], }); if (params.state) { searchParams.set('state', params.state); } if (params.codeChallenge) { authStore.storeCodeVerifier(`challenge_${client.client_id}`, params.codeChallenge); } targetUrl.search = searchParams.toString(); logger.info(`[LarkOIDC2OAuthServerProvider] Redirecting to authorization URL: ${targetUrl.toString()}`); res.redirect(targetUrl.toString()); } async challengeForAuthorizationCode( _client: OAuthClientInformationFull, _authorizationCode: string, ): Promise<string> { return ''; } async exchangeAuthorizationCode( client: OAuthClientInformationFull, authorizationCode: string, codeVerifier?: string, _redirectUri?: string, ): Promise<OAuthTokens> { if (codeVerifier) { const storedChallenge = authStore.getCodeVerifier(`challenge_${client.client_id}`); if (!storedChallenge) { logger.error( `[LarkOIDC2OAuthServerProvider] exchangeAuthorizationCode: PKCE validation failed: code challenge not found`, ); throw new Error('PKCE validation failed: code challenge not found'); } const expectedChallenge = generateCodeChallenge(codeVerifier); if (expectedChallenge !== storedChallenge) { logger.error( `[LarkOIDC2OAuthServerProvider] exchangeAuthorizationCode: PKCE validation failed: code verifier does not match challenge`, ); throw new Error('PKCE validation failed: code verifier does not match challenge'); } authStore.removeCodeVerifier(`challenge_${client.client_id}`); } const params: Record<string, string> = { grant_type: 'authorization_code', code: authorizationCode, }; try { logger.info( `[LarkOIDC2OAuthServerProvider] Exchanging authorization code for client ${client.client_id}; appId: ${this._options.appId}`, ); const appAccessTokenResponse = await commonHttpInstance.post( this._endpoints.appAccessTokenUrl, { app_id: this._options.appId, app_secret: this._options.appSecret }, { headers: { 'Content-Type': 'application/json; charset=utf-8' } }, ); const { app_access_token: appAccessToken } = appAccessTokenResponse.data; const response = await commonHttpInstance.post(this._endpoints.tokenUrl, params, { headers: { 'Content-Type': 'application/json; charset=utf-8', Authorization: `Bearer ${appAccessToken}` }, }); const data = response.data; const parseResult = LarkOIDCTokenSchema.safeParse(data); if (!parseResult.success) { throw new Error(`Token parse failed: invalid response: ${data?.code}, ${data?.msg}`); } const token = parseResult.data; const expiresAt = token.data.expires_in ? token.data.expires_in + Date.now() / 1000 : undefined; await authStore.storeToken({ clientId: client.client_id, token: token.data.access_token, scopes: token.data.scope?.split(' ') || [], expiresAt, extra: { refreshToken: token.data.refresh_token, token, appId: this._options.appId, appSecret: this._options.appSecret, }, }); logger.info( `[LarkOIDC2OAuthServerProvider] Successfully exchanged authorization code for client ${client.client_id}; appId: ${this._options.appId}; token: ${Boolean(token.data.access_token)}; refreshToken: ${Boolean(token.data.refresh_token)};expiresAt: ${expiresAt}`, ); return { access_token: token.data.access_token, token_type: token.data.token_type, expires_in: token.data.expires_in, scope: token.data.scope, refresh_token: token.data.refresh_token, }; } catch (error: any) { logger.error( `[LarkOIDC2OAuthServerProvider] exchangeAuthorizationCode: Token exchange failed: ${error.response?.status || error.status} ${error.response?.data || error.message}`, ); throw new Error( `Token exchange failed: ${error.response?.status || error.status} ${error.response?.data || error.message}`, ); } } async exchangeRefreshToken( client: OAuthClientInformationFull, refreshToken: string, _scopes?: string[], ): Promise<OAuthTokens> { const originalToken = await authStore.getTokenByRefreshToken(refreshToken); if (!originalToken) { logger.error(`[LarkOIDC2OAuthServerProvider] exchangeRefreshToken: Refresh token is invalid`); throw new Error('refresh token is invalid'); } const appId = (originalToken.extra?.app_id as string) || this._options.appId; const appSecret = (originalToken.extra?.app_secret as string) || this._options.appSecret; try { logger.info(`[LarkOIDC2OAuthServerProvider] Refreshing token for client ${client.client_id}`); const appAccessTokenResponse = await commonHttpInstance.post( this._endpoints.appAccessTokenUrl, { app_id: appId, app_secret: appSecret }, { headers: { 'Content-Type': 'application/json; charset=utf-8' } }, ); const { app_access_token: appAccessToken } = appAccessTokenResponse.data; const response = await commonHttpInstance.post( this._endpoints.refreshTokenUrl, { grant_type: 'refresh_token', refresh_token: refreshToken }, { headers: { 'Content-Type': 'application/json; charset=utf-8', Authorization: `Bearer ${appAccessToken}` } }, ); const data = response.data; const parseResult = LarkOIDCTokenSchema.safeParse(data); if (!parseResult.success) { throw new Error(`Token parse failed: invalid response: ${data?.code}, ${data?.msg}`); } const token = parseResult.data; const expiresAt = token.data.expires_in ? token.data.expires_in + Date.now() / 1000 : undefined; await authStore.storeToken({ clientId: client.client_id, token: token.data.access_token, scopes: token.data.scope?.split(' ') || [], expiresAt, extra: { refreshToken: token.data.refresh_token, token, appId, appSecret }, }); logger.info( `[LarkOIDC2OAuthServerProvider] Successfully refreshed token for client ${client.client_id}; appId: ${appId}; token: ${Boolean(token.data.access_token)}; refreshToken: ${Boolean(token.data.refresh_token)};expiresAt: ${expiresAt}`, ); return { access_token: token.data.access_token, token_type: token.data.token_type, expires_in: token.data.expires_in, scope: token.data.scope, refresh_token: token.data.refresh_token, }; } catch (error: any) { logger.error( `[LarkOIDC2OAuthServerProvider] exchangeRefreshToken: Token refresh failed: ${error.response?.status || error.status} ${error.response?.data || error.message}`, ); throw new Error( `Token refresh failed: ${error.response?.status || error.status} ${error.response?.data || error.message}`, ); } } async verifyAccessToken(token: string): Promise<AuthInfo> { const { valid, token: storedToken } = await isTokenValid(token); if (!valid) { return { token: storedToken?.token || '', clientId: storedToken?.clientId || '', scopes: storedToken?.scopes || [], expiresAt: storedToken?.expiresAt || 1, extra: storedToken?.extra || {}, }; } return storedToken!; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/larksuite/lark-openapi-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server