0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-30 20:33:54 -05:00

refactor(schemas,core,console): unify the usage of OIDC "keyType" in both frontend and backend (#4670)

This commit is contained in:
Charles Zhao 2023-10-16 22:27:46 -05:00 committed by GitHub
parent 81e8a2641a
commit b991597342
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 57 additions and 40 deletions

View file

@ -1,7 +1,7 @@
import { import {
LogtoOidcConfigKey,
SupportedSigningKeyAlgorithm, SupportedSigningKeyAlgorithm,
type OidcConfigKeysResponse, type OidcConfigKeysResponse,
LogtoOidcConfigKeyType,
} from '@logto/schemas'; } from '@logto/schemas';
import { condArray } from '@silverhand/essentials'; import { condArray } from '@silverhand/essentials';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
@ -26,9 +26,10 @@ import * as styles from './index.module.scss';
function SigningKeys() { function SigningKeys() {
const api = useApi(); const api = useApi();
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console.tenants.signing_keys' }); const { t } = useTranslation(undefined, { keyPrefix: 'admin_console.tenants.signing_keys' });
const [activeTab, setActiveTab] = useState<LogtoOidcConfigKey>(LogtoOidcConfigKey.PrivateKeys); const [keyType, setKeyType] = useState<LogtoOidcConfigKeyType>(
const isPrivateKey = activeTab === LogtoOidcConfigKey.PrivateKeys; LogtoOidcConfigKeyType.PrivateKeys
const keyType = isPrivateKey ? 'private-keys' : 'cookie-keys'; );
const isPrivateKey = keyType === LogtoOidcConfigKeyType.PrivateKeys;
const { data, error, mutate } = useSWR<OidcConfigKeysResponse[], RequestError>( const { data, error, mutate } = useSWR<OidcConfigKeysResponse[], RequestError>(
`api/configs/oidc/${keyType}` `api/configs/oidc/${keyType}`
@ -97,17 +98,17 @@ function SigningKeys() {
<FormCard title="tenants.signing_keys.title" description="tenants.signing_keys.description"> <FormCard title="tenants.signing_keys.title" description="tenants.signing_keys.description">
<TabNav> <TabNav>
<TabNavItem <TabNavItem
isActive={activeTab === LogtoOidcConfigKey.PrivateKeys} isActive={keyType === LogtoOidcConfigKeyType.PrivateKeys}
onClick={() => { onClick={() => {
setActiveTab(LogtoOidcConfigKey.PrivateKeys); setKeyType(LogtoOidcConfigKeyType.PrivateKeys);
}} }}
> >
<DynamicT forKey="tenants.signing_keys.type.private_key" /> <DynamicT forKey="tenants.signing_keys.type.private_key" />
</TabNavItem> </TabNavItem>
<TabNavItem <TabNavItem
isActive={activeTab === LogtoOidcConfigKey.CookieKeys} isActive={keyType === LogtoOidcConfigKeyType.CookieKeys}
onClick={() => { onClick={() => {
setActiveTab(LogtoOidcConfigKey.CookieKeys); setKeyType(LogtoOidcConfigKeyType.CookieKeys);
}} }}
> >
<DynamicT forKey="tenants.signing_keys.type.cookie_key" /> <DynamicT forKey="tenants.signing_keys.type.cookie_key" />

View file

@ -11,6 +11,7 @@ import {
SupportedSigningKeyAlgorithm, SupportedSigningKeyAlgorithm,
type OidcConfigKeysResponse, type OidcConfigKeysResponse,
type OidcConfigKey, type OidcConfigKey,
LogtoOidcConfigKeyType,
} from '@logto/schemas'; } from '@logto/schemas';
import { z } from 'zod'; import { z } from 'zod';
@ -20,19 +21,11 @@ import { exportJWK } from '#src/utils/jwks.js';
import type { AuthedRouter, RouterInitArgs } from './types.js'; import type { AuthedRouter, RouterInitArgs } from './types.js';
/*
* Logto OIDC private key type used in API routes
*/
enum LogtoOidcPrivateKeyType {
PrivateKeys = 'private-keys',
CookieKeys = 'cookie-keys',
}
/** /**
* Provide a simple API router key type and DB column mapping * Provide a simple API router key type and DB config key mapping
*/ */
const getOidcConfigKeyDatabaseColumnName = (key: LogtoOidcPrivateKeyType): LogtoOidcConfigKey => const getOidcConfigKeyDatabaseColumnName = (key: LogtoOidcConfigKeyType): LogtoOidcConfigKey =>
key === LogtoOidcPrivateKeyType.PrivateKeys key === LogtoOidcConfigKeyType.PrivateKeys
? LogtoOidcConfigKey.PrivateKeys ? LogtoOidcConfigKey.PrivateKeys
: LogtoOidcConfigKey.CookieKeys; : LogtoOidcConfigKey.CookieKeys;
@ -105,7 +98,7 @@ export default function logtoConfigRoutes<T extends AuthedRouter>(
'/configs/oidc/:keyType', '/configs/oidc/:keyType',
koaGuard({ koaGuard({
params: z.object({ params: z.object({
keyType: z.nativeEnum(LogtoOidcPrivateKeyType), keyType: z.nativeEnum(LogtoOidcConfigKeyType),
}), }),
response: z.array(oidcConfigKeysResponseGuard), response: z.array(oidcConfigKeysResponseGuard),
status: [200, 404], status: [200, 404],
@ -131,7 +124,7 @@ export default function logtoConfigRoutes<T extends AuthedRouter>(
'/configs/oidc/:keyType/:keyId', '/configs/oidc/:keyType/:keyId',
koaGuard({ koaGuard({
params: z.object({ params: z.object({
keyType: z.nativeEnum(LogtoOidcPrivateKeyType), keyType: z.nativeEnum(LogtoOidcConfigKeyType),
keyId: z.string(), keyId: z.string(),
}), }),
status: [204, 404, 422], status: [204, 404, 422],
@ -171,7 +164,7 @@ export default function logtoConfigRoutes<T extends AuthedRouter>(
'/configs/oidc/:keyType/rotate', '/configs/oidc/:keyType/rotate',
koaGuard({ koaGuard({
params: z.object({ params: z.object({
keyType: z.nativeEnum(LogtoOidcPrivateKeyType), keyType: z.nativeEnum(LogtoOidcConfigKeyType),
}), }),
body: z.object({ body: z.object({
signingKeyAlgorithm: z.nativeEnum(SupportedSigningKeyAlgorithm).optional(), signingKeyAlgorithm: z.nativeEnum(SupportedSigningKeyAlgorithm).optional(),

View file

@ -2,6 +2,7 @@ import {
SupportedSigningKeyAlgorithm, SupportedSigningKeyAlgorithm,
type AdminConsoleData, type AdminConsoleData,
type OidcConfigKeysResponse, type OidcConfigKeysResponse,
type LogtoOidcConfigKeyType,
} from '@logto/schemas'; } from '@logto/schemas';
import { authedAdminApi } from './api.js'; import { authedAdminApi } from './api.js';
@ -16,14 +17,14 @@ export const updateAdminConsoleConfig = async (payload: Partial<AdminConsoleData
}) })
.json<AdminConsoleData>(); .json<AdminConsoleData>();
export const getOidcKeys = async (keyType: 'private-keys' | 'cookie-keys') => export const getOidcKeys = async (keyType: LogtoOidcConfigKeyType) =>
authedAdminApi.get(`configs/oidc/${keyType}`).json<OidcConfigKeysResponse[]>(); authedAdminApi.get(`configs/oidc/${keyType}`).json<OidcConfigKeysResponse[]>();
export const deleteOidcKey = async (keyType: 'private-keys' | 'cookie-keys', id: string) => export const deleteOidcKey = async (keyType: LogtoOidcConfigKeyType, id: string) =>
authedAdminApi.delete(`configs/oidc/${keyType}/${id}`); authedAdminApi.delete(`configs/oidc/${keyType}/${id}`);
export const rotateOidcKeys = async ( export const rotateOidcKeys = async (
keyType: 'private-keys' | 'cookie-keys', keyType: LogtoOidcConfigKeyType,
signingKeyAlgorithm: SupportedSigningKeyAlgorithm = SupportedSigningKeyAlgorithm.EC signingKeyAlgorithm: SupportedSigningKeyAlgorithm = SupportedSigningKeyAlgorithm.EC
) => ) =>
authedAdminApi authedAdminApi

View file

@ -1,4 +1,8 @@
import { SupportedSigningKeyAlgorithm, type AdminConsoleData } from '@logto/schemas'; import {
SupportedSigningKeyAlgorithm,
type AdminConsoleData,
LogtoOidcConfigKeyType,
} from '@logto/schemas';
import { import {
deleteOidcKey, deleteOidcKey,
@ -33,8 +37,8 @@ describe('admin console sign-in experience', () => {
}); });
it('should get OIDC keys successfully', async () => { it('should get OIDC keys successfully', async () => {
const privateKeys = await getOidcKeys('private-keys'); const privateKeys = await getOidcKeys(LogtoOidcConfigKeyType.PrivateKeys);
const cookieKeys = await getOidcKeys('cookie-keys'); const cookieKeys = await getOidcKeys(LogtoOidcConfigKeyType.CookieKeys);
expect(privateKeys).toHaveLength(1); expect(privateKeys).toHaveLength(1);
expect(privateKeys[0]).toMatchObject( expect(privateKeys[0]).toMatchObject(
@ -49,24 +53,27 @@ describe('admin console sign-in experience', () => {
}); });
it('should not be able to delete the only private key', async () => { it('should not be able to delete the only private key', async () => {
const privateKeys = await getOidcKeys('private-keys'); const privateKeys = await getOidcKeys(LogtoOidcConfigKeyType.PrivateKeys);
expect(privateKeys).toHaveLength(1); expect(privateKeys).toHaveLength(1);
await expectRejects(deleteOidcKey('private-keys', privateKeys[0]!.id), { await expectRejects(deleteOidcKey(LogtoOidcConfigKeyType.PrivateKeys, privateKeys[0]!.id), {
code: 'oidc.key_required', code: 'oidc.key_required',
statusCode: 422, statusCode: 422,
}); });
const cookieKeys = await getOidcKeys('cookie-keys'); const cookieKeys = await getOidcKeys(LogtoOidcConfigKeyType.CookieKeys);
expect(cookieKeys).toHaveLength(1); expect(cookieKeys).toHaveLength(1);
await expectRejects(deleteOidcKey('cookie-keys', cookieKeys[0]!.id), { await expectRejects(deleteOidcKey(LogtoOidcConfigKeyType.CookieKeys, cookieKeys[0]!.id), {
code: 'oidc.key_required', code: 'oidc.key_required',
statusCode: 422, statusCode: 422,
}); });
}); });
it('should rotate OIDC keys successfully', async () => { it('should rotate OIDC keys successfully', async () => {
const existingPrivateKeys = await getOidcKeys('private-keys'); const existingPrivateKeys = await getOidcKeys(LogtoOidcConfigKeyType.PrivateKeys);
const newPrivateKeys = await rotateOidcKeys('private-keys', SupportedSigningKeyAlgorithm.RSA); const newPrivateKeys = await rotateOidcKeys(
LogtoOidcConfigKeyType.PrivateKeys,
SupportedSigningKeyAlgorithm.RSA
);
expect(newPrivateKeys).toHaveLength(2); expect(newPrivateKeys).toHaveLength(2);
expect(newPrivateKeys).toMatchObject([ expect(newPrivateKeys).toMatchObject([
@ -77,8 +84,8 @@ describe('admin console sign-in experience', () => {
]); ]);
expect(newPrivateKeys[1]?.id).toBe(existingPrivateKeys[0]?.id); expect(newPrivateKeys[1]?.id).toBe(existingPrivateKeys[0]?.id);
const existingCookieKeys = await getOidcKeys('cookie-keys'); const existingCookieKeys = await getOidcKeys(LogtoOidcConfigKeyType.CookieKeys);
const newCookieKeys = await rotateOidcKeys('cookie-keys'); const newCookieKeys = await rotateOidcKeys(LogtoOidcConfigKeyType.CookieKeys);
expect(newCookieKeys).toHaveLength(2); expect(newCookieKeys).toHaveLength(2);
expect(newCookieKeys).toMatchObject([ expect(newCookieKeys).toMatchObject([
@ -91,7 +98,7 @@ describe('admin console sign-in experience', () => {
}); });
it('should only keep 2 recent OIDC keys', async () => { it('should only keep 2 recent OIDC keys', async () => {
const privateKeys = await rotateOidcKeys('private-keys'); // Defaults to 'EC' algorithm const privateKeys = await rotateOidcKeys(LogtoOidcConfigKeyType.PrivateKeys); // Defaults to 'EC' algorithm
expect(privateKeys).toHaveLength(2); expect(privateKeys).toHaveLength(2);
expect(privateKeys).toMatchObject([ expect(privateKeys).toMatchObject([
@ -101,7 +108,10 @@ describe('admin console sign-in experience', () => {
{ id: expect.any(String), signingKeyAlgorithm: 'RSA', createdAt: expect.any(Number) }, { id: expect.any(String), signingKeyAlgorithm: 'RSA', createdAt: expect.any(Number) },
]); ]);
const privateKeys2 = await rotateOidcKeys('private-keys', SupportedSigningKeyAlgorithm.RSA); const privateKeys2 = await rotateOidcKeys(
LogtoOidcConfigKeyType.PrivateKeys,
SupportedSigningKeyAlgorithm.RSA
);
expect(privateKeys2).toHaveLength(2); expect(privateKeys2).toHaveLength(2);
expect(privateKeys2).toMatchObject([ expect(privateKeys2).toMatchObject([

View file

@ -1,13 +1,25 @@
import type { ZodType } from 'zod'; import type { ZodType } from 'zod';
import { z } from 'zod'; import { z } from 'zod';
/* --- Logto OIDC configs --- */ /**
* Logto OIDC signing key types, used mainly in REST API routes.
*/
export enum LogtoOidcConfigKeyType {
PrivateKeys = 'private-keys',
CookieKeys = 'cookie-keys',
}
/**
* Value maps to config key names in `logto_configs` table. Used mainly in DB SQL related scenarios.
*/
export enum LogtoOidcConfigKey { export enum LogtoOidcConfigKey {
PrivateKeys = 'oidc.privateKeys', PrivateKeys = 'oidc.privateKeys',
CookieKeys = 'oidc.cookieKeys', CookieKeys = 'oidc.cookieKeys',
} }
/* --- Logto supported JWK signing key types --- */ /**
* Logto supported signing key algorithms for OIDC private keys that sign JWT tokens.
*/
export enum SupportedSigningKeyAlgorithm { export enum SupportedSigningKeyAlgorithm {
RSA = 'RSA', RSA = 'RSA',
EC = 'EC', EC = 'EC',