-
-
Notifications
You must be signed in to change notification settings - Fork 492
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(connector): support
client_secret_basic
and `client_secret_jwt…
…` methods for oauth2 connectors
- Loading branch information
Showing
22 changed files
with
541 additions
and
130 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from './types.js'; | ||
export * from './utils.js'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
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(), | ||
userInfoEndpoint: 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<typeof oauth2AuthResponseGuard>; | ||
|
||
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<typeof oauth2AccessTokenResponseGuard>; |
129 changes: 129 additions & 0 deletions
129
packages/connectors/connector-oauth2/src/oauth2/utils.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
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 ClientCredentials = { | ||
clientId: string; | ||
clientSecret: string; | ||
}; | ||
|
||
type TokenEndpointAuthOptions<T extends TokenEndpointAuthMethod = TokenEndpointAuthMethod> = | ||
T extends TokenEndpointAuthMethod.ClientSecretJwt | ||
? { | ||
method: TokenEndpointAuthMethod.ClientSecretJwt; | ||
clientSecretJwtSigningAlgorithm: string; | ||
} | ||
: { | ||
method: | ||
| TokenEndpointAuthMethod.ClientSecretBasic | ||
| TokenEndpointAuthMethod.ClientSecretPost; | ||
}; | ||
|
||
export type RequestTokenEndpointOptions = { | ||
tokenEndpoint: string; | ||
clientCredentials: ClientCredentials; | ||
tokenEndpointAuthOptions: TokenEndpointAuthOptions; | ||
queryParameters: Record<string, string>; | ||
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 queryParameters - The query parameters to include in the request. | ||
* @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, | ||
clientCredentials, | ||
tokenEndpointAuthOptions, | ||
queryParameters, | ||
timeout, | ||
}: RequestTokenEndpointOptions) => { | ||
const postTokenEndpoint = async ({ | ||
form, | ||
headers, | ||
}: { | ||
form: Record<string, string>; | ||
headers?: Record<string, string>; | ||
}) => { | ||
try { | ||
return await ky.post(tokenEndpoint, { | ||
headers, | ||
body: new URLSearchParams(snakecaseKeys(form)), | ||
timeout, | ||
}); | ||
} catch (error: unknown) { | ||
if (error instanceof HTTPError) { | ||
throw new ConnectorError(ConnectorErrorCodes.General, JSON.stringify(error.response.body)); | ||
} | ||
|
||
throw error; | ||
} | ||
}; | ||
|
||
switch (tokenEndpointAuthOptions.method) { | ||
case TokenEndpointAuthMethod.ClientSecretJwt: { | ||
const clientSecretJwt = await new SignJWT({ | ||
iss: clientCredentials.clientId, | ||
sub: clientCredentials.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(clientCredentials.clientSecret)) | ||
.catch((error: unknown) => { | ||
if (error instanceof Error) { | ||
throw new ConnectorError( | ||
ConnectorErrorCodes.General, | ||
'Failed to sign client secret JWT' | ||
); | ||
} | ||
throw error; | ||
}); | ||
|
||
return postTokenEndpoint({ | ||
form: { | ||
...queryParameters, | ||
client_id: clientCredentials.clientId, | ||
client_assertion: 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 | ||
*/ | ||
client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', | ||
}, | ||
}); | ||
} | ||
case TokenEndpointAuthMethod.ClientSecretBasic: { | ||
return postTokenEndpoint({ | ||
form: queryParameters, | ||
headers: { | ||
Authorization: `Basic ${Buffer.from( | ||
`${clientCredentials.clientId}:${clientCredentials.clientSecret}` | ||
).toString('base64')}`, | ||
}, | ||
}); | ||
} | ||
case TokenEndpointAuthMethod.ClientSecretPost: { | ||
return postTokenEndpoint({ | ||
form: { ...queryParameters, ...clientCredentials }, | ||
}); | ||
} | ||
} | ||
}; |
Oops, something went wrong.