0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-03 22:15:32 -05:00

refactor(schemas,core,cli): alter signing key type to json object (#4582)

This commit is contained in:
Charles Zhao 2023-10-08 12:51:04 -05:00 committed by GitHub
parent f53778894d
commit 46d0d4c0b9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 165 additions and 23 deletions

View file

@ -0,0 +1,7 @@
---
"@logto/schemas": patch
"@logto/core": patch
"@logto/cli": patch
---
convert private signing key type from string to JSON object, in order to provide additional information such as key ID and creation timestamp.

View file

@ -2,6 +2,7 @@ import { readFile } from 'node:fs/promises';
import type { LogtoOidcConfigType } from '@logto/schemas';
import { LogtoOidcConfigKey, logtoConfigGuards } from '@logto/schemas';
import { generateStandardId } from '@logto/shared';
import { getEnvAsStringArray } from '@silverhand/essentials';
import chalk from 'chalk';
import type { DatabaseTransactionConnection } from 'slonik';
@ -9,7 +10,11 @@ import { z } from 'zod';
import { getRowsByKeys, updateValueByKey } from '../../../queries/logto-config.js';
import { consoleLog } from '../../../utils.js';
import { generateOidcCookieKey, generateOidcPrivateKey } from '../utils.js';
import {
buildOidcKeyFromRawString,
generateOidcCookieKey,
generateOidcPrivateKey,
} from '../utils.js';
const isBase64FormatPrivateKey = (key: string) => !key.includes('-');
@ -88,13 +93,11 @@ export const oidcConfigReaders: {
if (privateKeys.length > 0) {
return {
value: privateKeys.map((key) => {
if (isBase64FormatPrivateKey(key)) {
return Buffer.from(key, 'base64').toString('utf8');
}
return key;
}),
value: privateKeys.map((key) =>
buildOidcKeyFromRawString(
isBase64FormatPrivateKey(key) ? Buffer.from(key, 'base64').toString('utf8') : key
)
),
fromEnv: true,
};
}
@ -103,8 +106,11 @@ export const oidcConfigReaders: {
const privateKeyPaths = getEnvAsStringArray('OIDC_PRIVATE_KEY_PATHS');
if (privateKeyPaths.length > 0) {
const privateKeys = await Promise.all(
privateKeyPaths.map(async (path) => readFile(path, 'utf8'))
);
return {
value: await Promise.all(privateKeyPaths.map(async (path) => readFile(path, 'utf8'))),
value: privateKeys.map((key) => buildOidcKeyFromRawString(key)),
fromEnv: true,
};
}
@ -116,7 +122,11 @@ export const oidcConfigReaders: {
},
[LogtoOidcConfigKey.CookieKeys]: async () => {
const envKey = 'OIDC_COOKIE_KEYS';
const keys = getEnvAsStringArray(envKey);
const keys = getEnvAsStringArray(envKey).map((key) => ({
id: generateStandardId(),
value: key,
createdAt: Math.floor(Date.now() / 1000),
}));
return { value: keys.length > 0 ? keys : [generateOidcCookieKey()], fromEnv: keys.length > 0 };
},

View file

@ -1,14 +1,17 @@
import { generateKeyPair } from 'node:crypto';
import { promisify } from 'node:util';
import { generateStandardSecret } from '@logto/shared';
import { type PrivateKey } from '@logto/schemas';
import { generateStandardId, generateStandardSecret } from '@logto/shared';
export enum PrivateKeyType {
RSA = 'rsa',
EC = 'ec',
}
export const generateOidcPrivateKey = async (type: PrivateKeyType = PrivateKeyType.EC) => {
export const generateOidcPrivateKey = async (
type: PrivateKeyType = PrivateKeyType.EC
): Promise<PrivateKey> => {
if (type === PrivateKeyType.RSA) {
const { privateKey } = await promisify(generateKeyPair)('rsa', {
modulusLength: 4096,
@ -22,7 +25,7 @@ export const generateOidcPrivateKey = async (type: PrivateKeyType = PrivateKeyTy
},
});
return privateKey;
return buildOidcKeyFromRawString(privateKey);
}
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
@ -40,10 +43,16 @@ export const generateOidcPrivateKey = async (type: PrivateKeyType = PrivateKeyTy
},
});
return privateKey;
return buildOidcKeyFromRawString(privateKey);
}
throw new Error(`Unsupported private key ${String(type)}`);
};
export const generateOidcCookieKey = () => generateStandardSecret();
export const generateOidcCookieKey = () => buildOidcKeyFromRawString(generateStandardSecret());
export const buildOidcKeyFromRawString = (raw: string) => ({
id: generateStandardId(),
value: raw,
createdAt: Math.floor(Date.now() / 1000),
});

View file

@ -8,9 +8,9 @@ import { createLocalJWKSet } from 'jose';
import { exportJWK } from '#src/utils/jwks.js';
const loadOidcValues = async (issuer: string, configs: LogtoOidcConfigType) => {
const cookieKeys = configs[LogtoOidcConfigKey.CookieKeys];
const privateKeys = configs[LogtoOidcConfigKey.PrivateKeys].map((key) =>
crypto.createPrivateKey(key)
const cookieKeys = configs[LogtoOidcConfigKey.CookieKeys].map(({ value }) => value);
const privateKeys = configs[LogtoOidcConfigKey.PrivateKeys].map(({ value }) =>
crypto.createPrivateKey(value)
);
const publicKeys = privateKeys.map((key) => crypto.createPublicKey(key));
const privateJwks = await Promise.all(privateKeys.map(async (key) => exportJWK(key)));

View file

@ -41,7 +41,7 @@ export const getAdminTenantTokenValidationSet = async (): Promise<{
`);
const privateKeys = logtoOidcConfigGuard['oidc.privateKeys']
.parse(value)
.map((key) => crypto.createPrivateKey(key));
.map(({ value }) => crypto.createPrivateKey(value));
const publicKeys = privateKeys.map((key) => crypto.createPublicKey(key));
return {

View file

@ -0,0 +1,108 @@
import { generateStandardId } from '@logto/shared';
import type { DatabaseTransactionConnection } from 'slonik';
import { sql } from 'slonik';
import type { AlterationScript } from '../lib/types/alteration.js';
const targetConfigKeys = ['oidc.cookieKeys', 'oidc.privateKeys'];
type OldPrivateKeyData = {
tenantId: string;
value: string[];
};
type PrivateKey = {
id: string;
value: string;
createdAt: number;
};
type NewPrivateKeyData = {
tenantId: string;
value: PrivateKey[];
};
/**
* Alternate string array private signing keys to JSON array
* "oidc.cookieKeys": string[] -> PrivateKey[]
* "oidc.privateKeys": string[] -> PrivateKey[]
* @param configKey oidc.cookieKeys | oidc.privateKeys
* @param logtoConfig existing private key data for a specific tenant
* @param pool postgres database connection pool
*/
const alterPrivateKeysInLogtoConfig = async (
configKey: string,
logtoConfig: OldPrivateKeyData,
pool: DatabaseTransactionConnection
) => {
const { tenantId, value: oldPrivateKey } = logtoConfig;
// Use tenant creation time as `createdAt` timestamp for new private keys
const tenantData = await pool.maybeOne<{ createdAt: number }>(
sql`select * from tenants where id = ${tenantId}`
);
const newPrivateKeyData: PrivateKey[] = oldPrivateKey.map((key) => ({
id: generateStandardId(),
value: key,
createdAt: Math.floor((tenantData?.createdAt ?? Date.now()) / 1000),
}));
await pool.query(
sql`update logto_configs set value = ${JSON.stringify(
newPrivateKeyData
)} where tenant_id = ${tenantId} and key = ${configKey}`
);
};
/**
* Rollback JSON array private signing keys to string array
* "oidc.cookieKeys": PrivateKey[] -> string[]
* "oidc.privateKeys": PrivateKey[] -> string[]
* @param configKey oidc.cookieKeys | oidc.privateKeys
* @param logtoConfig new private key data for a specific tenant
* @param pool postgres database connection pool
*/
const rollbackPrivateKeysInLogtoConfig = async (
configKey: string,
logtoConfig: NewPrivateKeyData,
pool: DatabaseTransactionConnection
) => {
const { tenantId, value: newPrivateKeyData } = logtoConfig;
const oldPrivateKeys = newPrivateKeyData.map(({ value }) => value);
await pool.query(
sql`update logto_configs set value = ${JSON.stringify(
oldPrivateKeys
)} where tenant_id = ${tenantId} and key = ${configKey}`
);
};
const alteration: AlterationScript = {
up: async (pool) => {
await Promise.all(
targetConfigKeys.map(async (configKey) => {
const rows = await pool.many<OldPrivateKeyData>(
sql`select * from logto_configs where key = ${configKey}`
);
await Promise.all(
rows.map(async (row) => alterPrivateKeysInLogtoConfig(configKey, row, pool))
);
})
);
},
down: async (pool) => {
await Promise.all(
targetConfigKeys.map(async (configKey) => {
const rows = await pool.many<NewPrivateKeyData>(
sql`select * from logto_configs where key = ${configKey}`
);
await Promise.all(
rows.map(async (row) => rollbackPrivateKeysInLogtoConfig(configKey, row, pool))
);
})
);
},
};
export default alteration;

View file

@ -7,16 +7,24 @@ export enum LogtoOidcConfigKey {
CookieKeys = 'oidc.cookieKeys',
}
const oidcPrivateKeyGuard = z.object({
id: z.string(),
value: z.string(),
createdAt: z.number(),
});
export type PrivateKey = z.infer<typeof oidcPrivateKeyGuard>;
export type LogtoOidcConfigType = {
[LogtoOidcConfigKey.PrivateKeys]: string[];
[LogtoOidcConfigKey.CookieKeys]: string[];
[LogtoOidcConfigKey.PrivateKeys]: PrivateKey[];
[LogtoOidcConfigKey.CookieKeys]: PrivateKey[];
};
export const logtoOidcConfigGuard: Readonly<{
[key in LogtoOidcConfigKey]: ZodType<LogtoOidcConfigType[key]>;
}> = Object.freeze({
[LogtoOidcConfigKey.PrivateKeys]: z.string().array(),
[LogtoOidcConfigKey.CookieKeys]: z.string().array(),
[LogtoOidcConfigKey.PrivateKeys]: oidcPrivateKeyGuard.array(),
[LogtoOidcConfigKey.CookieKeys]: oidcPrivateKeyGuard.array(),
});
/* --- Logto tenant configs --- */