mirror of
https://github.com/logto-io/logto.git
synced 2025-01-06 20:40:08 -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';
|
import { nanoid } from 'nanoid';
|
||||||
|
|
||||||
export const generateOidcPrivateKey = async () => {
|
export const generateOidcPrivateKey = async (type: 'rsa' | 'ec' = 'ec') => {
|
||||||
const { privateKey } = await promisify(generateKeyPair)('rsa', {
|
if (type === 'rsa') {
|
||||||
modulusLength: 4096,
|
const { privateKey } = await promisify(generateKeyPair)('rsa', {
|
||||||
publicKeyEncoding: {
|
modulusLength: 4096,
|
||||||
type: 'spki',
|
publicKeyEncoding: {
|
||||||
format: 'pem',
|
type: 'spki',
|
||||||
},
|
format: 'pem',
|
||||||
privateKeyEncoding: {
|
},
|
||||||
type: 'pkcs8',
|
privateKeyEncoding: {
|
||||||
format: 'pem',
|
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();
|
export const generateOidcCookieKey = () => nanoid();
|
||||||
|
|
|
@ -2,6 +2,7 @@ import crypto from 'crypto';
|
||||||
|
|
||||||
import type { LogtoOidcConfigType } from '@logto/schemas';
|
import type { LogtoOidcConfigType } from '@logto/schemas';
|
||||||
import { LogtoOidcConfigKey } from '@logto/schemas';
|
import { LogtoOidcConfigKey } from '@logto/schemas';
|
||||||
|
import { conditional } from '@silverhand/essentials';
|
||||||
import { createLocalJWKSet } from 'jose';
|
import { createLocalJWKSet } from 'jose';
|
||||||
|
|
||||||
import { exportJWK } from '#src/utils/jwks.js';
|
import { exportJWK } from '#src/utils/jwks.js';
|
||||||
|
@ -17,9 +18,14 @@ const loadOidcValues = async (issuer: string, configs: LogtoOidcConfigType) => {
|
||||||
const localJWKSet = createLocalJWKSet({ keys: publicJwks });
|
const localJWKSet = createLocalJWKSet({ keys: publicJwks });
|
||||||
const refreshTokenReuseInterval = configs[LogtoOidcConfigKey.RefreshTokenReuseInterval];
|
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({
|
return Object.freeze({
|
||||||
cookieKeys,
|
cookieKeys,
|
||||||
privateJwks,
|
privateJwks,
|
||||||
|
jwkSigningAlg,
|
||||||
localJWKSet,
|
localJWKSet,
|
||||||
issuer,
|
issuer,
|
||||||
refreshTokenReuseInterval,
|
refreshTokenReuseInterval,
|
||||||
|
|
|
@ -13,4 +13,7 @@ export const addOidcEventListeners = (provider: Provider) => {
|
||||||
provider.addListener('grant.revoked', grantRevocationListener);
|
provider.addListener('grant.revoked', grantRevocationListener);
|
||||||
provider.addListener('interaction.started', interactionStartedListener);
|
provider.addListener('interaction.started', interactionStartedListener);
|
||||||
provider.addListener('interaction.ended', interactionEndedListener);
|
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';
|
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 {
|
export default function initOidc(): Provider {
|
||||||
const { issuer, cookieKeys, privateJwks, defaultIdTokenTtl, defaultRefreshTokenTtl } =
|
const {
|
||||||
envSet.oidc;
|
issuer,
|
||||||
|
cookieKeys,
|
||||||
|
privateJwks,
|
||||||
|
jwkSigningAlg,
|
||||||
|
defaultIdTokenTtl,
|
||||||
|
defaultRefreshTokenTtl,
|
||||||
|
} = envSet.oidc;
|
||||||
const logoutSource = readFileSync('static/html/logout.html', 'utf8');
|
const logoutSource = readFileSync('static/html/logout.html', 'utf8');
|
||||||
|
|
||||||
const cookieConfig = Object.freeze({
|
const cookieConfig = Object.freeze({
|
||||||
|
@ -35,7 +44,8 @@ export default function initOidc(): Provider {
|
||||||
const oidc = new Provider(issuer, {
|
const oidc = new Provider(issuer, {
|
||||||
adapter: postgresAdapter,
|
adapter: postgresAdapter,
|
||||||
renderError: (_ctx, _out, error) => {
|
renderError: (_ctx, _out, error) => {
|
||||||
console.log('OIDC error', error);
|
console.error(error);
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
},
|
},
|
||||||
cookies: {
|
cookies: {
|
||||||
|
@ -46,6 +56,12 @@ export default function initOidc(): Provider {
|
||||||
jwks: {
|
jwks: {
|
||||||
keys: privateJwks,
|
keys: privateJwks,
|
||||||
},
|
},
|
||||||
|
enabledJWA: {
|
||||||
|
authorizationSigningAlgValues: [...supportedSigningAlgs],
|
||||||
|
userinfoSigningAlgValues: [...supportedSigningAlgs],
|
||||||
|
idTokenSigningAlgValues: [...supportedSigningAlgs],
|
||||||
|
introspectionSigningAlgValues: [...supportedSigningAlgs],
|
||||||
|
},
|
||||||
conformIdTokenClaims: false,
|
conformIdTokenClaims: false,
|
||||||
features: {
|
features: {
|
||||||
userinfo: { enabled: true },
|
userinfo: { enabled: true },
|
||||||
|
@ -77,6 +93,9 @@ export default function initOidc(): Provider {
|
||||||
accessTokenFormat: 'jwt',
|
accessTokenFormat: 'jwt',
|
||||||
scope: '',
|
scope: '',
|
||||||
accessTokenTTL,
|
accessTokenTTL,
|
||||||
|
jwt: {
|
||||||
|
sign: { alg: jwkSigningAlg },
|
||||||
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,12 +4,11 @@ import { conditional } from '@silverhand/essentials';
|
||||||
import type { AllClientMetadata, ClientAuthMethod } from 'oidc-provider';
|
import type { AllClientMetadata, ClientAuthMethod } from 'oidc-provider';
|
||||||
import { errors } from 'oidc-provider';
|
import { errors } from 'oidc-provider';
|
||||||
|
|
||||||
export const getConstantClientMetadata = (
|
import envSet from '#src/env-set/index.js';
|
||||||
type: ApplicationType
|
|
||||||
): Pick<
|
export const getConstantClientMetadata = (type: ApplicationType): AllClientMetadata => {
|
||||||
AllClientMetadata,
|
const { jwkSigningAlg } = envSet.oidc;
|
||||||
'application_type' | 'grant_types' | 'token_endpoint_auth_method' | 'response_types'
|
|
||||||
> => {
|
|
||||||
const getTokenEndpointAuthMethod = (): ClientAuthMethod => {
|
const getTokenEndpointAuthMethod = (): ClientAuthMethod => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case ApplicationType.Native:
|
case ApplicationType.Native:
|
||||||
|
@ -28,6 +27,11 @@ export const getConstantClientMetadata = (
|
||||||
: [GrantType.AuthorizationCode, GrantType.RefreshToken],
|
: [GrantType.AuthorizationCode, GrantType.RefreshToken],
|
||||||
token_endpoint_auth_method: getTokenEndpointAuthMethod(),
|
token_endpoint_auth_method: getTokenEndpointAuthMethod(),
|
||||||
response_types: conditional(type === ApplicationType.MachineToMachine && []),
|
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