Skip to content
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
Comment thread
ZiyamSanthosh marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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<void> => {
await copy(primarySecret);
Expand Down Expand Up @@ -103,10 +151,10 @@ export default function ShowClientSecret({
{/* Header */}
<Stack direction="column" spacing={1} sx={{textAlign: 'center'}}>
<Typography variant="h3" component="h1">
{t('applications:clientSecret.saveTitle')}
{saveTitle}
</Typography>
<Typography variant="body1" color="text.secondary">
{t('applications:clientSecret.saveSubtitle')}
{saveSubtitle}
</Typography>
</Stack>

Expand All @@ -124,10 +172,13 @@ export default function ShowClientSecret({
{clientSecret && (
<Box>
<Typography variant="caption" color="text.secondary" sx={{display: 'block', mb: 0.5}}>
{t('applications:clientSecret.clientSecretLabel')}
{t('applications:clientSecret.clientSecretLabel', 'Client Secret')}
</Typography>
<Typography variant="caption" color="text.secondary" sx={{display: 'block', mb: 1}}>
{t('applications:clientSecret.purpose')}
{t(
'applications:clientSecret.purpose',
'Used to authenticate your application at the OAuth 2 token endpoint.',
)}
</Typography>
<TextField
fullWidth
Expand All @@ -139,15 +190,18 @@ export default function ShowClientSecret({
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label={t('applications:regenerateSecret.success.toggleVisibility')}
aria-label={t(
'applications:regenerateSecret.success.toggleVisibility',
'Toggle secret visibility',
)}
onClick={handleToggleVisibility}
edge="end"
size="small"
>
{showSecret ? <EyeOff size={16} /> : <Eye size={16} />}
</IconButton>
<IconButton
aria-label={`${t('common:actions.copy')} ${t('applications:clientSecret.clientSecretLabel')}`}
aria-label={`${t('common:actions.copy', 'Copy')} ${t('applications:clientSecret.clientSecretLabel', 'Client Secret')}`}
onClick={() => {
copy(clientSecret).catch(() => {
// Error already handled in copy
Expand All @@ -172,10 +226,13 @@ export default function ShowClientSecret({

<Box>
<Typography variant="caption" color="text.secondary" sx={{display: 'block', mb: 0.5}}>
{t('applications:flowSecret.label')}
{t('applications:flowSecret.label', 'Flow Secret')}
</Typography>
<Typography variant="caption" color="text.secondary" sx={{display: 'block', mb: 1}}>
{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.',
)}
</Typography>
<TextField
fullWidth
Expand All @@ -187,15 +244,18 @@ export default function ShowClientSecret({
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label={t('applications:regenerateSecret.success.toggleVisibility')}
aria-label={t(
'applications:regenerateSecret.success.toggleVisibility',
'Toggle secret visibility',
)}
onClick={handleToggleFlowSecretVisibility}
edge="end"
size="small"
>
{showFlowSecret ? <EyeOff size={16} /> : <Eye size={16} />}
</IconButton>
<IconButton
aria-label={`${t('common:actions.copy')} ${t('applications:flowSecret.label')}`}
aria-label={`${t('common:actions.copy', 'Copy')} ${t('applications:flowSecret.label', 'Flow Secret')}`}
onClick={() => {
handleFlowSecretCopy().catch(() => {
// Error already handled in handleFlowSecretCopy
Expand All @@ -220,14 +280,15 @@ export default function ShowClientSecret({
{/* Security Reminder Alert */}
<Alert severity="warning" icon={<AlertTriangle size={20} />}>
<Typography variant="body2" sx={{fontWeight: 'medium', mb: 1}}>
{t('applications:clientSecret.securityReminder.title')}
{t('applications:clientSecret.securityReminder.title', 'Security Reminder')}
</Typography>
<Typography variant="body2">{t('applications:clientSecret.securityReminder.description')}</Typography>
<Typography variant="body2">{securityReminderDescription}</Typography>
</Alert>

{/* Action Buttons */}
<Stack direction="row" spacing={2} sx={{width: '100%'}}>
<Button
data-testid="application-copy-secret-button"
variant="contained"
fullWidth
startIcon={<Copy size={16} />}
Expand All @@ -238,10 +299,10 @@ export default function ShowClientSecret({
}}
disabled={copied}
>
{copied ? t('applications:clientSecret.copied') : t('applications:clientSecret.copySecret')}
{copied ? t('applications:clientSecret.copied', 'Copied to clipboard') : copySecretLabel}
</Button>
<Button data-testid="application-client-secret-continue" variant="outlined" fullWidth onClick={onContinue}>
{t('common:actions.continue')}
{t('common:actions.continue', 'Continue')}
</Button>
</Stack>
</Stack>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});

describe('visibility toggle', () => {
it('should toggle client secret visibility when eye icon is clicked', async () => {
const user = userEvent.setup();
Expand Down Expand Up @@ -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);

Expand All @@ -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(() => {
Expand Down
11 changes: 10 additions & 1 deletion frontend/packages/i18n/src/locales/en-US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Loading