0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-01-27 21:39:16 -05:00

feat(cli): rotate keys

This commit is contained in:
Gao Sun 2022-11-07 11:47:51 +08:00
parent 877eb892c9
commit 682c88aac8
No known key found for this signature in database
GPG key ID: 13EBE123E4773688
3 changed files with 88 additions and 20 deletions

View file

@ -1,5 +1,5 @@
import type { LogtoConfigKey } from '@logto/schemas';
import { logtoConfigGuards, logtoConfigKeys } from '@logto/schemas';
import { LogtoOidcConfigKey, logtoConfigGuards, logtoConfigKeys } from '@logto/schemas';
import { deduplicate, noop } from '@silverhand/essentials';
import chalk from 'chalk';
import type { CommandModule } from 'yargs';
@ -7,6 +7,7 @@ import type { CommandModule } from 'yargs';
import { createPoolFromConfig } from '../../database';
import { getRowsByKeys, updateValueByKey } from '../../queries/logto-config';
import { log } from '../../utilities';
import { generateOidcCookieKey, generateOidcPrivateKey } from './utilities';
const validKeysDisplay = chalk.green(logtoConfigKeys.join(', '));
@ -29,6 +30,21 @@ const validateKeys: ValidateKeysFunction = (keys) => {
}
};
const validRotateKeys = Object.freeze([
LogtoOidcConfigKey.PrivateKeys,
LogtoOidcConfigKey.CookieKeys,
] as const);
type ValidateRotateKeyFunction = (key: string) => asserts key is typeof validRotateKeys[number];
const validateRotateKey: ValidateRotateKeyFunction = (key) => {
// Using `.includes()` will result a type error
// eslint-disable-next-line unicorn/prefer-includes
if (!validRotateKeys.some((element) => element === key)) {
log.error(`Invalid config key ${chalk.red(key)} found, expected one of ${validKeysDisplay}`);
}
};
const getConfig: CommandModule<unknown, { key: string; keys: string[] }> = {
command: 'get <key> [keys...]',
describe: 'Get config value(s) of the given key(s) in Logto database',
@ -97,10 +113,54 @@ const setConfig: CommandModule<unknown, { key: string; value: string }> = {
},
};
const rotateConfig: CommandModule<unknown, { key: string }> = {
command: 'rotate <key>',
describe:
'Generate a new private or secret key for the given config key and prepend to the key array',
builder: (yargs) =>
yargs.positional('key', {
describe: `The key to rotate, one of ${chalk.green(validRotateKeys.join(', '))}`,
type: 'string',
demandOption: true,
}),
handler: async ({ key }) => {
validateRotateKey(key);
const pool = await createPoolFromConfig();
const { rows } = await getRowsByKeys(pool, [key]);
if (!rows[0]) {
log.warn('No key found, create a new one');
}
const getValue = async () => {
const parsed = logtoConfigGuards[key].safeParse(rows[0]?.value);
const original = parsed.success ? parsed.data : [];
switch (key) {
case LogtoOidcConfigKey.PrivateKeys:
return [await generateOidcPrivateKey(), ...original];
case LogtoOidcConfigKey.CookieKeys:
return [generateOidcCookieKey(), ...original];
default:
log.warn('No proper handler found, use empty array');
return [];
}
};
const rotated = await getValue();
await updateValueByKey(pool, key, rotated);
await pool.end();
log.info(`Rotate ${chalk.green(key)} succeeded, now it has ${rotated.length} keys`);
},
};
const config: CommandModule = {
command: ['config', 'configs'],
describe: 'Commands for Logto database config',
builder: (yargs) => yargs.command(getConfig).command(setConfig).demandCommand(1),
builder: (yargs) =>
yargs.command(getConfig).command(setConfig).command(rotateConfig).demandCommand(1),
handler: noop,
};

View file

@ -1,11 +1,10 @@
import { generateKeyPair } from 'crypto';
import { readFile } from 'fs/promises';
import { promisify } from 'util';
import type { LogtoOidcConfigType } from '@logto/schemas';
import { LogtoOidcConfigKey } from '@logto/schemas';
import { getEnv, getEnvAsStringArray } from '@silverhand/essentials';
import { nanoid } from 'nanoid';
import { generateOidcCookieKey, generateOidcPrivateKey } from '../utilities';
const isBase64FormatPrivateKey = (key: string) => !key.includes('-');
@ -57,21 +56,8 @@ export const oidcConfigReaders: {
};
}
// Generate a new key
const { privateKey } = await promisify(generateKeyPair)('rsa', {
modulusLength: 4096,
publicKeyEncoding: {
type: 'spki',
format: 'pem',
},
privateKeyEncoding: {
type: 'pkcs8',
format: 'pem',
},
});
return {
value: [privateKey],
value: [await generateOidcPrivateKey()],
fromEnv: false,
};
},
@ -79,7 +65,7 @@ export const oidcConfigReaders: {
const envKey = 'OIDC_COOKIE_KEYS';
const keys = getEnvAsStringArray(envKey);
return { value: keys.length > 0 ? keys : [nanoid()], fromEnv: keys.length > 0 };
return { value: keys.length > 0 ? keys : [generateOidcCookieKey()], fromEnv: keys.length > 0 };
},
[LogtoOidcConfigKey.RefreshTokenReuseInterval]: async () => {
const envKey = 'OIDC_REFRESH_TOKEN_REUSE_INTERVAL';

View file

@ -0,0 +1,22 @@
import { generateKeyPair } from 'crypto';
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',
},
});
return privateKey;
};
export const generateOidcCookieKey = () => nanoid();