0
Fork 0
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:
Gao Sun 2023-01-09 17:34:13 +08:00 committed by GitHub
parent 3c4aeec30a
commit 080a6385c8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 83 additions and 22 deletions

View 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

View file

@ -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();

View file

@ -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,

View file

@ -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);
});
};

View file

@ -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 },
},
};
},
},

View file

@ -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,
};
};