From 1de263311d4869a043d801bb9a9f859324ade5cd Mon Sep 17 00:00:00 2001 From: ZiyamSanthosh Date: Tue, 11 Aug 2026 00:44:18 +0530 Subject: [PATCH] Name the issued credential on the one-time secret screen The one-time secret screen was hardcoded to client secret copy, so applications issued only a Flow Secret were told to save a client secret they do not have. Pick the title, subtitle, security reminder and copy button label based on which credentials are shown. Fixes #4742 --- .../create-application/ShowClientSecret.tsx | 89 ++++++++++++++++--- .../__tests__/ShowClientSecret.test.tsx | 38 +++++++- frontend/packages/i18n/src/locales/en-US.ts | 11 ++- 3 files changed, 120 insertions(+), 18 deletions(-) diff --git a/frontend/apps/console/src/features/applications/components/create-application/ShowClientSecret.tsx b/frontend/apps/console/src/features/applications/components/create-application/ShowClientSecret.tsx index 2bf993d747..493bac1745 100644 --- a/frontend/apps/console/src/features/applications/components/create-application/ShowClientSecret.tsx +++ b/frontend/apps/console/src/features/applications/components/create-application/ShowClientSecret.tsx @@ -19,6 +19,11 @@ import type {JSX} from 'react'; import {useState} from 'react'; import {useTranslation} from 'react-i18next'; +/** + * A translation key paired with its English fallback. + */ +type SecretCopy = [key: string, fallback: string]; + export interface ShowClientSecretProps { /** * The OAuth client secret that needs to be saved. Used to authenticate at the token endpoint. @@ -66,6 +71,49 @@ export default function ShowClientSecret({ // The primary secret backs the footer copy button: prefer the client secret, fall back to the // Flow Secret for embedded apps that only have the latter. const primarySecret = clientSecret || flowSecret; + const flowSecretOnly = Boolean(flowSecret) && !clientSecret; + const bothSecrets = Boolean(clientSecret) && Boolean(flowSecret); + + const resolveCopy = (clientCopy: SecretCopy, flowCopy: SecretCopy, bothCopy: SecretCopy): string => { + if (bothSecrets) { + return t(bothCopy[0], bothCopy[1]); + } + const [key, fallback] = flowSecretOnly ? flowCopy : clientCopy; + return t(key, fallback); + }; + + const saveTitle = resolveCopy( + ['applications:clientSecret.saveTitle', 'Save Your Client Secret'], + ['applications:flowSecret.saveTitle', 'Save Your Flow Secret'], + ['applications:secrets.saveTitle', 'Save Your Secrets'], + ); + const saveSubtitle = resolveCopy( + [ + 'applications:clientSecret.saveSubtitle', + "This is the only time you'll see this secret. Store it somewhere safe.", + ], + ['applications:flowSecret.saveSubtitle', "This is the only time you'll see this secret. Store it somewhere safe."], + ['applications:secrets.saveSubtitle', "This is the only time you'll see these secrets. Store them somewhere safe."], + ); + const securityReminderDescription = resolveCopy( + [ + 'applications:clientSecret.securityReminder.description', + 'Your client secret is a confidential key used to authenticate your application. It should be treated with the same level of security as a password. Never expose it in browser console, version control, or logs.', + ], + [ + 'applications:flowSecret.securityReminder.description', + 'Your Flow Secret is a confidential key used to authenticate your application when it starts a sign-in flow. It should be treated with the same level of security as a password. Never expose it in browser console, version control, or logs.', + ], + [ + 'applications:secrets.securityReminder.description', + 'These secrets are confidential keys used to authenticate your application. They should be treated with the same level of security as passwords. Never expose them in browser console, version control, or logs.', + ], + ); + // When both secrets are shown the footer button copies the client secret, so it is labelled + // explicitly; the Flow Secret has its own copy button on its field. + const copySecretLabel = flowSecretOnly + ? t('applications:flowSecret.copySecret', 'Copy Flow Secret') + : t('applications:clientSecret.copySecret', 'Copy Client Secret'); const handleCopy = async (): Promise => { await copy(primarySecret); @@ -103,10 +151,10 @@ export default function ShowClientSecret({ {/* Header */} - {t('applications:clientSecret.saveTitle')} + {saveTitle} - {t('applications:clientSecret.saveSubtitle')} + {saveSubtitle} @@ -124,10 +172,13 @@ export default function ShowClientSecret({ {clientSecret && ( - {t('applications:clientSecret.clientSecretLabel')} + {t('applications:clientSecret.clientSecretLabel', 'Client Secret')} - {t('applications:clientSecret.purpose')} + {t( + 'applications:clientSecret.purpose', + 'Used to authenticate your application at the OAuth 2 token endpoint.', + )} : } { copy(clientSecret).catch(() => { // Error already handled in copy @@ -172,10 +226,13 @@ export default function ShowClientSecret({ - {t('applications:flowSecret.label')} + {t('applications:flowSecret.label', 'Flow Secret')} - {t('applications:flowSecret.purpose')} + {t( + 'applications:flowSecret.purpose', + 'Used to authenticate your server when it starts a sign-in flow directly via the Flow Execution API.', + )} : } { handleFlowSecretCopy().catch(() => { // Error already handled in handleFlowSecretCopy @@ -220,14 +280,15 @@ export default function ShowClientSecret({ {/* Security Reminder Alert */} }> - {t('applications:clientSecret.securityReminder.title')} + {t('applications:clientSecret.securityReminder.title', 'Security Reminder')} - {t('applications:clientSecret.securityReminder.description')} + {securityReminderDescription} {/* Action Buttons */} diff --git a/frontend/apps/console/src/features/applications/components/create-application/__tests__/ShowClientSecret.test.tsx b/frontend/apps/console/src/features/applications/components/create-application/__tests__/ShowClientSecret.test.tsx index 28e47cb2e2..64dd4cdc43 100644 --- a/frontend/apps/console/src/features/applications/components/create-application/__tests__/ShowClientSecret.test.tsx +++ b/frontend/apps/console/src/features/applications/components/create-application/__tests__/ShowClientSecret.test.tsx @@ -70,11 +70,43 @@ describe('ShowClientSecret', () => { it('should render action buttons', () => { renderComponent(); - expect(screen.getByRole('button', {name: /copy secret/i})).toBeInTheDocument(); + expect(screen.getByTestId('application-copy-secret-button')).toHaveTextContent(/copy client secret/i); expect(screen.getByRole('button', {name: /continue/i})).toBeInTheDocument(); }); }); + describe('flow secret copy', () => { + it('should name the Flow Secret when only a Flow Secret is issued', () => { + renderComponent({clientSecret: '', flowSecret: 'flow_secret_12345'}); + + expect(screen.getByRole('heading', {level: 1, name: /save your flow secret/i})).toBeInTheDocument(); + expect(screen.getByText(/only time you'll see this secret\. Store it somewhere safe\./i)).toBeInTheDocument(); + expect(screen.getByText(/your flow secret is a confidential key/i)).toBeInTheDocument(); + expect(screen.getByTestId('application-copy-secret-button')).toHaveTextContent(/copy flow secret/i); + expect(screen.queryByText(/your client secret is a confidential key/i)).not.toBeInTheDocument(); + }); + + it('should copy the Flow Secret from the main copy button when it is the only secret', async () => { + const user = userEvent.setup(); + renderComponent({clientSecret: '', flowSecret: 'flow_secret_12345'}); + + await user.click(screen.getByTestId('application-copy-secret-button')); + + await waitFor(() => { + expect(mockCopy).toHaveBeenCalledWith('flow_secret_12345'); + }); + }); + + it('should use neutral copy when both secrets are issued', () => { + renderComponent({flowSecret: 'flow_secret_12345'}); + + expect(screen.getByRole('heading', {level: 1, name: /save your secrets/i})).toBeInTheDocument(); + expect(screen.getByText(/only time you'll see these secrets\. Store them somewhere safe\./i)).toBeInTheDocument(); + expect(screen.getByText(/these secrets are confidential keys/i)).toBeInTheDocument(); + expect(screen.getByTestId('application-copy-secret-button')).toHaveTextContent(/copy client secret/i); + }); + }); + describe('visibility toggle', () => { it('should toggle client secret visibility when eye icon is clicked', async () => { const user = userEvent.setup(); @@ -103,7 +135,7 @@ describe('ShowClientSecret', () => { const user = userEvent.setup(); renderComponent(); - const copyButton = screen.getByRole('button', {name: 'Copy Client Secret'}); + const copyButton = screen.getAllByRole('button', {name: 'Copy Client Secret'})[0]; await user.click(copyButton); @@ -116,7 +148,7 @@ describe('ShowClientSecret', () => { const user = userEvent.setup(); renderComponent(); - const mainCopyButton = screen.getByRole('button', {name: /copy secret/i}); + const mainCopyButton = screen.getByTestId('application-copy-secret-button'); await user.click(mainCopyButton); await waitFor(() => { diff --git a/frontend/packages/i18n/src/locales/en-US.ts b/frontend/packages/i18n/src/locales/en-US.ts index 2b70227607..d551907fdd 100644 --- a/frontend/packages/i18n/src/locales/en-US.ts +++ b/frontend/packages/i18n/src/locales/en-US.ts @@ -2613,13 +2613,22 @@ const translations = { 'clientSecret.clientSecretLabel': 'Client Secret', 'clientSecret.purpose': 'Used to authenticate your application at the OAuth 2 token endpoint.', 'clientSecret.copied': 'Copied to clipboard', - 'clientSecret.copySecret': 'Copy Secret', + 'clientSecret.copySecret': 'Copy Client Secret', 'clientSecret.securityReminder.title': 'Security Reminder', 'clientSecret.securityReminder.description': 'Your client secret is a confidential key used to authenticate your application. It should be treated with the same level of security as a password. Never expose it in browser console, version control, or logs.', 'flowSecret.label': 'Flow Secret', 'flowSecret.purpose': 'Used to authenticate your server when it starts a sign-in flow directly via the Flow Execution API.', + 'flowSecret.saveTitle': 'Save Your Flow Secret', + 'flowSecret.saveSubtitle': "This is the only time you'll see this secret. Store it somewhere safe.", + 'flowSecret.copySecret': 'Copy Flow Secret', + 'flowSecret.securityReminder.description': + 'Your Flow Secret is a confidential key used to authenticate your application when it starts a sign-in flow. It should be treated with the same level of security as a password. Never expose it in browser console, version control, or logs.', + 'secrets.saveTitle': 'Save Your Secrets', + 'secrets.saveSubtitle': "This is the only time you'll see these secrets. Store them somewhere safe.", + 'secrets.securityReminder.description': + 'These secrets are confidential keys used to authenticate your application. They should be treated with the same level of security as passwords. Never expose them in browser console, version control, or logs.', 'view.title': 'Application Details', 'view.subtitle': 'View application details and configuration', 'view.sections.basicInformation': 'Basic Information',