Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(console): support signing-key rotation for oss version #5559

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/curvy-paws-breathe.md
xiaoyijun marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@logto/console": minor
"@logto/phrases": minor
---

feat(console): support signing-key rotation
3 changes: 3 additions & 0 deletions packages/console/src/assets/icons/key.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions packages/console/src/consts/external-links.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ export const logtoThirdPartyGuideLink = '/docs/recipes/logto-as-idp/';
export const logtoThirdPartyAppPermissionsLink =
'/docs/recipes/logto-as-idp/permissions-management/';
export const logtoThirdPartyAppBrandingLink = '/docs/recipes/logto-as-idp/branding-customization/';
export const signingKeysLink = '/docs/recipes/openid-connect/signing-keys-rotation/';
xiaoyijun marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import Connection from '@/assets/icons/connection.svg';
import Gear from '@/assets/icons/gear.svg';
import Hook from '@/assets/icons/hook.svg';
import JwtClaims from '@/assets/icons/jwt-claims.svg';
import Key from '@/assets/icons/key.svg';
import List from '@/assets/icons/list.svg';
import Organization from '@/assets/icons/organization.svg';
import UserProfile from '@/assets/icons/profile.svg';
Expand Down Expand Up @@ -116,6 +117,10 @@ export const useSidebarMenuItems = (): {
{
title: 'developer',
items: [
{
Icon: Key,
title: 'signing_keys',
},
{
Icon: JwtClaims,
title: 'jwt_customizer',
Expand Down
2 changes: 2 additions & 0 deletions packages/console/src/containers/ConsoleContent/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import RoleUsers from '@/pages/RoleDetails/RoleUsers';
import Roles from '@/pages/Roles';
import SignInExperience from '@/pages/SignInExperience';
import { SignInExperienceTab } from '@/pages/SignInExperience/types';
import SigningKeys from '@/pages/SigningKeys';
import TenantSettings from '@/pages/TenantSettings';
import BillingHistory from '@/pages/TenantSettings/BillingHistory';
import Subscription from '@/pages/TenantSettings/Subscription';
Expand Down Expand Up @@ -193,6 +194,7 @@ function ConsoleContent() {
<Route path="link-email" element={<LinkEmailModal />} />
<Route path="verification-code" element={<VerificationCodeModal />} />
</Route>
<Route path="signing-keys" element={<SigningKeys />} />
{isCloud && (
<Route path="tenant-settings" element={<TenantSettings />}>
<Route index element={<Navigate replace to={TenantSettingsTabs.Settings} />} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,25 @@ import Delete from '@/assets/icons/delete.svg';
import FormCard from '@/components/FormCard';
import Button from '@/ds-components/Button';
import DangerConfirmModal from '@/ds-components/DeleteConfirmModal';
import DynamicT from '@/ds-components/DynamicT';
import FormField from '@/ds-components/FormField';
import IconButton from '@/ds-components/IconButton';
import Select from '@/ds-components/Select';
import TabNav, { TabNavItem } from '@/ds-components/TabNav';
import Table from '@/ds-components/Table';
import Tag from '@/ds-components/Tag';
import useApi, { type RequestError } from '@/hooks/use-api';

import * as styles from './index.module.scss';

function SigningKeys() {
type Props = {
keyType: LogtoOidcConfigKeyType;
};

function SigningKeyFormCard({ keyType }: Props) {
const api = useApi();
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console.tenants.signing_keys' });
const [keyType, setKeyType] = useState<LogtoOidcConfigKeyType>(
LogtoOidcConfigKeyType.PrivateKeys
);
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console.signing_keys' });

const isPrivateKey = keyType === LogtoOidcConfigKeyType.PrivateKeys;
const keyTypePhrase = isPrivateKey ? 'private' : 'cookie';
xiaoyijun marked this conversation as resolved.
Show resolved Hide resolved

const { data, error, mutate } = useSWR<OidcConfigKeysResponse[], RequestError>(
`api/configs/oidc/${keyType}`
Expand Down Expand Up @@ -96,26 +97,11 @@ function SigningKeys() {
);

return (
<FormCard title="tenants.signing_keys.title" description="tenants.signing_keys.description">
<TabNav>
<TabNavItem
isActive={keyType === LogtoOidcConfigKeyType.PrivateKeys}
onClick={() => {
setKeyType(LogtoOidcConfigKeyType.PrivateKeys);
}}
>
<DynamicT forKey="tenants.signing_keys.type.private_key" />
</TabNavItem>
<TabNavItem
isActive={keyType === LogtoOidcConfigKeyType.CookieKeys}
onClick={() => {
setKeyType(LogtoOidcConfigKeyType.CookieKeys);
}}
>
<DynamicT forKey="tenants.signing_keys.type.cookie_key" />
</TabNavItem>
</TabNav>
<FormField title={`tenants.signing_keys.${isPrivateKey ? 'private' : 'cookie'}_keys_in_use`}>
<FormCard
title={`signing_keys.${keyTypePhrase}_key`}
description={`signing_keys.${keyTypePhrase}_keys_description`}
>
<FormField title={`signing_keys.${keyTypePhrase}_keys_in_use`}>
<Table
hasBorder
isRowHoverEffectDisabled
Expand All @@ -126,13 +112,11 @@ function SigningKeys() {
columns={tableColumns}
/>
</FormField>
<FormField title={`tenants.signing_keys.rotate_${isPrivateKey ? 'private' : 'cookie'}_keys`}>
<FormField title={`signing_keys.rotate_${keyTypePhrase}_keys`}>
<div className={styles.rotateKey}>
<div className={styles.description}>
{t(`rotate_${isPrivateKey ? 'private' : 'cookie'}_keys_description`)}
</div>
<div className={styles.description}>{t(`rotate_${keyTypePhrase}_keys_description`)}</div>
<Button
title={`tenants.signing_keys.rotate_${isPrivateKey ? 'private' : 'cookie'}_keys`}
title={`signing_keys.rotate_${keyTypePhrase}_keys`}
type="default"
onClick={() => {
setShowRotateConfirmModal(true);
Expand All @@ -141,7 +125,7 @@ function SigningKeys() {
</div>
</FormField>
<DangerConfirmModal
confirmButtonText="tenants.signing_keys.rotate_button"
confirmButtonText="signing_keys.rotate_button"
isOpen={showRotateConfirmModal}
onCancel={() => {
setShowRotateConfirmModal(false);
Expand All @@ -164,11 +148,11 @@ function SigningKeys() {
>
<span>
<Trans components={{ strong: <strong /> }}>
{t(`reminder.rotate_${isPrivateKey ? 'private' : 'cookie'}_key`)}
{t(`reminder.rotate_${keyTypePhrase}_key`)}
</Trans>
</span>
{isPrivateKey && (
<FormField title="tenants.signing_keys.select_private_key_algorithm">
<FormField title="signing_keys.select_private_key_algorithm">
<Select
options={Object.values(SupportedSigningKeyAlgorithm).map((value) => ({
title: value,
Expand Down Expand Up @@ -205,12 +189,12 @@ function SigningKeys() {
>
<span>
<Trans components={{ strong: <strong /> }}>
{t(`reminder.delete_${isPrivateKey ? 'private' : 'cookie'}_key`)}
{t(`reminder.delete_${keyTypePhrase}_key`)}
</Trans>
</span>
</DangerConfirmModal>
</FormCard>
);
}

export default SigningKeys;
export default SigningKeyFormCard;
14 changes: 14 additions & 0 deletions packages/console/src/pages/SigningKeys/index.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
@use '@/scss/underscore' as _;

.container {
display: flex;
flex-direction: column;
min-height: 100%;
gap: _.unit(4);
padding-bottom: _.unit(6);

.header {
flex-shrink: 0;
}
}

30 changes: 30 additions & 0 deletions packages/console/src/pages/SigningKeys/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { withAppInsights } from '@logto/app-insights/react/AppInsightsReact';
import { LogtoOidcConfigKeyType } from '@logto/schemas';

import PageMeta from '@/components/PageMeta';
import { signingKeysLink } from '@/consts';
import CardTitle from '@/ds-components/CardTitle';
import useDocumentationUrl from '@/hooks/use-documentation-url';

import SigningKeyFormCard from './SigningKeyFormCard';
import * as styles from './index.module.scss';

function SigningKeys() {
const { getDocumentationUrl } = useDocumentationUrl();

return (
<div className={styles.container}>
<PageMeta titleKey="signing_keys.title" />
<CardTitle
title="signing_keys.title"
subtitle="signing_keys.description"
learnMoreLink={{ href: getDocumentationUrl(signingKeysLink), targetBlank: 'noopener' }}
className={styles.header}
/>
<SigningKeyFormCard keyType={LogtoOidcConfigKeyType.PrivateKeys} />
<SigningKeyFormCard keyType={LogtoOidcConfigKeyType.CookieKeys} />
</div>
);
}

export default withAppInsights(SigningKeys);
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import { trySubmitSafe } from '@/utils/form';
import DeleteCard from './DeleteCard';
import DeleteModal from './DeleteModal';
import ProfileForm from './ProfileForm';
import SigningKeys from './SigningKeys';
import * as styles from './index.module.scss';
import { type TenantSettingsForm } from './types.js';

Expand Down Expand Up @@ -121,7 +120,6 @@ function TenantBasicSettings() {
<FormProvider {...methods}>
<div className={styles.fields}>
<ProfileForm currentTenantId={currentTenantId} />
<SigningKeys />
<DeleteCard currentTenantId={currentTenantId} onClick={onClickDeletionButton} />
</div>
</FormProvider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import role_details from './role-details.js';
import roles from './roles.js';
import session_expired from './session-expired.js';
import sign_in_exp from './sign-in-exp/index.js';
import signing_keys from './signing-keys.js';
import subscription from './subscription/index.js';
import tab_sections from './tab-sections.js';
import tabs from './tabs.js';
Expand Down Expand Up @@ -91,6 +92,7 @@ const admin_console = {
protected_app,
jwt_claims,
invitation,
signing_keys,
};

export default Object.freeze(admin_console);
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
const signing_keys = {
title: 'Signierschlüssel',
description:
'Verwalten Sie sicher die Signierschlüssel, die von Ihren Anwendungen verwendet werden.',
private_key: 'OIDC-Privatschlüssel',
private_keys_description: 'OIDC-Privatschlüssel werden zum Signieren von JWT-Token verwendet.',
cookie_key: 'OIDC-Cookieschlüssel',
cookie_keys_description: 'OIDC-Cookieschlüssel werden zum Signieren von Cookies verwendet.',
private_keys_in_use: 'Verwendete private Schlüssel',
cookie_keys_in_use: 'Verwendete Cookie-Schlüssel',
rotate_private_keys: 'Private Schlüssel rotieren',
rotate_cookie_keys: 'Cookie-Schlüssel rotieren',
rotate_private_keys_description:
'Diese Aktion erstellt einen neuen privaten Signierungsschlüssel, rotiert den aktuellen Schlüssel und entfernt Ihren vorherigen Schlüssel. Ihre JWT-Token, die mit dem aktuellen Schlüssel signiert sind, bleiben gültig, bis sie gelöscht oder erneut rotiert werden.',
rotate_cookie_keys_description:
'Diese Aktion erstellt einen neuen Cookie-Schlüssel, rotiert den aktuellen Schlüssel und entfernt Ihren vorherigen Schlüssel. Ihre Cookies mit dem aktuellen Schlüssel bleiben gültig, bis sie gelöscht oder erneut rotiert werden.',
select_private_key_algorithm: 'Signierungsalgorithmus für den neuen privaten Schlüssel auswählen',
rotate_button: 'Rotieren',
table_column: {
id: 'ID',
status: 'Status',
algorithm: 'Signierungsschlüssel Algorithmus',
},
status: {
current: 'Aktuell',
previous: 'Vorherige',
},
reminder: {
rotate_private_key:
'Sind Sie sicher, dass Sie die <strong>OIDC-Private Keys</strong> rotieren möchten? Neue ausgestellte JWT-Token werden vom neuen Schlüssel signiert. Bestehende JWT-Token bleiben gültig, bis Sie sie erneut rotieren.',
rotate_cookie_key:
'Sind Sie sicher, dass Sie die <strong>OIDC-Cookie-Keys</strong> rotieren möchten? Neue Cookies, die in Anmelde-Sitzungen generiert werden, werden mit dem neuen Cookie-Schlüssel signiert. Bestehende Cookies bleiben gültig, bis Sie sie erneut rotieren.',
delete_private_key:
'Sind Sie sicher, dass Sie den <strong>OIDC-Privaten Schlüssel</strong> löschen möchten? Bestehende JWT-Token, die mit diesem privaten Signierungsschlüssel signiert wurden, sind nicht mehr gültig.',
delete_cookie_key:
'Sind Sie sicher, dass Sie den <strong>OIDC-Cookie-Schlüssel</strong> löschen möchten? Ältere Anmelde-Sitzungen mit Cookies, die mit diesem Cookie-Schlüssel signiert wurden, sind nicht mehr gültig. Eine erneute Authentifizierung ist für diese Benutzer erforderlich.',
},
messages: {
rotate_key_success: 'Signierungsschlüssel erfolgreich rotieren.',
delete_key_success: 'Schlüssel erfolgreich gelöscht.',
},
};

export default Object.freeze(signing_keys);
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const tabs = {
mfa: 'Multi-Faktor-Authentifizierung',
/** UNTRANSLATED */
jwt_customizer: 'JWT Claims',
signing_keys: 'Signierschlüssel',
};

export default Object.freeze(tabs);
Original file line number Diff line number Diff line change
Expand Up @@ -112,48 +112,6 @@ const tenants = {
description_2:
'Wenn Sie weitere Informationen wünschen, Bedenken haben oder die volle Funktionalität wiederherstellen und Ihre Mieter entsperren möchten, zögern Sie nicht, uns umgehend zu kontaktieren.',
},
signing_keys: {
title: 'SIGNIERUNGSSCHLÜSSEL',
description: 'Sicherer Umgang mit Signierungsschlüsseln in Ihrem Mandanten.',
type: {
private_key: 'OIDC-Private Keys',
cookie_key: 'OIDC-Cookie-Keys',
},
private_keys_in_use: 'Verwendete private Schlüssel',
cookie_keys_in_use: 'Verwendete Cookie-Schlüssel',
rotate_private_keys: 'Private Schlüssel rotieren',
rotate_cookie_keys: 'Cookie-Schlüssel rotieren',
rotate_private_keys_description:
'Diese Aktion erstellt einen neuen privaten Signierungsschlüssel, rotiert den aktuellen Schlüssel und entfernt Ihren vorherigen Schlüssel. Ihre JWT-Token, die mit dem aktuellen Schlüssel signiert sind, bleiben gültig, bis sie gelöscht oder erneut rotiert werden.',
rotate_cookie_keys_description:
'Diese Aktion erstellt einen neuen Cookie-Schlüssel, rotiert den aktuellen Schlüssel und entfernt Ihren vorherigen Schlüssel. Ihre Cookies mit dem aktuellen Schlüssel bleiben gültig, bis sie gelöscht oder erneut rotiert werden.',
select_private_key_algorithm:
'Signierungsalgorithmus für den neuen privaten Schlüssel auswählen',
rotate_button: 'Rotieren',
table_column: {
id: 'ID',
status: 'Status',
algorithm: 'Signierungsschlüssel Algorithmus',
},
status: {
current: 'Aktuell',
previous: 'Vorherige',
},
reminder: {
rotate_private_key:
'Sind Sie sicher, dass Sie die <strong>OIDC-Private Keys</strong> rotieren möchten? Neue ausgestellte JWT-Token werden vom neuen Schlüssel signiert. Bestehende JWT-Token bleiben gültig, bis Sie sie erneut rotieren.',
rotate_cookie_key:
'Sind Sie sicher, dass Sie die <strong>OIDC-Cookie-Keys</strong> rotieren möchten? Neue Cookies, die in Anmelde-Sitzungen generiert werden, werden mit dem neuen Cookie-Schlüssel signiert. Bestehende Cookies bleiben gültig, bis Sie sie erneut rotieren.',
delete_private_key:
'Sind Sie sicher, dass Sie den <strong>OIDC-Privaten Schlüssel</strong> löschen möchten? Bestehende JWT-Token, die mit diesem privaten Signierungsschlüssel signiert wurden, sind nicht mehr gültig.',
delete_cookie_key:
'Sind Sie sicher, dass Sie den <strong>OIDC-Cookie-Schlüssel</strong> löschen möchten? Ältere Anmelde-Sitzungen mit Cookies, die mit diesem Cookie-Schlüssel signiert wurden, sind nicht mehr gültig. Eine erneute Authentifizierung ist für diese Benutzer erforderlich.',
},
messages: {
rotate_key_success: 'Signierungsschlüssel erfolgreich rotieren.',
delete_key_success: 'Schlüssel erfolgreich gelöscht.',
},
},
};

export default Object.freeze(tenants);
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import role_details from './role-details.js';
import roles from './roles.js';
import session_expired from './session-expired.js';
import sign_in_exp from './sign-in-exp/index.js';
import signing_keys from './signing-keys.js';
import subscription from './subscription/index.js';
import tab_sections from './tab-sections.js';
import tabs from './tabs.js';
Expand Down Expand Up @@ -91,6 +92,7 @@ const admin_console = {
protected_app,
jwt_claims,
invitation,
signing_keys,
};

export default Object.freeze(admin_console);
Loading
Loading