From 3df69a4ed5029928f4514d4e073f4280f324e033 Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Fri, 19 Apr 2024 16:36:42 +0800 Subject: [PATCH] feat(connector): support `client_secret_basic` and `client_secret_jwt` methods for oauth2 connectors --- .changeset/chilled-pugs-notice.md | 6 + .github/workflows/main.yml | 4 + .github/workflows/upload-annotations.yml | 4 + .scripts/process-connectors.js | 9 + package.json | 2 +- .../connectors/connector-oauth2/README.md | 4 + .../connectors/connector-oauth2/package.json | 6 +- .../connector-oauth2/src/constant.ts | 52 +++++ .../connectors/connector-oauth2/src/index.ts | 44 ++--- .../connector-oauth2/src/oauth2/index.ts | 2 + .../connector-oauth2/src/oauth2/types.ts | 68 +++++++ .../connector-oauth2/src/oauth2/utils.test.ts | 183 ++++++++++++++++++ .../connector-oauth2/src/oauth2/utils.ts | 151 +++++++++++++++ .../connectors/connector-oauth2/src/types.ts | 32 +-- .../connectors/connector-oauth2/src/utils.ts | 99 +++++----- packages/connectors/connector-oidc/README.md | 4 + .../connectors/connector-oidc/package.json | 5 +- .../connectors/connector-oidc/src/constant.ts | 51 +++++ .../connectors/connector-oidc/src/index.ts | 45 +++-- .../connectors/connector-oidc/src/types.ts | 21 +- .../connectors/connector-oidc/src/utils.ts | 76 ++++---- packages/core/jest.config.js | 2 + packages/core/nodemon.json | 3 +- packages/shared/package.json | 1 - pnpm-lock.yaml | 40 ++-- 25 files changed, 713 insertions(+), 201 deletions(-) create mode 100644 .changeset/chilled-pugs-notice.md create mode 100644 .scripts/process-connectors.js create mode 100644 packages/connectors/connector-oauth2/src/oauth2/index.ts create mode 100644 packages/connectors/connector-oauth2/src/oauth2/types.ts create mode 100644 packages/connectors/connector-oauth2/src/oauth2/utils.test.ts create mode 100644 packages/connectors/connector-oauth2/src/oauth2/utils.ts diff --git a/.changeset/chilled-pugs-notice.md b/.changeset/chilled-pugs-notice.md new file mode 100644 index 000000000000..06b33009b6f6 --- /dev/null +++ b/.changeset/chilled-pugs-notice.md @@ -0,0 +1,6 @@ +--- +"@logto/connector-oauth": minor +"@logto/connector-oidc": minor +--- + +Support `client_secret_basic` and `client_secret_jwt` token endpoint auth method for oauth & oidc connectors diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a0af63a96556..cf4bbafcb659 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -36,6 +36,10 @@ jobs: - name: Prepack run: pnpm prepack + # Build connectors before running lint since some connectors rely on the generated types + - name: Build connectors + run: pnpm connectors build + - name: Lint run: pnpm ci:lint diff --git a/.github/workflows/upload-annotations.yml b/.github/workflows/upload-annotations.yml index a7d48c5ce806..f4ec39c74789 100644 --- a/.github/workflows/upload-annotations.yml +++ b/.github/workflows/upload-annotations.yml @@ -27,6 +27,10 @@ jobs: - name: Prepack run: pnpm prepack + # Build connectors before running lint since some connectors rely on the generated types + - name: Build connectors + run: pnpm connectors build + - name: Lint with Report run: pnpm -r --parallel lint:report && node .scripts/merge-eslint-reports.js diff --git a/.scripts/process-connectors.js b/.scripts/process-connectors.js new file mode 100644 index 000000000000..0a30d3ec8bd1 --- /dev/null +++ b/.scripts/process-connectors.js @@ -0,0 +1,9 @@ +import { execSync } from "child_process"; + +const action = process.argv[2]; // Read the action from the command line arguments + +// Handle connector-oauth2 package first since other packages may depend on it +execSync( + `pnpm --dir packages/connectors/connector-oauth2 ${action} && pnpm -r --filter "./packages/connectors/connector-*" --filter "!./packages/connectors/connector-oauth2" ${action}`, + { stdio: "inherit" }, // 'inherit' will use the same console as the parent process +); diff --git a/package.json b/package.json index 18dd65f2c138..ccbff476b4ee 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "cli": "logto", "changeset": "changeset", "alteration": "logto db alt", - "connectors": "pnpm -r --filter \"./packages/connectors/connector-*\"", + "connectors": "node ./.scripts/process-connectors.js", "//": "# `changeset version` won't run version lifecycle scripts, see https://github.com/changesets/changesets/issues/860", "ci:version": "changeset version && pnpm -r version", "ci:build": "pnpm -r build", diff --git a/packages/connectors/connector-oauth2/README.md b/packages/connectors/connector-oauth2/README.md index 9098feaa73e7..16263a1fd760 100644 --- a/packages/connectors/connector-oauth2/README.md +++ b/packages/connectors/connector-oauth2/README.md @@ -24,6 +24,10 @@ We ONLY support "Authorization Code" grant type for security consideration and i *clientSecret*: The client secret is a confidential key that is issued to the client application by the authorization server during registration. The client application uses this secret key to authenticate itself with the authorization server when requesting access tokens. The client secret is considered confidential information and should be kept secure at all times. +*tokenEndpointAuthMethod*: The token endpoint authentication method is used by the client application to authenticate itself with the authorization server when requesting access tokens. To discover supported methods, consult the `token_endpoint_auth_methods_supported` field available at the OAuth 2.0 service provider’s OpenID Connect discovery endpoint, or refer to the relevant documentation provided by the OAuth 2.0 service provider. + +*clientSecretJwtSigningAlgorithm (Optional)*: Only required when `tokenEndpointAuthMethod` is `client_secret_jwt`. The client secret JWT signing algorithm is used by the client application to sign the JWT that is sent to the authorization server during the token request. + *scope*: The scope parameter is used to specify the set of resources and permissions that the client application is requesting access to. The scope parameter is typically defined as a space-separated list of values that represent specific permissions. For example, a scope value of "read write" might indicate that the client application is requesting read and write access to a user's data. You are expected to find `authorizationEndpoint`, `tokenEndpoint` and `userInfoEndpoint` in social vendor's documentation. diff --git a/packages/connectors/connector-oauth2/package.json b/packages/connectors/connector-oauth2/package.json index 9b62d82e9120..0112f67ec51a 100644 --- a/packages/connectors/connector-oauth2/package.json +++ b/packages/connectors/connector-oauth2/package.json @@ -5,8 +5,10 @@ "author": "Silverhand Inc. ", "dependencies": { "@logto/connector-kit": "workspace:^3.0.0", + "@logto/shared": "workspace:^3.1.0", "@silverhand/essentials": "^2.9.0", - "got": "^14.0.0", + "jose": "^5.0.0", + "ky": "^1.2.3", "query-string": "^9.0.0", "snakecase-keys": "^8.0.0", "zod": "^3.22.4" @@ -64,7 +66,7 @@ "@vitest/coverage-v8": "^1.4.0", "eslint": "^8.56.0", "lint-staged": "^15.0.2", - "nock": "^13.3.1", + "nock": "14.0.0-beta.6", "prettier": "^3.0.0", "rollup": "^4.12.0", "rollup-plugin-output-size": "^1.3.0", diff --git a/packages/connectors/connector-oauth2/src/constant.ts b/packages/connectors/connector-oauth2/src/constant.ts index 4624d338a5f3..6e7ec1d7d66c 100644 --- a/packages/connectors/connector-oauth2/src/constant.ts +++ b/packages/connectors/connector-oauth2/src/constant.ts @@ -1,6 +1,8 @@ import type { ConnectorMetadata } from '@logto/connector-kit'; import { ConnectorConfigFormItemType, ConnectorPlatform } from '@logto/connector-kit'; +import { ClientSecretJwtSigningAlgorithm, TokenEndpointAuthMethod } from './oauth2/types.js'; + export const defaultMetadata: ConnectorMetadata = { id: 'oauth2', target: 'oauth2', @@ -53,6 +55,56 @@ export const defaultMetadata: ConnectorMetadata = { required: true, placeholder: '', }, + { + key: 'tokenEndpointAuthMethod', + label: 'Token Endpoint Auth Method', + type: ConnectorConfigFormItemType.Select, + selectItems: [ + { + title: TokenEndpointAuthMethod.ClientSecretPost, + value: TokenEndpointAuthMethod.ClientSecretPost, + }, + { + title: TokenEndpointAuthMethod.ClientSecretBasic, + value: TokenEndpointAuthMethod.ClientSecretBasic, + }, + { + title: TokenEndpointAuthMethod.ClientSecretJwt, + value: TokenEndpointAuthMethod.ClientSecretJwt, + }, + ], + required: true, + defaultValue: TokenEndpointAuthMethod.ClientSecretPost, + description: 'The method used for client authentication at the token endpoint in OAuth 2.0.', + }, + { + key: 'clientSecretJwtSigningAlgorithm', + label: 'Client Secret JWT Signing Algorithm', + type: ConnectorConfigFormItemType.Select, + selectItems: [ + { + title: ClientSecretJwtSigningAlgorithm.HS256, + value: ClientSecretJwtSigningAlgorithm.HS256, + }, + { + title: ClientSecretJwtSigningAlgorithm.HS384, + value: ClientSecretJwtSigningAlgorithm.HS384, + }, + { + title: ClientSecretJwtSigningAlgorithm.HS512, + value: ClientSecretJwtSigningAlgorithm.HS512, + }, + ], + showConditions: [ + { + targetKey: 'tokenEndpointAuthMethod', + expectValue: TokenEndpointAuthMethod.ClientSecretJwt, + }, + ], + required: true, + defaultValue: ClientSecretJwtSigningAlgorithm.HS256, + description: 'The signing algorithm used for the client secret JWT.', + }, { key: 'tokenEndpointResponseType', label: 'Token Endpoint Response Type', diff --git a/packages/connectors/connector-oauth2/src/index.ts b/packages/connectors/connector-oauth2/src/index.ts index f2b1c274b1cd..6d07368306c8 100644 --- a/packages/connectors/connector-oauth2/src/index.ts +++ b/packages/connectors/connector-oauth2/src/index.ts @@ -1,6 +1,4 @@ import { assert, pick } from '@silverhand/essentials'; -import { got, HTTPError } from 'got'; -import snakecaseKeys from 'snakecase-keys'; import { type GetAuthorizationUri, @@ -14,45 +12,40 @@ import { validateConfig, ConnectorType, } from '@logto/connector-kit'; +import ky, { HTTPError } from 'ky'; import { defaultMetadata, defaultTimeout } from './constant.js'; -import { oauthConfigGuard } from './types.js'; +import { constructAuthorizationUri } from './oauth2/utils.js'; +import { oauth2ConnectorConfigGuard } from './types.js'; import { userProfileMapping, getAccessToken } from './utils.js'; -const removeUndefinedKeys = (object: Record) => - Object.fromEntries(Object.entries(object).filter(([, value]) => value !== undefined)); +export * from './oauth2/index.js'; const getAuthorizationUri = (getConfig: GetConnectorConfig): GetAuthorizationUri => async ({ state, redirectUri }, setSession) => { const config = await getConfig(defaultMetadata.id); - validateConfig(config, oauthConfigGuard); - const parsedConfig = oauthConfigGuard.parse(config); - - const { customConfig, ...rest } = parsedConfig; - - const parameterObject = snakecaseKeys({ - ...pick(rest, 'responseType', 'clientId', 'scope'), - ...customConfig, - }); + validateConfig(config, oauth2ConnectorConfigGuard); + const parsedConfig = oauth2ConnectorConfigGuard.parse(config); await setSession({ redirectUri }); - const queryParameters = new URLSearchParams({ - ...removeUndefinedKeys(parameterObject), + const { authorizationEndpoint, customConfig } = parsedConfig; + + return constructAuthorizationUri(authorizationEndpoint, { + ...pick(parsedConfig, 'responseType', 'clientId', 'scope'), + redirectUri, state, - redirect_uri: redirectUri, + ...customConfig, }); - - return `${parsedConfig.authorizationEndpoint}?${queryParameters.toString()}`; }; const getUserInfo = (getConfig: GetConnectorConfig): GetUserInfo => async (data, getSession) => { const config = await getConfig(defaultMetadata.id); - validateConfig(config, oauthConfigGuard); - const parsedConfig = oauthConfigGuard.parse(config); + validateConfig(config, oauth2ConnectorConfigGuard); + const parsedConfig = oauth2ConnectorConfigGuard.parse(config); const { redirectUri } = await getSession(); assert( @@ -65,13 +58,14 @@ const getUserInfo = const { access_token, token_type } = await getAccessToken(parsedConfig, data, redirectUri); try { - const httpResponse = await got.get(parsedConfig.userInfoEndpoint, { + const httpResponse = await ky.get(parsedConfig.userInfoEndpoint, { headers: { authorization: `${token_type} ${access_token}`, }, - timeout: { request: defaultTimeout }, + timeout: defaultTimeout, }); - const rawData = parseJsonObject(httpResponse.body); + + const rawData = parseJsonObject(await httpResponse.text()); return { ...userProfileMapping(rawData, parsedConfig.profileMap), rawData }; } catch (error: unknown) { @@ -87,7 +81,7 @@ const createOauthConnector: CreateConnector = async ({ getConfi return { metadata: defaultMetadata, type: ConnectorType.Social, - configGuard: oauthConfigGuard, + configGuard: oauth2ConnectorConfigGuard, getAuthorizationUri: getAuthorizationUri(getConfig), getUserInfo: getUserInfo(getConfig), }; diff --git a/packages/connectors/connector-oauth2/src/oauth2/index.ts b/packages/connectors/connector-oauth2/src/oauth2/index.ts new file mode 100644 index 000000000000..920534d03d41 --- /dev/null +++ b/packages/connectors/connector-oauth2/src/oauth2/index.ts @@ -0,0 +1,2 @@ +export * from './types.js'; +export * from './utils.js'; diff --git a/packages/connectors/connector-oauth2/src/oauth2/types.ts b/packages/connectors/connector-oauth2/src/oauth2/types.ts new file mode 100644 index 000000000000..6c9e8c47331e --- /dev/null +++ b/packages/connectors/connector-oauth2/src/oauth2/types.ts @@ -0,0 +1,68 @@ +import { z } from 'zod'; + +/** + * OAuth 2.0 Client Authentication methods that are used by Clients to authenticate to the Authorization Server when using the Token Endpoint. + */ +export enum TokenEndpointAuthMethod { + ClientSecretBasic = 'client_secret_basic', + ClientSecretPost = 'client_secret_post', + ClientSecretJwt = 'client_secret_jwt', +} + +/* + * Enumeration of algorithms supported for JWT signing when using client secrets. + * + * These "HS" algorithms (HMAC using SHA) are specifically chosen for scenarios where the + * client authentication method is 'client_secret_jwt'. HMAC algorithms utilize the + * client_secret as a shared symmetric key to generate a secure hash, ensuring the integrity + * and authenticity of the JWT. + * + * Other types of algorithms, such as RSASSA (RS256, RS384, RS512) or ECDSA (ES256, ES384, ES512), + * utilize asymmetric keys, are complex and requires secure key management infrastructure. + * + * In the 'client_secret_jwt' context, where simplicity and symmetric key usage are preferred for + * straightforward validation by the authorization server without the need to manage or distribute + * public keys, HMAC algorithms are more suitable. + */ +export enum ClientSecretJwtSigningAlgorithm { + /** HMAC using SHA-256 hash algorithm */ + HS256 = 'HS256', + /** HMAC using SHA-384 hash algorithm */ + HS384 = 'HS384', + /** HMAC using SHA-512 hash algorithm */ + HS512 = 'HS512', +} + +export const oauth2ConfigGuard = z.object({ + responseType: z.literal('code').optional().default('code'), + grantType: z.literal('authorization_code').optional().default('authorization_code'), + authorizationEndpoint: z.string(), + tokenEndpoint: z.string(), + clientId: z.string(), + clientSecret: z.string(), + tokenEndpointAuthMethod: z + .nativeEnum(TokenEndpointAuthMethod) + .optional() + .default(TokenEndpointAuthMethod.ClientSecretPost), + clientSecretJwtSigningAlgorithm: z + .nativeEnum(ClientSecretJwtSigningAlgorithm) + .optional() + .default(ClientSecretJwtSigningAlgorithm.HS256), + scope: z.string().optional(), +}); + +export const oauth2AuthResponseGuard = z.object({ + code: z.string(), + state: z.string().optional(), +}); + +export type Oauth2AuthResponse = z.infer; + +export const oauth2AccessTokenResponseGuard = z.object({ + access_token: z.string(), + token_type: z.string(), + expires_in: z.number().optional(), + refresh_token: z.string().optional(), +}); + +export type Oauth2AccessTokenResponse = z.infer; diff --git a/packages/connectors/connector-oauth2/src/oauth2/utils.test.ts b/packages/connectors/connector-oauth2/src/oauth2/utils.test.ts new file mode 100644 index 000000000000..9bde611fd21c --- /dev/null +++ b/packages/connectors/connector-oauth2/src/oauth2/utils.test.ts @@ -0,0 +1,183 @@ +import nock from 'nock'; + +import ky from 'ky'; + +import { ClientSecretJwtSigningAlgorithm, TokenEndpointAuthMethod } from './types.js'; +import { constructAuthorizationUri, type RequestTokenEndpointOptions } from './utils.js'; + +const kyPostMock = vi.spyOn(ky, 'post'); + +vi.mock('jose', () => ({ + SignJWT: vi.fn(() => ({ + setProtectedHeader: vi.fn().mockReturnThis(), + sign: vi.fn().mockResolvedValue('signed-jwt'), + })), +})); + +const { requestTokenEndpoint } = await import('./utils.js'); + +const tokenEndpointUrl = new URL('https://example.com/token'); + +describe('requestTokenEndpoint', () => { + beforeEach(() => { + nock(tokenEndpointUrl.origin) + .post(tokenEndpointUrl.pathname) + .query(true) + .reply( + 200, + JSON.stringify({ + access_token: 'access_token', + token_type: 'bearer', + }) + ); + }); + + afterEach(() => { + nock.cleanAll(); + }); + + beforeAll(() => { + vi.clearAllMocks(); + }); + + it('should handle TokenEndpointAuthMethod.ClientSecretJwt correctly', async () => { + const options: RequestTokenEndpointOptions = { + tokenEndpoint: 'https://example.com/token', + tokenEndpointAuthOptions: { + method: TokenEndpointAuthMethod.ClientSecretJwt, + clientSecretJwtSigningAlgorithm: ClientSecretJwtSigningAlgorithm.HS256, + }, + tokenRequestBody: { + grantType: 'authorization_code', + code: 'authcode123', + redirectUri: 'https://example.com/callback', + clientId: 'client123', + clientSecret: 'secret123', + extraParam: 'extra', + }, + timeout: 5000, + }; + + await requestTokenEndpoint(options); + expect(kyPostMock).toHaveBeenCalledWith(options.tokenEndpoint, { + body: new URLSearchParams({ + grant_type: 'authorization_code', + code: 'authcode123', + redirect_uri: 'https://example.com/callback', + extra_param: 'extra', + client_id: 'client123', + client_assertion: 'signed-jwt', + client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', + }), + timeout: 5000, + }); + }); + + it('should handle TokenEndpointAuthMethod.ClientSecretBasic correctly', async () => { + const options: RequestTokenEndpointOptions = { + tokenEndpoint: 'https://example.com/token', + tokenEndpointAuthOptions: { + method: TokenEndpointAuthMethod.ClientSecretBasic, + }, + tokenRequestBody: { + grantType: 'authorization_code', + code: 'authcode123', + redirectUri: 'https://example.com/callback', + clientId: 'client123', + clientSecret: 'secret123', + extraParam: 'extra', + }, + timeout: 5000, + }; + + await requestTokenEndpoint(options); + expect(kyPostMock).toHaveBeenCalledWith(options.tokenEndpoint, { + headers: { + Authorization: `Basic ${Buffer.from('client123:secret123').toString('base64')}`, + }, + body: new URLSearchParams({ + grant_type: 'authorization_code', + code: 'authcode123', + redirect_uri: 'https://example.com/callback', + extra_param: 'extra', + }), + timeout: 5000, + }); + }); + + it('should handle TokenEndpointAuthMethod.ClientSecretPost correctly', async () => { + const options: RequestTokenEndpointOptions = { + tokenEndpoint: 'https://example.com/token', + tokenEndpointAuthOptions: { + method: TokenEndpointAuthMethod.ClientSecretPost, + }, + tokenRequestBody: { + grantType: 'authorization_code', + code: 'authcode123', + redirectUri: 'https://example.com/callback', + clientId: 'client123', + clientSecret: 'secret123', + extraParam: 'extra', + }, + timeout: 5000, + }; + + await requestTokenEndpoint(options); + expect(kyPostMock).toHaveBeenCalledWith(options.tokenEndpoint, { + body: new URLSearchParams({ + grant_type: 'authorization_code', + code: 'authcode123', + redirect_uri: 'https://example.com/callback', + client_id: 'client123', + client_secret: 'secret123', + extra_param: 'extra', + }), + timeout: 5000, + }); + }); +}); + +describe('constructAuthorizationUri', () => { + it('constructs a valid authorization URL with all parameters', async () => { + const authorizationEndpoint = 'https://example.com/oauth/authorize'; + const queryParameters = { + responseType: 'code', + clientId: 'client123', + scope: 'openid email', + redirectUri: 'https://example.com/callback', + state: 'state123', + }; + + const expectedParams = new URLSearchParams({ + response_type: 'code', + client_id: 'client123', + scope: 'openid email', + redirect_uri: 'https://example.com/callback', + state: 'state123', + }).toString(); + + const result = constructAuthorizationUri(authorizationEndpoint, queryParameters); + expect(result).toBe(`${authorizationEndpoint}?${expectedParams}`); + }); + + it('omits undefined values from the constructed URL', async () => { + const authorizationEndpoint = 'https://example.com/oauth/authorize'; + const queryParameters = { + responseType: 'code', + clientId: 'client123', + redirectUri: 'https://example.com/callback', + state: 'state123', + scope: undefined, // This should not appear in the final URL + }; + + const expectedParams = new URLSearchParams({ + response_type: 'code', + client_id: 'client123', + redirect_uri: 'https://example.com/callback', + state: 'state123', + }).toString(); + + const result = constructAuthorizationUri(authorizationEndpoint, queryParameters); + expect(result).toBe(`${authorizationEndpoint}?${expectedParams}`); + }); +}); diff --git a/packages/connectors/connector-oauth2/src/oauth2/utils.ts b/packages/connectors/connector-oauth2/src/oauth2/utils.ts new file mode 100644 index 000000000000..51f87565f153 --- /dev/null +++ b/packages/connectors/connector-oauth2/src/oauth2/utils.ts @@ -0,0 +1,151 @@ +import { removeUndefinedKeys } from '@silverhand/essentials'; +import snakecaseKeys from 'snakecase-keys'; + +import { ConnectorError, ConnectorErrorCodes } from '@logto/connector-kit'; +import { generateStandardId } from '@logto/shared/universal'; +import { SignJWT } from 'jose'; +import ky, { HTTPError } from 'ky'; + +import { TokenEndpointAuthMethod } from './types.js'; + +type TokenEndpointAuthOptions = + T extends TokenEndpointAuthMethod.ClientSecretJwt + ? { + method: TokenEndpointAuthMethod.ClientSecretJwt; + clientSecretJwtSigningAlgorithm: string; + } + : { + method: + | TokenEndpointAuthMethod.ClientSecretBasic + | TokenEndpointAuthMethod.ClientSecretPost; + }; + +export type RequestTokenEndpointOptions = { + tokenEndpoint: string; + tokenEndpointAuthOptions: TokenEndpointAuthOptions; + tokenRequestBody: { + grantType: string; + code: string; + redirectUri: string; + clientId: string; + clientSecret: string; + } & Record; + timeout?: number; +}; + +/** + * Requests the token endpoint for an access token with given client authentication options. + * + * @param tokenEndpoint - The URL of the token endpoint. + * @param clientCredentials - The client credentials (client ID and client secret). + * @param tokenEndpointAuthOptions - The options for authenticating with the token endpoint. + * @param tokenEndpointAuthOptions.method - The method to use for authenticating with the token endpoint. + * @param tokenEndpointAuthOptions.clientSecretJwtSigningAlgorithm - The signing algorithm to use for the client secret JWT. Required if the `method` is `TokenEndpointAuthMethod.ClientSecretJwt`. + * @param tokenRequestBody - The request body to be sent as application/x-www-form-urlencoded to the token endpoint. Parameters are automatically converted to snake_case and undefined values are removed. + * @param timeout - The timeout for the request in milliseconds. + * @returns A Promise that resolves to the response from the token endpoint. + */ +export const requestTokenEndpoint = async ({ + tokenEndpoint, + tokenEndpointAuthOptions, + tokenRequestBody, + timeout, +}: RequestTokenEndpointOptions) => { + const postTokenEndpoint = async ({ + form, + headers, + }: { + form: Record; + headers?: Record; + }) => { + try { + return await ky.post(tokenEndpoint, { + headers, + body: new URLSearchParams(removeUndefinedKeys(snakecaseKeys(form))), + timeout, + }); + } catch (error: unknown) { + if (error instanceof HTTPError) { + throw new ConnectorError(ConnectorErrorCodes.General, JSON.stringify(error.response.body)); + } + + throw error; + } + }; + + const { clientId, clientSecret, ...requestBodyWithoutClientCredentials } = tokenRequestBody; + + switch (tokenEndpointAuthOptions.method) { + case TokenEndpointAuthMethod.ClientSecretJwt: { + const clientSecretJwt = await new SignJWT({ + iss: clientId, + sub: clientId, + aud: tokenEndpoint, + jti: generateStandardId(), + exp: Math.floor(Date.now() / 1000) + 600, // Expiration time is 10 minutes + iat: Math.floor(Date.now() / 1000), + }) + .setProtectedHeader({ + alg: tokenEndpointAuthOptions.clientSecretJwtSigningAlgorithm, + }) + .sign(Buffer.from(clientSecret)) + .catch((error: unknown) => { + if (error instanceof Error) { + throw new ConnectorError( + ConnectorErrorCodes.General, + 'Failed to sign client secret JWT' + ); + } + throw error; + }); + + return postTokenEndpoint({ + form: { + ...requestBodyWithoutClientCredentials, + clientId, + clientAssertion: clientSecretJwt, + /** + * `client_assertion_type` parameter MUST be "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" + * see https://datatracker.ietf.org/doc/html/rfc7523#section-2.2 + */ + clientAssertionType: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', + }, + }); + } + case TokenEndpointAuthMethod.ClientSecretBasic: { + return postTokenEndpoint({ + form: requestBodyWithoutClientCredentials, + headers: { + Authorization: `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`, + }, + }); + } + case TokenEndpointAuthMethod.ClientSecretPost: { + return postTokenEndpoint({ + form: tokenRequestBody, + }); + } + } +}; + +/** + * Constructs a complete URL for initiating OAuth authorization by appending properly formatted + * query parameters to the provided authorization endpoint URL. + * + * @param authorizationEndpoint The base URL to which the OAuth authorization request is sent. + * @param queryParameters An object containing OAuth specific parameters such as responseType, clientId, scope, redirectUri, and state. Additional custom parameters can also be included as needed. Parameters are automatically converted to snake_case and undefined values are removed. + * @returns A string representing the fully constructed URL to be used for OAuth authorization. + */ +export const constructAuthorizationUri = ( + authorizationEndpoint: string, + queryParameters: { + responseType: string; + clientId: string; + scope?: string; + redirectUri: string; + state: string; + } & Record +) => + `${authorizationEndpoint}?${new URLSearchParams( + removeUndefinedKeys(snakecaseKeys(queryParameters)) + ).toString()}`; diff --git a/packages/connectors/connector-oauth2/src/types.ts b/packages/connectors/connector-oauth2/src/types.ts index efae11b4c5f9..1aa04aaee272 100644 --- a/packages/connectors/connector-oauth2/src/types.ts +++ b/packages/connectors/connector-oauth2/src/types.ts @@ -1,5 +1,7 @@ import { z } from 'zod'; +import { oauth2ConfigGuard } from './oauth2/types.js'; + export const profileMapGuard = z .object({ id: z.string().optional().default('id'), @@ -36,35 +38,11 @@ const tokenEndpointResponseTypeGuard = z export type TokenEndpointResponseType = z.input; -export const oauthConfigGuard = z.object({ - responseType: z.literal('code').optional().default('code'), - grantType: z.literal('authorization_code').optional().default('authorization_code'), - tokenEndpointResponseType: tokenEndpointResponseTypeGuard, - authorizationEndpoint: z.string(), - tokenEndpoint: z.string(), +export const oauth2ConnectorConfigGuard = oauth2ConfigGuard.extend({ userInfoEndpoint: z.string(), - clientId: z.string(), - clientSecret: z.string(), - scope: z.string().optional(), + tokenEndpointResponseType: tokenEndpointResponseTypeGuard, profileMap: profileMapGuard, customConfig: z.record(z.string()).optional(), }); -export type OauthConfig = z.infer; - -export const authResponseGuard = z.object({ - code: z.string(), - state: z.string().optional(), -}); - -export type AuthResponse = z.infer; - -export const accessTokenResponseGuard = z.object({ - access_token: z.string(), - token_type: z.string(), - expires_in: z.number().optional(), - refresh_token: z.string().optional(), - scope: z.string().optional(), -}); - -export type AccessTokenResponse = z.infer; +export type Oauth2ConnectorConfig = z.infer; diff --git a/packages/connectors/connector-oauth2/src/utils.ts b/packages/connectors/connector-oauth2/src/utils.ts index a304854bd7c1..18d51f407a46 100644 --- a/packages/connectors/connector-oauth2/src/utils.ts +++ b/packages/connectors/connector-oauth2/src/utils.ts @@ -1,48 +1,25 @@ -import { assert, pick } from '@silverhand/essentials'; -import type { Response } from 'got'; -import { got, HTTPError } from 'got'; -import snakecaseKeys from 'snakecase-keys'; +import { assert } from '@silverhand/essentials'; import { ConnectorError, ConnectorErrorCodes, parseJson } from '@logto/connector-kit'; +import { type KyResponse } from 'ky'; import qs from 'query-string'; -import { defaultTimeout } from './constant.js'; -import type { - OauthConfig, - TokenEndpointResponseType, - AccessTokenResponse, - ProfileMap, -} from './types.js'; -import { authResponseGuard, accessTokenResponseGuard, userProfileGuard } from './types.js'; - -export const accessTokenRequester = async ( - tokenEndpoint: string, - queryParameters: Record, - tokenEndpointResponseType: TokenEndpointResponseType, - timeout: number = defaultTimeout -): Promise => { - try { - const httpResponse = await got.post({ - url: tokenEndpoint, - form: queryParameters, - timeout: { request: timeout }, - }); - - return await accessTokenResponseHandler(httpResponse, tokenEndpointResponseType); - } catch (error: unknown) { - if (error instanceof HTTPError) { - throw new ConnectorError(ConnectorErrorCodes.General, JSON.stringify(error.response.body)); - } - throw error; - } -}; +import { + type Oauth2AccessTokenResponse, + oauth2AccessTokenResponseGuard, + oauth2AuthResponseGuard, +} from './oauth2/types.js'; +import { requestTokenEndpoint } from './oauth2/utils.js'; +import type { Oauth2ConnectorConfig, TokenEndpointResponseType, ProfileMap } from './types.js'; +import { userProfileGuard } from './types.js'; const accessTokenResponseHandler = async ( - response: Response, + response: KyResponse, tokenEndpointResponseType: TokenEndpointResponseType -): Promise => { - const result = accessTokenResponseGuard.safeParse( - tokenEndpointResponseType === 'json' ? parseJson(response.body) : qs.parse(response.body) +): Promise => { + const responseContent = await response.text(); + const result = oauth2AccessTokenResponseGuard.safeParse( + tokenEndpointResponseType === 'json' ? parseJson(responseContent) : qs.parse(responseContent) ); // Why it works with qs.parse() if (!result.success) { @@ -84,8 +61,12 @@ export const userProfileMapping = ( return result.data; }; -export const getAccessToken = async (config: OauthConfig, data: unknown, redirectUri: string) => { - const result = authResponseGuard.safeParse(data); +export const getAccessToken = async ( + config: Oauth2ConnectorConfig, + data: unknown, + redirectUri: string +) => { + const result = oauth2AuthResponseGuard.safeParse(data); if (!result.success) { throw new ConnectorError(ConnectorErrorCodes.General, data); @@ -93,18 +74,32 @@ export const getAccessToken = async (config: OauthConfig, data: unknown, redirec const { code } = result.data; - const { customConfig, ...rest } = config; - - const parameterObject = snakecaseKeys({ - ...pick(rest, 'grantType', 'clientId', 'clientSecret'), - ...customConfig, - code, - redirectUri, + const { + grantType, + tokenEndpoint, + tokenEndpointResponseType, + clientId, + clientSecret, + tokenEndpointAuthMethod, + clientSecretJwtSigningAlgorithm, + customConfig, + } = config; + + const tokenResponse = await requestTokenEndpoint({ + tokenEndpoint, + tokenEndpointAuthOptions: { + method: tokenEndpointAuthMethod, + clientSecretJwtSigningAlgorithm, + }, + tokenRequestBody: { + grantType, + code, + redirectUri, + clientId, + clientSecret, + ...customConfig, + }, }); - return accessTokenRequester( - config.tokenEndpoint, - parameterObject, - config.tokenEndpointResponseType - ); + return accessTokenResponseHandler(tokenResponse, tokenEndpointResponseType); }; diff --git a/packages/connectors/connector-oidc/README.md b/packages/connectors/connector-oidc/README.md index eb58ca8e3ab1..68bae7495ad3 100644 --- a/packages/connectors/connector-oidc/README.md +++ b/packages/connectors/connector-oidc/README.md @@ -24,6 +24,10 @@ We ONLY support "Authorization Code" grant type for security consideration and i *clientSecret*: The client secret is a confidential key that is issued to the client application by the authorization server during registration. The client application uses this secret key to authenticate itself with the authorization server when requesting access tokens. The client secret is considered confidential information and should be kept secure at all times. +*tokenEndpointAuthMethod*: The token endpoint authentication method is used by the client application to authenticate itself with the authorization server when requesting access tokens. To discover supported methods, consult the `token_endpoint_auth_methods_supported` field available at the OAuth 2.0 service provider’s OpenID Connect discovery endpoint, or refer to the relevant documentation provided by the OAuth 2.0 service provider. + +*clientSecretJwtSigningAlgorithm (Optional)*: Only required when `tokenEndpointAuthMethod` is `client_secret_jwt`. The client secret JWT signing algorithm is used by the client application to sign the JWT that is sent to the authorization server during the token request. + *scope*: The scope parameter is used to specify the set of resources and permissions that the client application is requesting access to. The scope parameter is typically defined as a space-separated list of values that represent specific permissions. For example, a scope value of "read write" might indicate that the client application is requesting read and write access to a user's data. You are expected to find `authorizationEndpoint`, `tokenEndpoint`, `jwksUri` and `issuer` as OpenID Provider's configuration information. They should be available in social vendor's documentation. diff --git a/packages/connectors/connector-oidc/package.json b/packages/connectors/connector-oidc/package.json index 56dc91630af6..d605f729d69a 100644 --- a/packages/connectors/connector-oidc/package.json +++ b/packages/connectors/connector-oidc/package.json @@ -4,10 +4,11 @@ "description": "OIDC standard connector implementation.", "dependencies": { "@logto/connector-kit": "workspace:^3.0.0", + "@logto/connector-oauth": "workspace:^1.2.0", "@logto/shared": "workspace:^3.1.0", "@silverhand/essentials": "^2.9.0", - "got": "^14.0.0", "jose": "^5.0.0", + "ky": "^1.2.3", "nanoid": "^5.0.1", "snakecase-keys": "^8.0.0", "zod": "^3.22.4" @@ -65,7 +66,7 @@ "@vitest/coverage-v8": "^1.4.0", "eslint": "^8.56.0", "lint-staged": "^15.0.2", - "nock": "^13.3.1", + "nock": "14.0.0-beta.6", "prettier": "^3.0.0", "rollup": "^4.12.0", "rollup-plugin-output-size": "^1.3.0", diff --git a/packages/connectors/connector-oidc/src/constant.ts b/packages/connectors/connector-oidc/src/constant.ts index 2315bc1e4a0b..85071b507df4 100644 --- a/packages/connectors/connector-oidc/src/constant.ts +++ b/packages/connectors/connector-oidc/src/constant.ts @@ -1,5 +1,6 @@ import type { ConnectorMetadata } from '@logto/connector-kit'; import { ConnectorConfigFormItemType, ConnectorPlatform } from '@logto/connector-kit'; +import { ClientSecretJwtSigningAlgorithm, TokenEndpointAuthMethod } from '@logto/connector-oauth'; export const defaultMetadata: ConnectorMetadata = { id: 'oidc', @@ -46,6 +47,56 @@ export const defaultMetadata: ConnectorMetadata = { required: true, placeholder: '', }, + { + key: 'tokenEndpointAuthMethod', + label: 'Token Endpoint Auth Method', + type: ConnectorConfigFormItemType.Select, + selectItems: [ + { + title: TokenEndpointAuthMethod.ClientSecretPost, + value: TokenEndpointAuthMethod.ClientSecretPost, + }, + { + title: TokenEndpointAuthMethod.ClientSecretBasic, + value: TokenEndpointAuthMethod.ClientSecretBasic, + }, + { + title: TokenEndpointAuthMethod.ClientSecretJwt, + value: TokenEndpointAuthMethod.ClientSecretJwt, + }, + ], + required: true, + defaultValue: TokenEndpointAuthMethod.ClientSecretPost, + description: 'The method used for client authentication at the token endpoint in OAuth 2.0.', + }, + { + key: 'clientSecretJwtSigningAlgorithm', + label: 'Client Secret JWT Signing Algorithm', + type: ConnectorConfigFormItemType.Select, + selectItems: [ + { + title: ClientSecretJwtSigningAlgorithm.HS256, + value: ClientSecretJwtSigningAlgorithm.HS256, + }, + { + title: ClientSecretJwtSigningAlgorithm.HS384, + value: ClientSecretJwtSigningAlgorithm.HS384, + }, + { + title: ClientSecretJwtSigningAlgorithm.HS512, + value: ClientSecretJwtSigningAlgorithm.HS512, + }, + ], + showConditions: [ + { + targetKey: 'tokenEndpointAuthMethod', + expectValue: TokenEndpointAuthMethod.ClientSecretJwt, + }, + ], + required: true, + defaultValue: ClientSecretJwtSigningAlgorithm.HS256, + description: 'The signing algorithm used for the client secret JWT.', + }, { key: 'scope', label: 'Scope', diff --git a/packages/connectors/connector-oidc/src/index.ts b/packages/connectors/connector-oidc/src/index.ts index a413a26c06f2..a05af49ca09c 100644 --- a/packages/connectors/connector-oidc/src/index.ts +++ b/packages/connectors/connector-oidc/src/index.ts @@ -1,6 +1,4 @@ -import { assert, conditional, pick } from '@silverhand/essentials'; -import { HTTPError } from 'got'; -import snakecaseKeys from 'snakecase-keys'; +import { assert, conditional } from '@silverhand/essentials'; import type { GetAuthorizationUri, @@ -16,11 +14,13 @@ import { ConnectorType, jsonGuard, } from '@logto/connector-kit'; +import { constructAuthorizationUri } from '@logto/connector-oauth'; import { generateStandardId } from '@logto/shared/universal'; import { createRemoteJWKSet, jwtVerify } from 'jose'; +import { HTTPError } from 'ky'; import { defaultMetadata } from './constant.js'; -import { idTokenProfileStandardClaimsGuard, oidcConfigGuard } from './types.js'; +import { idTokenProfileStandardClaimsGuard, oidcConnectorConfigGuard } from './types.js'; import { getIdToken } from './utils.js'; const generateNonce = () => generateStandardId(); @@ -29,8 +29,8 @@ const getAuthorizationUri = (getConfig: GetConnectorConfig): GetAuthorizationUri => async ({ state, redirectUri }, setSession) => { const config = await getConfig(defaultMetadata.id); - validateConfig(config, oidcConfigGuard); - const parsedConfig = oidcConfigGuard.parse(config); + validateConfig(config, oidcConnectorConfigGuard); + const parsedConfig = oidcConnectorConfigGuard.parse(config); const nonce = generateNonce(); @@ -42,28 +42,33 @@ const getAuthorizationUri = ); await setSession({ nonce, redirectUri }); - const { customConfig, authRequestOptionalConfig, ...rest } = parsedConfig; - - const queryParameters = new URLSearchParams({ + const { + authorizationEndpoint, + responseType, + clientId, + scope, + customConfig, + authRequestOptionalConfig, + } = parsedConfig; + + return constructAuthorizationUri(authorizationEndpoint, { + responseType, + clientId, + scope, + redirectUri, state, - ...snakecaseKeys({ - ...pick(rest, 'responseType', 'scope', 'clientId'), - ...authRequestOptionalConfig, - ...customConfig, - }), nonce, - redirect_uri: redirectUri, + ...authRequestOptionalConfig, + ...customConfig, }); - - return `${parsedConfig.authorizationEndpoint}?${queryParameters.toString()}`; }; const getUserInfo = (getConfig: GetConnectorConfig): GetUserInfo => async (data, getSession) => { const config = await getConfig(defaultMetadata.id); - validateConfig(config, oidcConfigGuard); - const parsedConfig = oidcConfigGuard.parse(config); + validateConfig(config, oidcConnectorConfigGuard); + const parsedConfig = oidcConnectorConfigGuard.parse(config); assert( getSession, @@ -153,7 +158,7 @@ const createOidcConnector: CreateConnector = async ({ getConfig return { metadata: defaultMetadata, type: ConnectorType.Social, - configGuard: oidcConfigGuard, + configGuard: oidcConnectorConfigGuard, getAuthorizationUri: getAuthorizationUri(getConfig), getUserInfo: getUserInfo(getConfig), }; diff --git a/packages/connectors/connector-oidc/src/types.ts b/packages/connectors/connector-oidc/src/types.ts index 4039d553c7c1..432a43af916f 100644 --- a/packages/connectors/connector-oidc/src/types.ts +++ b/packages/connectors/connector-oidc/src/types.ts @@ -1,5 +1,7 @@ import { z } from 'zod'; +import { oauth2ConfigGuard } from '@logto/connector-oauth'; + const scopeOpenid = 'openid'; export const delimiter = /[ +]/; @@ -38,16 +40,6 @@ export const userProfileGuard = z.object({ export type UserProfile = z.infer; -const endpointConfigObject = { - authorizationEndpoint: z.string(), - tokenEndpoint: z.string(), -}; - -const clientConfigObject = { - clientId: z.string(), - clientSecret: z.string(), -}; - /** * We remove `nonce` in `authRequestOptionalConfigGuard` because it should be a randomly generated string, * should not be fixed in config and will be generated in Logto core according to `response_type` of authorization request. @@ -84,18 +76,15 @@ export const idTokenVerificationConfigGuard = z.object({ jwksUri: z.string() }). export type IdTokenVerificationConfig = z.infer; -export const oidcConfigGuard = z.object({ - responseType: z.literal('code').optional().default('code'), - grantType: z.literal('authorization_code').optional().default('authorization_code'), +export const oidcConnectorConfigGuard = oauth2ConfigGuard.extend({ + // Override `scope` to ensure it contains 'openid'. scope: z.string().transform(scopePostProcessor), idTokenVerificationConfig: idTokenVerificationConfigGuard, authRequestOptionalConfig: authRequestOptionalConfigGuard.optional(), customConfig: z.record(z.string()).optional(), - ...endpointConfigObject, - ...clientConfigObject, }); -export type OidcConfig = z.infer; +export type OidcConnectorConfig = z.infer; export const authResponseGuard = z .object({ diff --git a/packages/connectors/connector-oidc/src/utils.ts b/packages/connectors/connector-oidc/src/utils.ts index fa227364d0ab..e623eb202720 100644 --- a/packages/connectors/connector-oidc/src/utils.ts +++ b/packages/connectors/connector-oidc/src/utils.ts @@ -1,39 +1,12 @@ -import { pick } from '@silverhand/essentials'; -import type { Response } from 'got'; -import { got, HTTPError } from 'got'; -import snakecaseKeys from 'snakecase-keys'; - import { ConnectorError, ConnectorErrorCodes, parseJson } from '@logto/connector-kit'; +import { requestTokenEndpoint } from '@logto/connector-oauth'; +import { type KyResponse } from 'ky'; -import { defaultTimeout } from './constant.js'; -import type { AccessTokenResponse, OidcConfig } from './types.js'; +import type { AccessTokenResponse, OidcConnectorConfig } from './types.js'; import { accessTokenResponseGuard, authResponseGuard } from './types.js'; -export const accessTokenRequester = async ( - tokenEndpoint: string, - queryParameters: Record, - timeout: number = defaultTimeout -): Promise => { - try { - const httpResponse = await got.post({ - url: tokenEndpoint, - form: queryParameters, - timeout: { request: timeout }, - }); - - return await accessTokenResponseHandler(httpResponse); - } catch (error: unknown) { - if (error instanceof HTTPError) { - throw new ConnectorError(ConnectorErrorCodes.General, JSON.stringify(error.response.body)); - } - throw error; - } -}; - -const accessTokenResponseHandler = async ( - response: Response -): Promise => { - const result = accessTokenResponseGuard.safeParse(parseJson(response.body)); +const accessTokenResponseHandler = async (response: KyResponse): Promise => { + const result = accessTokenResponseGuard.safeParse(parseJson(await response.text())); if (!result.success) { throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, result.error); @@ -42,7 +15,11 @@ const accessTokenResponseHandler = async ( return result.data; }; -export const getIdToken = async (config: OidcConfig, data: unknown, redirectUri: string) => { +export const getIdToken = async ( + config: OidcConnectorConfig, + data: unknown, + redirectUri: string +) => { const result = authResponseGuard.safeParse(data); if (!result.success) { @@ -51,14 +28,31 @@ export const getIdToken = async (config: OidcConfig, data: unknown, redirectUri: const { code } = result.data; - const { customConfig, ...rest } = config; - - const parameterObject = snakecaseKeys({ - ...pick(rest, 'grantType', 'clientId', 'clientSecret'), - ...customConfig, - code, - redirectUri, + const { + tokenEndpoint, + grantType, + clientId, + clientSecret, + tokenEndpointAuthMethod, + clientSecretJwtSigningAlgorithm, + customConfig, + } = config; + + const tokenResponse = await requestTokenEndpoint({ + tokenEndpoint, + tokenEndpointAuthOptions: { + method: tokenEndpointAuthMethod, + clientSecretJwtSigningAlgorithm, + }, + tokenRequestBody: { + grantType, + code, + redirectUri, + clientId, + clientSecret, + ...customConfig, + }, }); - return accessTokenRequester(config.tokenEndpoint, parameterObject); + return accessTokenResponseHandler(tokenResponse); }; diff --git a/packages/core/jest.config.js b/packages/core/jest.config.js index dad1a7aff3cd..7345a0ed30da 100644 --- a/packages/core/jest.config.js +++ b/packages/core/jest.config.js @@ -11,6 +11,8 @@ const config = { moduleNameMapper: { '^#src/(.*)\\.js(x)?$': '/build/$1', '^(chalk|inquirer)$': '/../shared/lib/esm/module-proxy.js', + // Map the connector-kit to the installed version rather than finding it from the `shared` package (which is the default behavior of `mockEsm` in the `shared` package) + '^@logto/connector-kit$': '/node_modules/@logto/connector-kit/lib/index.js', }, }; diff --git a/packages/core/nodemon.json b/packages/core/nodemon.json index b56fb7e927f5..ca594d4cb818 100644 --- a/packages/core/nodemon.json +++ b/packages/core/nodemon.json @@ -9,7 +9,8 @@ "../core/src/", "../core/node_modules/", ".env", - "../../.env" + "../../.env", + "../connectors/*/lib/" ], "ext": "json,js,jsx,ts,tsx", "delay": 500 diff --git a/packages/shared/package.json b/packages/shared/package.json index 54ed3e0c77c8..d667762af30e 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -38,7 +38,6 @@ }, "devDependencies": { "@jest/globals": "^29.7.0", - "@logto/connector-kit": "workspace:^3.0.0", "@silverhand/eslint-config": "6.0.1", "@silverhand/ts-config": "6.0.0", "@types/node": "^20.9.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b8fd3bcfc176..d8f351617278 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1857,12 +1857,18 @@ importers: '@logto/connector-kit': specifier: workspace:^3.0.0 version: link:../../toolkit/connector-kit + '@logto/shared': + specifier: workspace:^3.1.0 + version: link:../../shared '@silverhand/essentials': specifier: ^2.9.0 version: 2.9.0 - got: - specifier: ^14.0.0 - version: 14.0.0 + jose: + specifier: ^5.0.0 + version: 5.2.2 + ky: + specifier: ^1.2.3 + version: 1.2.3 query-string: specifier: ^9.0.0 version: 9.0.0 @@ -1907,8 +1913,8 @@ importers: specifier: ^15.0.2 version: 15.0.2 nock: - specifier: ^13.3.1 - version: 13.3.1 + specifier: 14.0.0-beta.6 + version: 14.0.0-beta.6 prettier: specifier: ^3.0.0 version: 3.0.0 @@ -1933,18 +1939,21 @@ importers: '@logto/connector-kit': specifier: workspace:^3.0.0 version: link:../../toolkit/connector-kit + '@logto/connector-oauth': + specifier: workspace:^1.2.0 + version: link:../connector-oauth2 '@logto/shared': specifier: workspace:^3.1.0 version: link:../../shared '@silverhand/essentials': specifier: ^2.9.0 version: 2.9.0 - got: - specifier: ^14.0.0 - version: 14.0.0 jose: specifier: ^5.0.0 version: 5.0.1 + ky: + specifier: ^1.2.3 + version: 1.2.3 nanoid: specifier: ^5.0.1 version: 5.0.1 @@ -1989,8 +1998,8 @@ importers: specifier: ^15.0.2 version: 15.0.2 nock: - specifier: ^13.3.1 - version: 13.3.1 + specifier: 14.0.0-beta.6 + version: 14.0.0-beta.6 prettier: specifier: ^3.0.0 version: 3.0.0 @@ -3848,9 +3857,6 @@ importers: '@jest/globals': specifier: ^29.7.0 version: 29.7.0 - '@logto/connector-kit': - specifier: workspace:^3.0.0 - version: link:../toolkit/connector-kit '@silverhand/eslint-config': specifier: 6.0.1 version: 6.0.1(eslint@8.57.0)(prettier@3.0.0)(typescript@5.3.3) @@ -17254,6 +17260,14 @@ packages: propagate: 2.0.1 dev: true + /nock@14.0.0-beta.6: + resolution: {integrity: sha512-b7lc7qvj1dQzxtbU7TqyTMnKbNKwGQd585xsRtcCZOv3I/yOK9Vwv4nOgnLFxFtX9m1yjhQDRbgqFCqNh9HuEw==} + engines: {node: '>= 18'} + dependencies: + json-stringify-safe: 5.0.1 + propagate: 2.0.1 + dev: true + /node-addon-api@3.2.1: resolution: {integrity: sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==} dev: true