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

feat(core,console): enable backchannel logout

This commit is contained in:
Gao Sun 2024-06-07 22:12:24 +08:00
parent da5c71d916
commit f28a083ed0
No known key found for this signature in database
GPG key ID: 13EBE123E4773688
7 changed files with 105 additions and 17 deletions

View file

@ -54,6 +54,7 @@
"timestamptz", "timestamptz",
"topbar", "topbar",
"upsell", "upsell",
"withtyped" "withtyped",
"backchannel"
] ]
} }

View file

@ -0,0 +1,47 @@
import { type Application } from '@logto/schemas';
import { useFormContext } from 'react-hook-form';
import { Trans, useTranslation } from 'react-i18next';
import { z } from 'zod';
import FormCard from '@/components/FormCard';
import FormField from '@/ds-components/FormField';
import Switch from '@/ds-components/Switch';
import TextInput from '@/ds-components/TextInput';
function BackchannelLogout() {
const {
register,
formState: { errors },
} = useFormContext<Application>();
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
return (
<FormCard
title="application_details.backchannel_logout"
description="application_details.backchannel_logout_description"
learnMoreLink={{ href: 'https://openid.net/specs/openid-connect-backchannel-1_0-final.html' }}
>
<FormField title="application_details.backchannel_logout_uri">
<TextInput
error={errors.oidcClientMetadata?.backchannelLogoutUri?.message}
placeholder="https://your.website.com/backchannel_logout"
{...register('oidcClientMetadata.backchannelLogoutUri', {
validate: (value) =>
z.string().url().optional().safeParse(value).success ||
t('errors.invalid_uri_format'),
})}
/>
</FormField>
<FormField title="application_details.backchannel_logout_uri_session_required">
<Switch
label={
<Trans i18nKey="admin_console.application_details.backchannel_logout_uri_session_required_description" />
}
{...register('oidcClientMetadata.backchannelLogoutSessionRequired')}
/>
</FormField>
</FormCard>
);
}
export default BackchannelLogout;

View file

@ -36,6 +36,7 @@ import RefreshTokenSettings from './RefreshTokenSettings';
import Settings from './Settings'; import Settings from './Settings';
import * as styles from './index.module.scss'; import * as styles from './index.module.scss';
import { type ApplicationForm, applicationFormDataParser } from './utils'; import { type ApplicationForm, applicationFormDataParser } from './utils';
import BackchannelLogout from './BackchannelLogout';
type Props = { type Props = {
readonly data: ApplicationResponse; readonly data: ApplicationResponse;
@ -204,6 +205,7 @@ function ApplicationDetailsContent({ data, oidcConfig, onApplicationUpdated }: P
{![ApplicationType.MachineToMachine, ApplicationType.Protected].includes(data.type) && ( {![ApplicationType.MachineToMachine, ApplicationType.Protected].includes(data.type) && (
<RefreshTokenSettings data={data} /> <RefreshTokenSettings data={data} />
)} )}
<BackchannelLogout />
</DetailsForm> </DetailsForm>
</FormProvider> </FormProvider>
{tab === ApplicationDetailsTabs.Settings && ( {tab === ApplicationDetailsTabs.Settings && (

View file

@ -112,6 +112,7 @@ export default function initOidc(
introspection: { enabled: true }, introspection: { enabled: true },
devInteractions: { enabled: false }, devInteractions: { enabled: false },
clientCredentials: { enabled: true }, clientCredentials: { enabled: true },
backchannelLogout: { enabled: true },
rpInitiatedLogout: { rpInitiatedLogout: {
logoutSource: (ctx, form) => { logoutSource: (ctx, form) => {
// eslint-disable-next-line no-template-curly-in-string // eslint-disable-next-line no-template-curly-in-string

View file

@ -22,8 +22,8 @@ const application_details = {
application_name_placeholder: 'My App', application_name_placeholder: 'My App',
description: 'Description', description: 'Description',
description_placeholder: 'Enter your application description', description_placeholder: 'Enter your application description',
config_endpoint: 'OpenID Provider configuration endpoint', config_endpoint: 'OpenID provider configuration endpoint',
authorization_endpoint: 'Authorization Endpoint', authorization_endpoint: 'Authorization endpoint',
authorization_endpoint_tip: authorization_endpoint_tip:
"The endpoint to perform authentication and authorization. It's used for OpenID Connect <a>Authentication</a>.", "The endpoint to perform authentication and authorization. It's used for OpenID Connect <a>Authentication</a>.",
show_endpoint_details: 'Show endpoint details', show_endpoint_details: 'Show endpoint details',
@ -39,8 +39,8 @@ const application_details = {
redirect_uri_placeholder_native: 'io.logto://callback', redirect_uri_placeholder_native: 'io.logto://callback',
redirect_uri_tip: redirect_uri_tip:
'The URI redirects after a user sign-in (whether successful or not). See OpenID Connect <a>AuthRequest</a> for more info.', 'The URI redirects after a user sign-in (whether successful or not). See OpenID Connect <a>AuthRequest</a> for more info.',
post_sign_out_redirect_uri: 'Post Sign-out Redirect URI', post_sign_out_redirect_uri: 'Post sign-out redirect URI',
post_sign_out_redirect_uris: 'Post Sign-out Redirect URIs', post_sign_out_redirect_uris: 'Post sign-out redirect URIs',
post_sign_out_redirect_uri_placeholder: 'https://your.website.com/home', post_sign_out_redirect_uri_placeholder: 'https://your.website.com/home',
post_sign_out_redirect_uri_tip: post_sign_out_redirect_uri_tip:
'The URI redirects after a user sign-out (optional). It may have no practical effect in some app types.', 'The URI redirects after a user sign-out (optional). It may have no practical effect in some app types.',
@ -48,20 +48,27 @@ const application_details = {
cors_allowed_origins_placeholder: 'https://your.website.com', cors_allowed_origins_placeholder: 'https://your.website.com',
cors_allowed_origins_tip: cors_allowed_origins_tip:
'By default, all the origins of Redirect URIs will be allowed. Usually no action is required for this field. See the <a>MDN doc</a> for detailed info.', 'By default, all the origins of Redirect URIs will be allowed. Usually no action is required for this field. See the <a>MDN doc</a> for detailed info.',
token_endpoint: 'Token Endpoint', token_endpoint: 'Token endpoint',
user_info_endpoint: 'Userinfo Endpoint', user_info_endpoint: 'Userinfo endpoint',
enable_admin_access: 'Enable admin access', enable_admin_access: 'Enable admin access',
enable_admin_access_label: enable_admin_access_label:
'Enable or disable the access to Management API. Once enabled, you can use access tokens to call Management API on behalf on this application.', 'Enable or disable the access to Management API. Once enabled, you can use access tokens to call Management API on behalf on this application.',
always_issue_refresh_token: 'Always issue Refresh Token', always_issue_refresh_token: 'Always issue refresh token',
always_issue_refresh_token_label: always_issue_refresh_token_label:
'When enabled, Logto will always issue Refresh Tokens, regardless of whether `prompt=consent` is presented in the authentication request. However, this practice is discouraged unless necessary, as it is not compatible with OpenID Connect and may potentially cause issues.', 'When enabled, Logto will always issue refresh tokens, regardless of whether `prompt=consent` is presented in the authentication request. However, this practice is discouraged unless necessary, as it is not compatible with OpenID Connect and may potentially cause issues.',
refresh_token_ttl: 'Refresh Token Time to Live (TTL) in days', refresh_token_ttl: 'Refresh token time to live (TTL) in days',
refresh_token_ttl_tip: refresh_token_ttl_tip:
'The duration for which a Refresh Token can be used to request new access tokens before it expires and becomes invalid. Token requests will extend the TTL of the Refresh Token to this value.', 'The duration for which a refresh token can be used to request new access tokens before it expires and becomes invalid. Token requests will extend the TTL of the refresh token to this value.',
rotate_refresh_token: 'Rotate Refresh Token', rotate_refresh_token: 'Rotate refresh token',
rotate_refresh_token_label: rotate_refresh_token_label:
'When enabled, Logto will issue a new Refresh Token for token requests when 70% of the original Time to Live (TTL) has passed or certain conditions are met. <a>Learn more</a>', 'When enabled, Logto will issue a new refresh token for token requests when 70% of the original time to live (TTL) has passed or certain conditions are met. <a>Learn more</a>',
backchannel_logout: 'Backchannel Logout',
backchannel_logout_description:
'Configure the OpenID Connect backchannel logout endpoint and if session is required for this application.',
backchannel_logout_uri: 'Backchannel logout URI',
backchannel_logout_uri_session_required: 'Is session required?',
backchannel_logout_uri_session_required_description:
'When enabled, the RP requires that a `sid` (session ID) claim be included in the logout token to identify the RP session with the OP when the `backchannel_logout_uri` is used.',
delete_description: delete_description:
'This action cannot be undone. It will permanently delete the application. Please enter the application name <span>{{name}}</span> to confirm.', 'This action cannot be undone. It will permanently delete the application. Please enter the application name <span>{{name}}</span> to confirm.',
enter_your_application_name: 'Enter your application name', enter_your_application_name: 'Enter your application name',

View file

@ -1,6 +1,8 @@
import { validateRedirectUrl } from '@logto/core-kit'; import { validateRedirectUrl } from '@logto/core-kit';
import { z } from 'zod'; import { z } from 'zod';
import { type ToZodObject } from '../../utils/zod.js';
export const oidcModelInstancePayloadGuard = z export const oidcModelInstancePayloadGuard = z
.object({ .object({
userCode: z.string().optional(), userCode: z.string().optional(),
@ -15,6 +17,34 @@ export const oidcModelInstancePayloadGuard = z
export type OidcModelInstancePayload = z.infer<typeof oidcModelInstancePayloadGuard>; export type OidcModelInstancePayload = z.infer<typeof oidcModelInstancePayloadGuard>;
export type OidcClientMetadata = {
/**
* The redirect URIs that the client is allowed to use.
*
* @see {@link https://openid.net/specs/openid-connect-registration-1_0.html#ClientMetadata | OpenID Connect Dynamic Client Registration 1.0}
*/
redirectUris: string[];
/**
* The post-logout redirect URIs that the client is allowed to use.
*
* @see {@link https://openid.net/specs/openid-connect-rpinitiated-1_0.html#ClientMetadata | OpenID Connect RP-Initiated Logout 1.0}
*/
postLogoutRedirectUris: string[];
/**
* The URI for backchannel logout.
*
* @see {@link https://openid.net/specs/openid-connect-backchannel-1_0-final.html#BCRegistration | OpenID Connect Back-Channel Logout 1.0}
*/
backchannelLogoutUri?: string;
/**
* Whether the RP requires that a `sid` (session ID) Claim be included in the Logout Token.
*
* @see {@link https://openid.net/specs/openid-connect-backchannel-1_0-final.html#BCRegistration | OpenID Connect Back-Channel Logout 1.0}
*/
backchannelLogoutSessionRequired?: boolean;
logoUri?: string;
};
export const oidcClientMetadataGuard = z.object({ export const oidcClientMetadataGuard = z.object({
redirectUris: z redirectUris: z
.string() .string()
@ -22,10 +52,10 @@ export const oidcClientMetadataGuard = z.object({
.or(z.string().refine((url) => validateRedirectUrl(url, 'mobile'))) .or(z.string().refine((url) => validateRedirectUrl(url, 'mobile')))
.array(), .array(),
postLogoutRedirectUris: z.string().url().array(), postLogoutRedirectUris: z.string().url().array(),
backchannelLogoutUri: z.string().url().optional(),
backchannelLogoutSessionRequired: z.boolean().optional(),
logoUri: z.string().optional(), logoUri: z.string().optional(),
}); }) satisfies ToZodObject<OidcClientMetadata>;
export type OidcClientMetadata = z.infer<typeof oidcClientMetadataGuard>;
export enum CustomClientMetadataKey { export enum CustomClientMetadataKey {
CorsAllowedOrigins = 'corsAllowedOrigins', CorsAllowedOrigins = 'corsAllowedOrigins',

View file

@ -1,5 +1,5 @@
import { type z } from 'zod'; import { type z } from 'zod';
export type ToZodObject<T> = z.ZodObject<{ export type ToZodObject<T> = z.ZodObject<{
[K in keyof T]: z.ZodType<T[K]>; [K in keyof T]-?: z.ZodType<T[K]>;
}>; }>;