diff --git a/.vscode/settings.json b/.vscode/settings.json index b83dc8dfa..805805faa 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -54,6 +54,7 @@ "timestamptz", "topbar", "upsell", - "withtyped" + "withtyped", + "backchannel" ] } diff --git a/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/BackchannelLogout.tsx b/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/BackchannelLogout.tsx new file mode 100644 index 000000000..53e3bb598 --- /dev/null +++ b/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/BackchannelLogout.tsx @@ -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(); + const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); + + return ( + + + + z.string().url().optional().safeParse(value).success || + t('errors.invalid_uri_format'), + })} + /> + + + + } + {...register('oidcClientMetadata.backchannelLogoutSessionRequired')} + /> + + + ); +} + +export default BackchannelLogout; diff --git a/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/index.tsx b/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/index.tsx index 569aa0474..d2caafbb9 100644 --- a/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/index.tsx +++ b/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/index.tsx @@ -36,6 +36,7 @@ import RefreshTokenSettings from './RefreshTokenSettings'; import Settings from './Settings'; import * as styles from './index.module.scss'; import { type ApplicationForm, applicationFormDataParser } from './utils'; +import BackchannelLogout from './BackchannelLogout'; type Props = { readonly data: ApplicationResponse; @@ -204,6 +205,7 @@ function ApplicationDetailsContent({ data, oidcConfig, onApplicationUpdated }: P {![ApplicationType.MachineToMachine, ApplicationType.Protected].includes(data.type) && ( )} + {tab === ApplicationDetailsTabs.Settings && ( diff --git a/packages/core/src/oidc/init.ts b/packages/core/src/oidc/init.ts index 14e0ef15d..22ce03e63 100644 --- a/packages/core/src/oidc/init.ts +++ b/packages/core/src/oidc/init.ts @@ -112,6 +112,7 @@ export default function initOidc( introspection: { enabled: true }, devInteractions: { enabled: false }, clientCredentials: { enabled: true }, + backchannelLogout: { enabled: true }, rpInitiatedLogout: { logoutSource: (ctx, form) => { // eslint-disable-next-line no-template-curly-in-string diff --git a/packages/phrases/src/locales/en/translation/admin-console/application-details.ts b/packages/phrases/src/locales/en/translation/admin-console/application-details.ts index 1517550b3..f9ef9377f 100644 --- a/packages/phrases/src/locales/en/translation/admin-console/application-details.ts +++ b/packages/phrases/src/locales/en/translation/admin-console/application-details.ts @@ -22,8 +22,8 @@ const application_details = { application_name_placeholder: 'My App', description: 'Description', description_placeholder: 'Enter your application description', - config_endpoint: 'OpenID Provider configuration endpoint', - authorization_endpoint: 'Authorization Endpoint', + config_endpoint: 'OpenID provider configuration endpoint', + authorization_endpoint: 'Authorization endpoint', authorization_endpoint_tip: "The endpoint to perform authentication and authorization. It's used for OpenID Connect Authentication.", show_endpoint_details: 'Show endpoint details', @@ -39,8 +39,8 @@ const application_details = { redirect_uri_placeholder_native: 'io.logto://callback', redirect_uri_tip: 'The URI redirects after a user sign-in (whether successful or not). See OpenID Connect AuthRequest for more info.', - post_sign_out_redirect_uri: 'Post Sign-out Redirect URI', - post_sign_out_redirect_uris: 'Post Sign-out Redirect URIs', + post_sign_out_redirect_uri: 'Post sign-out redirect URI', + 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_tip: '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_tip: 'By default, all the origins of Redirect URIs will be allowed. Usually no action is required for this field. See the MDN doc for detailed info.', - token_endpoint: 'Token Endpoint', - user_info_endpoint: 'Userinfo Endpoint', + token_endpoint: 'Token endpoint', + user_info_endpoint: 'Userinfo endpoint', enable_admin_access: 'Enable admin access', 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.', - always_issue_refresh_token: 'Always issue Refresh Token', + always_issue_refresh_token: 'Always issue refresh token', 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.', - refresh_token_ttl: 'Refresh Token Time to Live (TTL) in days', + '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_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.', - rotate_refresh_token: 'Rotate Refresh Token', + '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_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. Learn more', + '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. Learn more', + 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: 'This action cannot be undone. It will permanently delete the application. Please enter the application name {{name}} to confirm.', enter_your_application_name: 'Enter your application name', diff --git a/packages/schemas/src/foundations/jsonb-types/oidc-module.ts b/packages/schemas/src/foundations/jsonb-types/oidc-module.ts index 062a49bea..6fba58531 100644 --- a/packages/schemas/src/foundations/jsonb-types/oidc-module.ts +++ b/packages/schemas/src/foundations/jsonb-types/oidc-module.ts @@ -1,6 +1,8 @@ import { validateRedirectUrl } from '@logto/core-kit'; import { z } from 'zod'; +import { type ToZodObject } from '../../utils/zod.js'; + export const oidcModelInstancePayloadGuard = z .object({ userCode: z.string().optional(), @@ -15,6 +17,34 @@ export const oidcModelInstancePayloadGuard = z export type OidcModelInstancePayload = z.infer; +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({ redirectUris: z .string() @@ -22,10 +52,10 @@ export const oidcClientMetadataGuard = z.object({ .or(z.string().refine((url) => validateRedirectUrl(url, 'mobile'))) .array(), postLogoutRedirectUris: z.string().url().array(), + backchannelLogoutUri: z.string().url().optional(), + backchannelLogoutSessionRequired: z.boolean().optional(), logoUri: z.string().optional(), -}); - -export type OidcClientMetadata = z.infer; +}) satisfies ToZodObject; export enum CustomClientMetadataKey { CorsAllowedOrigins = 'corsAllowedOrigins', diff --git a/packages/schemas/src/utils/zod.ts b/packages/schemas/src/utils/zod.ts index 7b17b8304..0379ad7e5 100644 --- a/packages/schemas/src/utils/zod.ts +++ b/packages/schemas/src/utils/zod.ts @@ -1,5 +1,5 @@ import { type z } from 'zod'; export type ToZodObject = z.ZodObject<{ - [K in keyof T]: z.ZodType; + [K in keyof T]-?: z.ZodType; }>;