mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
refactor: support EC key and ES signing algorithms (#2847)
This commit is contained in:
parent
3c4aeec30a
commit
080a6385c8
6 changed files with 83 additions and 22 deletions
7
.changeset-staged/six-falcons-sin.md
Normal file
7
.changeset-staged/six-falcons-sin.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
"@logto/cli": minor
|
||||
"@logto/core": minor
|
||||
---
|
||||
|
||||
- cli: use `ec` with `secp384r1` as the default key generation type
|
||||
- core: use `ES384` as the signing algorithm for EC keys
|
|
@ -3,20 +3,42 @@ import { promisify } from 'util';
|
|||
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
export const generateOidcPrivateKey = async () => {
|
||||
const { privateKey } = await promisify(generateKeyPair)('rsa', {
|
||||
modulusLength: 4096,
|
||||
publicKeyEncoding: {
|
||||
type: 'spki',
|
||||
format: 'pem',
|
||||
},
|
||||
privateKeyEncoding: {
|
||||
type: 'pkcs8',
|
||||
format: 'pem',
|
||||
},
|
||||
});
|
||||
export const generateOidcPrivateKey = async (type: 'rsa' | 'ec' = 'ec') => {
|
||||
if (type === 'rsa') {
|
||||
const { privateKey } = await promisify(generateKeyPair)('rsa', {
|
||||
modulusLength: 4096,
|
||||
publicKeyEncoding: {
|
||||
type: 'spki',
|
||||
format: 'pem',
|
||||
},
|
||||
privateKeyEncoding: {
|
||||
type: 'pkcs8',
|
||||
format: 'pem',
|
||||
},
|
||||
});
|
||||
|
||||
return privateKey;
|
||||
return privateKey;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (type === 'ec') {
|
||||
const { privateKey } = await promisify(generateKeyPair)('ec', {
|
||||
// https://security.stackexchange.com/questions/78621/which-elliptic-curve-should-i-use
|
||||
namedCurve: 'secp384r1',
|
||||
publicKeyEncoding: {
|
||||
type: 'spki',
|
||||
format: 'pem',
|
||||
},
|
||||
privateKeyEncoding: {
|
||||
type: 'pkcs8',
|
||||
format: 'pem',
|
||||
},
|
||||
});
|
||||
|
||||
return privateKey;
|
||||
}
|
||||
|
||||
throw new Error(`Unsupported private key ${String(type)}`);
|
||||
};
|
||||
|
||||
export const generateOidcCookieKey = () => nanoid();
|
||||
|
|
|
@ -2,6 +2,7 @@ import crypto from 'crypto';
|
|||
|
||||
import type { LogtoOidcConfigType } from '@logto/schemas';
|
||||
import { LogtoOidcConfigKey } from '@logto/schemas';
|
||||
import { conditional } from '@silverhand/essentials';
|
||||
import { createLocalJWKSet } from 'jose';
|
||||
|
||||
import { exportJWK } from '#src/utils/jwks.js';
|
||||
|
@ -17,9 +18,14 @@ const loadOidcValues = async (issuer: string, configs: LogtoOidcConfigType) => {
|
|||
const localJWKSet = createLocalJWKSet({ keys: publicJwks });
|
||||
const refreshTokenReuseInterval = configs[LogtoOidcConfigKey.RefreshTokenReuseInterval];
|
||||
|
||||
// Use ES384 if it's an Elliptic Curve key, otherwise fall back to default
|
||||
// It's for backwards compatibility since we were using RSA keys before v1.0.0-beta.20
|
||||
const jwkSigningAlg = conditional(privateJwks[0]?.kty === 'EC' && 'ES384');
|
||||
|
||||
return Object.freeze({
|
||||
cookieKeys,
|
||||
privateJwks,
|
||||
jwkSigningAlg,
|
||||
localJWKSet,
|
||||
issuer,
|
||||
refreshTokenReuseInterval,
|
||||
|
|
|
@ -13,4 +13,7 @@ export const addOidcEventListeners = (provider: Provider) => {
|
|||
provider.addListener('grant.revoked', grantRevocationListener);
|
||||
provider.addListener('interaction.started', interactionStartedListener);
|
||||
provider.addListener('interaction.ended', interactionEndedListener);
|
||||
provider.addListener('server_error', (_, error) => {
|
||||
console.error('OIDC Provider server_error:', error);
|
||||
});
|
||||
};
|
||||
|
|
|
@ -21,9 +21,18 @@ import assertThat from '#src/utils/assert-that.js';
|
|||
|
||||
import { claimToUserKey, getUserClaims } from './scope.js';
|
||||
|
||||
// Temporarily removed 'EdDSA' since it's not supported by browser yet
|
||||
const supportedSigningAlgs = Object.freeze(['RS256', 'PS256', 'ES256', 'ES384', 'ES512'] as const);
|
||||
|
||||
export default function initOidc(): Provider {
|
||||
const { issuer, cookieKeys, privateJwks, defaultIdTokenTtl, defaultRefreshTokenTtl } =
|
||||
envSet.oidc;
|
||||
const {
|
||||
issuer,
|
||||
cookieKeys,
|
||||
privateJwks,
|
||||
jwkSigningAlg,
|
||||
defaultIdTokenTtl,
|
||||
defaultRefreshTokenTtl,
|
||||
} = envSet.oidc;
|
||||
const logoutSource = readFileSync('static/html/logout.html', 'utf8');
|
||||
|
||||
const cookieConfig = Object.freeze({
|
||||
|
@ -35,7 +44,8 @@ export default function initOidc(): Provider {
|
|||
const oidc = new Provider(issuer, {
|
||||
adapter: postgresAdapter,
|
||||
renderError: (_ctx, _out, error) => {
|
||||
console.log('OIDC error', error);
|
||||
console.error(error);
|
||||
|
||||
throw error;
|
||||
},
|
||||
cookies: {
|
||||
|
@ -46,6 +56,12 @@ export default function initOidc(): Provider {
|
|||
jwks: {
|
||||
keys: privateJwks,
|
||||
},
|
||||
enabledJWA: {
|
||||
authorizationSigningAlgValues: [...supportedSigningAlgs],
|
||||
userinfoSigningAlgValues: [...supportedSigningAlgs],
|
||||
idTokenSigningAlgValues: [...supportedSigningAlgs],
|
||||
introspectionSigningAlgValues: [...supportedSigningAlgs],
|
||||
},
|
||||
conformIdTokenClaims: false,
|
||||
features: {
|
||||
userinfo: { enabled: true },
|
||||
|
@ -77,6 +93,9 @@ export default function initOidc(): Provider {
|
|||
accessTokenFormat: 'jwt',
|
||||
scope: '',
|
||||
accessTokenTTL,
|
||||
jwt: {
|
||||
sign: { alg: jwkSigningAlg },
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
|
|
|
@ -4,12 +4,11 @@ import { conditional } from '@silverhand/essentials';
|
|||
import type { AllClientMetadata, ClientAuthMethod } from 'oidc-provider';
|
||||
import { errors } from 'oidc-provider';
|
||||
|
||||
export const getConstantClientMetadata = (
|
||||
type: ApplicationType
|
||||
): Pick<
|
||||
AllClientMetadata,
|
||||
'application_type' | 'grant_types' | 'token_endpoint_auth_method' | 'response_types'
|
||||
> => {
|
||||
import envSet from '#src/env-set/index.js';
|
||||
|
||||
export const getConstantClientMetadata = (type: ApplicationType): AllClientMetadata => {
|
||||
const { jwkSigningAlg } = envSet.oidc;
|
||||
|
||||
const getTokenEndpointAuthMethod = (): ClientAuthMethod => {
|
||||
switch (type) {
|
||||
case ApplicationType.Native:
|
||||
|
@ -28,6 +27,11 @@ export const getConstantClientMetadata = (
|
|||
: [GrantType.AuthorizationCode, GrantType.RefreshToken],
|
||||
token_endpoint_auth_method: getTokenEndpointAuthMethod(),
|
||||
response_types: conditional(type === ApplicationType.MachineToMachine && []),
|
||||
// https://www.scottbrady91.com/jose/jwts-which-signing-algorithm-should-i-use
|
||||
authorization_signed_response_alg: jwkSigningAlg,
|
||||
userinfo_signed_response_alg: jwkSigningAlg,
|
||||
id_token_signed_response_alg: jwkSigningAlg,
|
||||
introspection_signed_response_alg: jwkSigningAlg,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in a new issue