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:
parent
f53778894d
commit
46d0d4c0b9
7 changed files with 165 additions and 23 deletions
7
.changeset/lucky-brooms-hide.md
Normal file
7
.changeset/lucky-brooms-hide.md
Normal 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.
|
|
@ -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 };
|
||||
},
|
||||
|
|
|
@ -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),
|
||||
});
|
||||
|
|
|
@ -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)));
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
|
@ -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 --- */
|
||||
|
|
Loading…
Add table
Reference in a new issue