Add multiple client secrets support for OAuth/OIDC applications - #10587
Add multiple client secrets support for OAuth/OIDC applications#10587AfraHussaindeen wants to merge 4 commits into
Conversation
📝 WalkthroughWalkthroughThe console adds configurable multiple-client-secret management for OIDC applications. It adds creation, deletion, expiry status, generation and confirmation modals, previous-secret display, expiry alerts, revoke-all controls, permissions, feature-flag wiring, and localized strings. ChangesOIDC Client Secret Management
Important Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional. ❌ Failed checks (1 error, 1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 Biome (2.5.6)features/admin.applications.v1/api/application.tsFile contains syntax errors that prevent linting: Line 328: Catch clause variable type annotation must be 'any' or 'unknown' if specified. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #10587 +/- ##
==========================================
+ Coverage 73.46% 73.50% +0.04%
==========================================
Files 471 471
Lines 71454 71562 +108
Branches 266 266
==========================================
+ Hits 52492 52600 +108
Misses 18855 18855
Partials 107 107
🚀 New features to boost your workflow:
|
0151ad8 to
02825c6
Compare
02825c6 to
1e9a511
Compare
1e9a511 to
8c24941
Compare
8c24941 to
6821902
Compare
| case ClientSecretExpiryState.EXPIRED: | ||
| return "grey"; | ||
| case ClientSecretExpiryState.EXPIRING_CRITICAL: | ||
| return "red"; | ||
| case ClientSecretExpiryState.EXPIRING_WARNING: | ||
| return "yellow"; |
There was a problem hiding this comment.
as discussed in the code review, were you able to consider using red for expired serets and yellow for secrets that are to be expired soon?
| const EXPIRY_WARNING_DAYS: number = 20; | ||
| const EXPIRY_CRITICAL_DAYS: number = 10; |
There was a problem hiding this comment.
shall we settle on a single expiry waring time period instead, for the sake of simplified UX?
| /** Green — not expiring anytime soon. */ | ||
| ACTIVE = "active", | ||
| /** Green — never expires. */ | ||
| NEVER = "never", | ||
| /** Yellow — expiring within the warning window. */ | ||
| EXPIRING_WARNING = "expiringWarning", | ||
| /** Red — expiring within the critical window. */ | ||
| EXPIRING_CRITICAL = "expiringCritical", | ||
| /** Grey — already expired. */ |
There was a problem hiding this comment.
Do we really need to mention the colors in the doc comments here?
There was a problem hiding this comment.
Actionable comments posted: 9
🧹 Nitpick comments (1)
features/admin.applications.v1/components/client-secrets/generate-client-secret-modal.tsx (1)
91-91: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueReplace the
anyref type with the form ref type.
formRefis typed asReact.MutableRefObject<any>. The coding guidelines prohibitany. Use the exported ref type ofFormfrom@wso2is/forms, orunknownwith a narrow local interface that declarestriggerSubmit.As per coding guidelines: "Never use
anytype; use proper types orunknownwith type guards instead".♻️ Proposed minimal typing
+interface FormRefInterface { + triggerSubmit: () => void; +} + - const formRef: React.MutableRefObject<any> = useRef(null); + const formRef: React.MutableRefObject<FormRefInterface> = useRef<FormRefInterface>(null);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@features/admin.applications.v1/components/client-secrets/generate-client-secret-modal.tsx` at line 91, Replace the any-based type on formRef with the exported Form ref type from `@wso2is/forms`; if unavailable, use unknown with a narrow interface/type guard exposing triggerSubmit, while preserving the existing ref usage.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@apps/console/java/org.wso2.identity.apps.console.server.feature/resources/deployment.config.json.j2`:
- Around line 847-850: Update the isMultipleClientSecretsEnabled default in the
deployment configuration template to emit true when
oauth.multiple_client_secrets.enable is undefined, while preserving the
configured value when it is provided.
In `@features/admin.applications.v1/components/banners/secret-expiry-banner.tsx`:
- Around line 71-74: Update the selector callbacks in the
isMultipleClientSecretsEnabled and applicationFeatureConfig useSelector calls to
include explicit return type annotations, while retaining the AppState parameter
type. Use the nullable FeatureAccessConfigInterface type consistently for
applicationFeatureConfig if the configuration may be absent.
In
`@features/admin.applications.v1/components/client-secrets/client-secret-status.tsx`:
- Around line 22-25: Replace the Semantic UI controls in ClientSecretStatus with
the themed Oxygen UI status element, using per-component imports and preserving
expiry-status semantics and existing data-componentid behavior. In
features/admin.applications.v1/components/client-secrets/client-secret-value-field.tsx
at lines 23-24, replace Input with the Oxygen UI TextField while preserving
masking, copy behavior, and data-componentid.
In
`@features/admin.applications.v1/components/client-secrets/client-secret-utils.ts`:
- Around line 75-84: Update the expiry-state classification in the client-secret
utility to check whether secret.expiresAt converted to milliseconds is less than
or equal to referenceTime before evaluating the EXPIRY_CRITICAL_DAYS and
EXPIRY_WARNING_DAYS thresholds; classify that elapsed timestamp as
ClientSecretExpiryState.EXPIRED, while preserving the existing threshold
behavior for future expirations.
In
`@features/admin.applications.v1/components/client-secrets/client-secret-value-field.tsx`:
- Around line 31-40: Update ClientSecretValueFieldPropsInterface and
ClientSecretValueField so value is optional and the masked read-only field
renders whenever hideSecretValue is true or value is absent. Preserve the
existing show/copy behavior only when a value is available and hideSecretValue
is false.
In
`@features/admin.applications.v1/components/client-secrets/delete-client-secret-modal.tsx`:
- Around line 19-21: Migrate delete-client-secret-modal.tsx by replacing
ConfirmationModal and Semantic UI subcomponents with Oxygen Dialog, Checkbox,
and Button while preserving the assertion checkbox, non-dismissible backdrop,
callbacks, and existing data-componentid values. In previous-client-secrets.tsx,
replace ContentLoader, Message, and Popup with Oxygen equivalents, remove
previous-client-secrets.scss and hardcoded dimensions, and use MUI styled or
limited theme-based sx values for styling.
In
`@features/admin.applications.v1/components/client-secrets/generate-client-secret-modal.tsx`:
- Around line 131-142: Update validateForm to reject customExpiryDays values
that are not integers while preserving the existing 1–3650 range validation. In
resolveExpiresAt, floor the computed custom expiration timestamp so expiresAt is
always an integer Unix epoch value.
In `@features/admin.applications.v1/components/settings/access-configuration.tsx`:
- Around line 1038-1064: Update resolveClientSecretsDangerZone to require the
selected protocol to be OIDC in its existing guard, alongside the inbound OIDC
configuration checks. Ensure the RevokeAllClientSecretsDangerZone is not
rendered when viewing SAML settings, while preserving the current exclusions and
behavior for the OIDC view.
In `@modules/i18n/src/translations/en-US/portals/applications.ts`:
- Around line 199-203: Update the delete confirmation content near the
client-secret confirmation strings so it states that clients using the deleted
secret can no longer authenticate, while explicitly clarifying that other client
secrets remain valid; preserve the existing notice about active tokens and
authorization codes unless it conflicts with this scope.
---
Nitpick comments:
In
`@features/admin.applications.v1/components/client-secrets/generate-client-secret-modal.tsx`:
- Line 91: Replace the any-based type on formRef with the exported Form ref type
from `@wso2is/forms`; if unavailable, use unknown with a narrow interface/type
guard exposing triggerSubmit, while preserving the existing ref usage.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro Plus
Run ID: 5012c6f1-4a34-469e-aeed-758a5520142d
📒 Files selected for processing (26)
apps/console/java/org.wso2.identity.apps.console.server.feature/resources/deployment.config.json.j2apps/console/src/public/deployment.config.jsonfeatures/admin.applications.v1/api/application.tsfeatures/admin.applications.v1/api/use-get-oauth-client-secrets.tsfeatures/admin.applications.v1/components/banners/secret-expiry-banner.tsxfeatures/admin.applications.v1/components/client-secrets/client-secret-row.scssfeatures/admin.applications.v1/components/client-secrets/client-secret-row.tsxfeatures/admin.applications.v1/components/client-secrets/client-secret-status.scssfeatures/admin.applications.v1/components/client-secrets/client-secret-status.tsxfeatures/admin.applications.v1/components/client-secrets/client-secret-utils.tsfeatures/admin.applications.v1/components/client-secrets/client-secret-value-field.scssfeatures/admin.applications.v1/components/client-secrets/client-secret-value-field.tsxfeatures/admin.applications.v1/components/client-secrets/client-secrets-section.scssfeatures/admin.applications.v1/components/client-secrets/client-secrets-section.tsxfeatures/admin.applications.v1/components/client-secrets/delete-client-secret-modal.tsxfeatures/admin.applications.v1/components/client-secrets/generate-client-secret-modal.tsxfeatures/admin.applications.v1/components/client-secrets/generated-client-secret-modal.tsxfeatures/admin.applications.v1/components/client-secrets/previous-client-secrets.scssfeatures/admin.applications.v1/components/client-secrets/previous-client-secrets.tsxfeatures/admin.applications.v1/components/client-secrets/revoke-all-client-secrets-danger-zone.tsxfeatures/admin.applications.v1/components/forms/inbound-oidc-form.tsxfeatures/admin.applications.v1/components/settings/access-configuration.tsxfeatures/admin.applications.v1/models/application-inbound.tsfeatures/admin.applications.v1/pages/application-edit.tsxmodules/i18n/src/models/namespaces/applications-ns.tsmodules/i18n/src/translations/en-US/portals/applications.ts
| "isMultipleClientSecretsEnabled": {% if oauth.multiple_client_secrets.enable is defined %}{{ oauth.multiple_client_secrets.enable }}{% else %}false{% endif %}, | ||
| "maxGracefulRefreshTokenReuseLimit": {% if oauth.graceful_refresh_token_rotation.maximum_reuse_limit is defined %}{{ oauth.graceful_refresh_token_rotation.maximum_reuse_limit }}{% else %}5{% endif %}, | ||
| "maxGracefulRefreshTokenRotationValidityPeriod": {% if oauth.graceful_refresh_token_rotation.maximum_validity_period is defined %}{{ oauth.graceful_refresh_token_rotation.maximum_validity_period }}{% else %}60{% endif %} | ||
| "maxGracefulRefreshTokenRotationValidityPeriod": {% if oauth.graceful_refresh_token_rotation.maximum_validity_period is defined %}{{ oauth.graceful_refresh_token_rotation.maximum_validity_period }}{% else %}60{% endif %}, | ||
| "multipleClientSecretsMaxCount": {% if oauth.multiple_client_secrets.max_secret_count is defined %}{{ oauth.multiple_client_secrets.max_secret_count }}{% else %}2{% endif %} |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n -C 3 'multiple_client_secrets|isMultipleClientSecretsEnabled|multipleClientSecretsMaxCount' \
apps/consoleRepository: wso2/identity-apps
Length of output: 3174
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- all repository references ---'
rg -n -C 4 'multiple_client_secrets|isMultipleClientSecretsEnabled|multipleClientSecretsMaxCount' .
printf '%s\n' '--- candidate deployment configuration files ---'
git ls-files | rg '(^|/)(deployment\.config|deployment.*config|config.*deployment)|multiple.*secret' | head -200
printf '%s\n' '--- template and configuration context ---'
sed -n '810,865p' apps/console/java/org.wso2.identity.apps.console.server.feature/resources/deployment.config.json.j2
sed -n '560,610p' apps/console/src/public/deployment.config.jsonRepository: wso2/identity-apps
Length of output: 21094
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- OAuth configuration declarations and defaults ---'
rg -n -i -C 3 'multiple[._-]?client[._-]?secret|maximum[._-]?reuse|maximum[._-]?validity' \
--glob '!apps/console/src/public/deployment.config.json' \
--glob '!apps/console/java/org.wso2.identity.apps.console.server.feature/resources/deployment.config.json.j2' .
printf '%s\n' '--- deployment.config consumers and fallback behavior ---'
rg -n -C 4 'deployment\.config\.json|DeploymentConfig|config\.ui\.features|features\?\.applications' \
features modules apps/console/src --glob '*.{ts,tsx,js,json}'
printf '%s\n' '--- tracked change summary ---'
git status --short
git diff --statRepository: wso2/identity-apps
Length of output: 50375
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- exact feature references outside generated/static config ---'
rg -n -C 2 'multiple_client_secrets|isMultipleClientSecretsEnabled|multipleClientSecretsMaxCount' \
--glob '*.j2' --glob '*.yaml' --glob '*.yml' --glob '*.toml' --glob '*.json' \
--glob '*.properties' --glob '*.md' --glob '*.ts' --glob '*.tsx' --glob '*.js' \
. | head -300
printf '%s\n' '--- deployment config loading ---'
rg -n -C 5 'getConfig\(\)|deployment\.config\.json|AppUtils' \
features/admin.core.v1 apps/console --glob '*.{ts,tsx,js,json}' | head -300
printf '%s\n' '--- repository state ---'
git status --short
git diff --name-onlyRepository: wso2/identity-apps
Length of output: 31650
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
import json
import re
from pathlib import Path
template = Path("apps/console/java/org.wso2.identity.apps.console.server.feature/resources/deployment.config.json.j2").read_text()
public = json.loads(Path("apps/console/src/public/deployment.config.json").read_text())
pattern = re.compile(
r'"isMultipleClientSecretsEnabled":\s*'
r'\{%\s*if\s+oauth\.multiple_client_secrets\.enable\s+is\s+defined\s*%\}'
r'(?P<defined>.*?)'
r'\{%\s*else\s*%\}(?P<fallback>.*?)\{%\s*endif\s*%\}'
)
match = pattern.search(template)
if not match:
raise SystemExit("template expression not found")
public_value = (
public["ui"]["features"]["applications"]["properties"]
["isMultipleClientSecretsEnabled"]
)
print("server_template_defined_value:", match.group("defined").strip())
print("server_template_absent_value:", match.group("fallback").strip())
print("checked_in_public_value:", public_value)
assert match.group("fallback").strip() == "false"
assert public_value is True
print("mismatch_for_absent_server_setting: true")
PYRepository: wso2/identity-apps
Length of output: 337
Set the server-side default to true. The template currently emits false when oauth.multiple_client_secrets.enable is absent, while the Console default is true. This disables multiple client secrets in deployments that omit the server setting.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@apps/console/java/org.wso2.identity.apps.console.server.feature/resources/deployment.config.json.j2`
around lines 847 - 850, Update the isMultipleClientSecretsEnabled default in the
deployment configuration template to emit true when
oauth.multiple_client_secrets.enable is undefined, while preserving the
configured value when it is provided.
| const isMultipleClientSecretsEnabled: boolean = useSelector((state: AppState) => | ||
| Boolean(state?.config?.ui?.features?.applications?.properties?.isMultipleClientSecretsEnabled)); | ||
| const applicationFeatureConfig: FeatureAccessConfigInterface = useSelector((state: AppState) => | ||
| state?.config?.ui?.features?.applications); |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Add return types to the selector callbacks.
Line 71 and Line 73 annotate the result variables, but the selector callbacks still rely on inferred return types. Add explicit return types to both callbacks. If the application feature configuration can be absent, use the nullable type consistently.
As per coding guidelines: “Always include explicit type annotations on useSelector hook with the selector function parameter type and return type.”
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@features/admin.applications.v1/components/banners/secret-expiry-banner.tsx`
around lines 71 - 74, Update the selector callbacks in the
isMultipleClientSecretsEnabled and applicationFeatureConfig useSelector calls to
include explicit return type annotations, while retaining the AppState parameter
type. Use the nullable FeatureAccessConfigInterface type consistently for
applicationFeatureConfig if the configuration may be absent.
Source: Coding guidelines
| import { Label, SemanticCOLORS } from "semantic-ui-react"; | ||
| import { ClientSecretExpiryState, resolveClientSecretExpiry } from "./client-secret-utils"; | ||
| import "./client-secret-status.scss"; | ||
| import { ClientSecretInterface } from "../../models/application-inbound"; |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- target files ---'
git ls-files 'features/admin.applications.v1/components/client-secrets/client-secret-status.tsx' \
'features/admin.applications.v1/components/client-secrets/client-secret-value-field.tsx'
printf '%s\n' '--- status outline ---'
ast-grep outline features/admin.applications.v1/components/client-secrets/client-secret-status.tsx
printf '%s\n' '--- value field outline ---'
ast-grep outline features/admin.applications.v1/components/client-secrets/client-secret-value-field.tsx
printf '%s\n' '--- status source ---'
cat -n features/admin.applications.v1/components/client-secrets/client-secret-status.tsx
printf '%s\n' '--- value field source ---'
cat -n features/admin.applications.v1/components/client-secrets/client-secret-value-field.tsx
printf '%s\n' '--- nearby Oxygen UI usage ---'
rg -n --glob '*.tsx' --glob '*.ts' \
"from ['\"]`@oxygen-ui/react`|`@oxygen-ui/react/`" \
features/admin.applications.v1/components | head -200
printf '%s\n' '--- package declarations ---'
rg -n --glob 'package.json' --glob 'pnpm-lock.yaml' --glob 'yarn.lock' --glob 'package-lock.json' \
'"`@oxygen-ui/react`"|oxygen-ui' .Repository: wso2/identity-apps
Length of output: 35961
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- working tree status ---'
git status --short
printf '%s\n' '--- target diff summary ---'
git diff --stat -- \
features/admin.applications.v1/components/client-secrets/client-secret-status.tsx \
features/admin.applications.v1/components/client-secrets/client-secret-value-field.tsx
printf '%s\n' '--- target diff ---'
git diff -- \
features/admin.applications.v1/components/client-secrets/client-secret-status.tsx \
features/admin.applications.v1/components/client-secrets/client-secret-value-field.tsx
printf '%s\n' '--- client-secret styles ---'
for file in \
features/admin.applications.v1/components/client-secrets/client-secret-status.scss \
features/admin.applications.v1/components/client-secrets/client-secret-value-field.scss; do
if [ -f "$file" ]; then
printf '%s\n' "--- $file ---"
cat -n "$file"
fi
done
printf '%s\n' '--- status and input alternatives in this feature ---'
rg -n --glob '*.tsx' --glob '*.ts' \
'SemanticCOLORS|<Label\b|from ["'\'']semantic-ui-react["'\'']|<Input\b|<TextField\b|<Chip\b|status.*indicator|Status' \
features/admin.applications.v1/components | head -250
printf '%s\n' '--- Oxygen input component references ---'
rg -n --glob '*.tsx' --glob '*.ts' \
'from ["'\'']`@oxygen-ui/react/`(Input|TextField|Chip|Badge)["'\'']|<Input\b|<TextField\b|<Chip\b|<Badge\b' \
features modules apps | head -300Repository: wso2/identity-apps
Length of output: 50374
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- concise Oxygen control imports ---'
rg -n --glob '*.tsx' --glob '*.ts' \
'import (Input|TextField|Chip|Box|Badge|.*) from "`@oxygen-ui/react/`(Input|TextField|Chip|Box|Badge)"' \
features/admin.applications.v1 features/admin.onboarding.v1 features/admin.push-providers.v1 | head -120
printf '%s\n' '--- reusable copyable field implementation ---'
rg -l --glob '*.tsx' 'CopyInputField|copyable-field' features modules apps | head -30
printf '%s\n' '--- nearby styled Oxygen components ---'
rg -l --glob '*.tsx' 'styled\(.*`@mui/material/styles`|from "`@mui/material/styles`"' \
features/admin.applications.v1/components/client-secrets \
features/admin.applications.v1/components \
features/admin.onboarding.v1/components/shared | head -30
printf '%s\n' '--- exact target import and control inventory ---'
python3 - <<'PY'
from pathlib import Path
import re
paths = [
Path("features/admin.applications.v1/components/client-secrets/client-secret-status.tsx"),
Path("features/admin.applications.v1/components/client-secrets/client-secret-value-field.tsx"),
]
for path in paths:
text = path.read_text()
imports = re.findall(r'import .*? from "([^"]+)";', text)
controls = re.findall(r'<([A-Z][A-Za-z0-9_]*)\b', text)
print(path)
print(" semantic-ui-react import:", "semantic-ui-react" in imports)
print(" imported controls:", [item for item in imports if "semantic-ui-react" in item or "oxygen-ui" in item])
print(" JSX controls:", controls)
print(" data-componentid count:", text.count("data-componentid"))
PYRepository: wso2/identity-apps
Length of output: 9064
🌐 Web query:
Oxygen UI React 2.4.6 component documentation Input TextField Chip status indicator
💡 Result:
Oxygen UI is a design system and React component library developed by WSO2, which provides a set of components built on top of Material-UI [1][2][3]. As of version 2.4.6, Oxygen UI does not provide a specific, standalone "Status Indicator" component [3][4]. Instead, developers typically implement status indicators using the Material-UI Chip component, which is available within the Oxygen UI ecosystem [1][2][5]. Key takeaways for the components mentioned: 1. TextField: You can use the standard Material-UI TextField component directly from the Oxygen UI library [1][2][3]. It is used for form inputs and integrates seamlessly with the design system's theme [6]. 2. Chip: While not explicitly labeled as a "status indicator" component in the library documentation, the Chip component is the standard way to represent status [5]. For example, in a ListingTable, a Chip is commonly used to visualize status with dynamic colors (e.g., using 'success' for active items) [5]. 3. Documentation: Documentation for components can be found in the Oxygen UI Storybook [1][7][8]. Since Oxygen UI is powered by Material-UI, you can often refer to official Material-UI documentation for core component behaviors, while relying on the Oxygen UI package for brand-specific theming and custom extensions [1][2][9]. To get started, ensure you have the library installed [3]: npm install @oxygen-ui/react You can then import components as follows [3][4]: import { TextField, Chip } from '@oxygen-ui/react';
Citations:
- 1: https://github.com/wso2/oxygen-ui
- 2: https://registry.npmjs.org/@wso2/oxygen-ui
- 3: https://www.npmjs.com/package/@oxygen-ui/react
- 4: https://cdn.jsdelivr.net/npm/@oxygen-ui/react@2.4.6/README.md
- 5: https://cdn.jsdelivr.net/npm/@wso2/oxygen-ui@0.13.1/.ai/patterns.md
- 6: https://cdn.jsdelivr.net/npm/@wso2/oxygen-ui@0.13.1/.claude/skills/oxygen-form/SKILL.md
- 7: https://wso2.github.io/oxygen-ui/
- 8: https://cdn.jsdelivr.net/npm/@oxygen-ui/react@2.4.6/esm/types/components/Button/Button.d.ts
- 9: https://cdn.jsdelivr.net/npm/@wso2/oxygen-ui@0.13.0/.ai/components.md
Replace Semantic UI controls in the client-secret components.
Use Oxygen UI per-component imports. Replace Label/SemanticCOLORS with a themed Oxygen UI status element, and replace Input with an Oxygen UI TextField. Preserve data-componentid, masking, copy behavior, and expiry-status semantics.
📍 Affects 2 files
features/admin.applications.v1/components/client-secrets/client-secret-status.tsx#L22-L25(this comment)features/admin.applications.v1/components/client-secrets/client-secret-value-field.tsx#L23-L24
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@features/admin.applications.v1/components/client-secrets/client-secret-status.tsx`
around lines 22 - 25, Replace the Semantic UI controls in ClientSecretStatus
with the themed Oxygen UI status element, using per-component imports and
preserving expiry-status semantics and existing data-componentid behavior. In
features/admin.applications.v1/components/client-secrets/client-secret-value-field.tsx
at lines 23-24, replace Input with the Oxygen UI TextField while preserving
masking, copy behavior, and data-componentid.
Source: Coding guidelines
| const daysRemaining: number = Math.ceil((secret.expiresAt * 1000 - referenceTime) / MILLISECONDS_PER_DAY); | ||
| const formattedDate: string = formatDate(secret.expiresAt); | ||
|
|
||
| /* The secret is active here (EXPIRED is trusted from the API above); classify how soon it expires. */ | ||
| let state: ClientSecretExpiryState; | ||
|
|
||
| if (daysRemaining <= EXPIRY_CRITICAL_DAYS) { | ||
| state = ClientSecretExpiryState.EXPIRING_CRITICAL; | ||
| } else if (daysRemaining <= EXPIRY_WARNING_DAYS) { | ||
| state = ClientSecretExpiryState.EXPIRING_WARNING; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Classify elapsed expiry timestamps as expired.
If expiresAt is before referenceTime but status is still ACTIVE, daysRemaining is negative and this function returns EXPIRING_CRITICAL. The status line then shows “Expires Soon” with a past date.
Check secret.expiresAt * 1000 <= referenceTime before the warning thresholds and return EXPIRED.
Proposed fix
const daysRemaining: number = Math.ceil((secret.expiresAt * 1000 - referenceTime) / MILLISECONDS_PER_DAY);
const formattedDate: string = formatDate(secret.expiresAt);
+ if (secret.expiresAt * 1000 <= referenceTime) {
+ return {
+ formattedDate,
+ state: ClientSecretExpiryState.EXPIRED
+ };
+ }
+
/* The secret is active here (EXPIRED is trusted from the API above); classify how soon it expires. */📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const daysRemaining: number = Math.ceil((secret.expiresAt * 1000 - referenceTime) / MILLISECONDS_PER_DAY); | |
| const formattedDate: string = formatDate(secret.expiresAt); | |
| /* The secret is active here (EXPIRED is trusted from the API above); classify how soon it expires. */ | |
| let state: ClientSecretExpiryState; | |
| if (daysRemaining <= EXPIRY_CRITICAL_DAYS) { | |
| state = ClientSecretExpiryState.EXPIRING_CRITICAL; | |
| } else if (daysRemaining <= EXPIRY_WARNING_DAYS) { | |
| state = ClientSecretExpiryState.EXPIRING_WARNING; | |
| const daysRemaining: number = Math.ceil((secret.expiresAt * 1000 - referenceTime) / MILLISECONDS_PER_DAY); | |
| const formattedDate: string = formatDate(secret.expiresAt); | |
| if (secret.expiresAt * 1000 <= referenceTime) { | |
| return { | |
| formattedDate, | |
| state: ClientSecretExpiryState.EXPIRED | |
| }; | |
| } | |
| /* The secret is active here (EXPIRED is trusted from the API above); classify how soon it expires. */ | |
| let state: ClientSecretExpiryState; | |
| if (daysRemaining <= EXPIRY_CRITICAL_DAYS) { | |
| state = ClientSecretExpiryState.EXPIRING_CRITICAL; | |
| } else if (daysRemaining <= EXPIRY_WARNING_DAYS) { | |
| state = ClientSecretExpiryState.EXPIRING_WARNING; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@features/admin.applications.v1/components/client-secrets/client-secret-utils.ts`
around lines 75 - 84, Update the expiry-state classification in the
client-secret utility to check whether secret.expiresAt converted to
milliseconds is less than or equal to referenceTime before evaluating the
EXPIRY_CRITICAL_DAYS and EXPIRY_WARNING_DAYS thresholds; classify that elapsed
timestamp as ClientSecretExpiryState.EXPIRED, while preserving the existing
threshold behavior for future expirations.
| interface ClientSecretValueFieldPropsInterface extends IdentifiableComponentInterface { | ||
| /** | ||
| * The client secret value to display (masked with show/copy). | ||
| */ | ||
| value: string; | ||
| /** | ||
| * Whether the secret value is unavailable (e.g. when client secret hashing is enabled). | ||
| * When true, a masked read-only field is shown without the show/copy actions. | ||
| */ | ||
| hideSecretValue?: boolean; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
fd -HI -t f '^tsconfig.*\.json$' . -x sh -c '
echo "=== $1 ==="
jq ".compilerOptions | { strict, strictNullChecks }" "$1"
' sh {}
rg -n -C 3 \
'secretValue\??:|value:\s*string|value=\{\s*secret\?\.secretValue\s*\}' \
features/admin.applications.v1/models/application-inbound.ts \
features/admin.applications.v1/components/client-secrets/client-secret-value-field.tsx \
features/admin.applications.v1/components/client-secrets/client-secret-row.tsxRepository: wso2/identity-apps
Length of output: 11376
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '=== application-inbound.ts matches ==='
rg -n -C 5 'secretValue' features/admin.applications.v1/models/application-inbound.ts
printf '%s\n' '=== client-secret-value-field.tsx ==='
cat -n features/admin.applications.v1/components/client-secrets/client-secret-value-field.tsx
printf '%s\n' '=== client-secret-row.tsx ==='
cat -n features/admin.applications.v1/components/client-secrets/client-secret-row.tsx
printf '%s\n' '=== applications feature TypeScript configuration ==='
cat -n features/admin.applications.v1/tsconfig.json
printf '%s\n' '=== shared React TypeScript configuration ==='
cat -n tsconfig.base.react.jsonRepository: wso2/identity-apps
Length of output: 9437
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '=== ClientSecretValueField call sites ==='
rg -n -C 4 'ClientSecretValueField|hideSecretValue|secretValue' features/admin.applications.v1/components features/admin.applications.v1/hooks features/admin.applications.v1/utils 2>/dev/null || true
printf '%s\n' '=== CopyInputField declarations and implementation references ==='
rg -n -C 5 'CopyInputField' modules features | head -n 240Repository: wso2/identity-apps
Length of output: 47968
Handle missing secretValue in the masked branch.
ClientSecretInterface.secretValue is optional, but ClientSecretRow passes it to CopyInputField when hideSecretValue is false. Make value optional and render the masked read-only field when hideSecretValue is true or value is absent. The current TypeScript configuration does not catch this undefined value.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@features/admin.applications.v1/components/client-secrets/client-secret-value-field.tsx`
around lines 31 - 40, Update ClientSecretValueFieldPropsInterface and
ClientSecretValueField so value is optional and the masked read-only field
renders whenever hideSecretValue is true or value is absent. Preserve the
existing show/copy behavior only when a value is available and hideSecretValue
is false.
| import { IdentifiableComponentInterface } from "@wso2is/core/models"; | ||
| import { ConfirmationModal } from "@wso2is/react-components"; | ||
| import React, { FunctionComponent, ReactElement } from "react"; |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- candidate files ---'
for f in \
features/admin.applications.v1/components/client-secrets/delete-client-secret-modal.tsx \
features/admin.applications.v1/components/client-secrets/previous-client-secrets.tsx
do
printf '\n--- %s ---\n' "$f"
wc -l "$f"
sed -n '1,260p' "$f"
done
printf '%s\n' '--- related dialog and styling patterns ---'
rg -n --glob '*.{ts,tsx}' \
'ConfirmationModal|ConfirmationDialog|styled\(|<Dialog|disable[A-Za-z]*Click|assert' \
features/admin.applications.v1/components features 2>/dev/null | head -240Repository: wso2/identity-apps
Length of output: 41793
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- change status ---'
git status --short
git diff --stat -- \
features/admin.applications.v1/components/client-secrets/delete-client-secret-modal.tsx \
features/admin.applications.v1/components/client-secrets/previous-client-secrets.tsx
printf '%s\n' '--- Oxygen dialog patterns ---'
rg -n --glob '*.{ts,tsx}' \
'from "`@oxygen-ui/react/`(Dialog|DialogTitle|DialogContent|DialogActions|Checkbox|Button|Alert|Typography)"|<Dialog|DialogTitle|DialogContent|DialogActions' \
. | head -240
printf '%s\n' '--- candidate stylesheet ---'
sed -n '1,240p' features/admin.applications.v1/components/client-secrets/previous-client-secrets.scss
printf '%s\n' '--- component provenance and usages ---'
rg -n --glob '*.{ts,tsx}' \
'DeleteClientSecretModal|PreviousClientSecrets' \
features/admin.applications.v1
git log --oneline --follow -- \
features/admin.applications.v1/components/client-secrets/delete-client-secret-modal.tsx \
features/admin.applications.v1/components/client-secrets/previous-client-secrets.tsx | head -20Repository: wso2/identity-apps
Length of output: 36650
🏁 Script executed:
#!/bin/bash
set -eu
for f in \
features/admin.applications.v1/components/client-secrets/delete-client-secret-modal.tsx \
features/admin.applications.v1/components/client-secrets/previous-client-secrets.tsx
do
printf '\n--- history: %s ---\n' "$f"
git log --oneline -8 -- "$f"
printf '%s\n' '--- tracked file metadata ---'
git ls-files --stage -- "$f"
done
printf '%s\n' '--- Oxygen modal implementation pattern ---'
sed -n '1,190p' features/admin.tenants.v1/components/add-tenant/add-tenant-modal.tsx
printf '%s\n' '--- client-secret component history summary ---'
git log --oneline -12 -- features/admin.applications.v1/components/client-secretsRepository: wso2/identity-apps
Length of output: 7233
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- ConfirmationModal definition and dependencies ---'
rg -n --glob '*.{ts,tsx}' \
'const ConfirmationModal|function ConfirmationModal|ConfirmationModal\s*=|export.*ConfirmationModal|ConfirmationModalProps' \
. | head -120
rg -n --glob '*.{ts,tsx,scss}' \
'semantic-ui|semantic-ui-react|Modal\.|Dimmer|closeOnDimmerClick|assertionHint' \
packages features/admin.applications.v1/components/client-secrets 2>/dev/null | head -180
printf '%s\n' '--- commit contents introducing the components ---'
git show --stat --oneline 68219024e09 -- \
features/admin.applications.v1/components/client-secrets
git show --format= --no-ext-diff 68219024e09 -- \
features/admin.applications.v1/components/client-secrets/delete-client-secret-modal.tsx \
features/admin.applications.v1/components/client-secrets/previous-client-secrets.tsx \
features/admin.applications.v1/components/client-secrets/previous-client-secrets.scss | sed -n '1,360p'
printf '%s\n' '--- styling conventions in the client-secret directory ---'
find features/admin.applications.v1/components/client-secrets -maxdepth 1 -type f -print
rg -n --glob '*.{ts,tsx}' 'styled|sx=' features/admin.applications.v1/components/client-secretsRepository: wso2/identity-apps
Length of output: 34014
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- ConfirmationModal implementation ---'
sed -n '1,230p' modules/react-components/src/components/modal/confirmation-modal/confirmation-modal.tsx
sed -n '1,170p' modules/react-components/src/components/modal/confirmation-modal/confirmation-modal-header.tsx
sed -n '1,170p' modules/react-components/src/components/modal/confirmation-modal/confirmation-modal-message.tsxRepository: wso2/identity-apps
Length of output: 11903
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- legacy react-components used by PreviousClientSecrets ---'
rg -n --glob '*.{ts,tsx}' \
'export .*ContentLoader|const ContentLoader|export .*Message|const Message|export .*Popup|const Popup' \
modules/react-components/src | head -100
rg -n --glob '*.{ts,tsx}' \
'from "semantic-ui-react"' \
modules/react-components/src/components/{loader,message,popup} modules/react-components/src 2>/dev/null | head -100Repository: wso2/identity-apps
Length of output: 16714
Migrate the new client-secret components to Oxygen UI and theme-based styling.
- Replace
ConfirmationModaland its Semantic UI subcomponents with OxygenDialog,Checkbox, andButton. Preserve the assertion checkbox, non-dismissible backdrop, callbacks, anddata-componentidvalues. - Replace
ContentLoader,Message, andPopupinprevious-client-secrets.tsxwith Oxygen components. Replaceprevious-client-secrets.scssand hardcoded dimensions with MUIstyledor limitedsxusing theme values.
📍 Affects 2 files
features/admin.applications.v1/components/client-secrets/delete-client-secret-modal.tsx#L19-L21(this comment)features/admin.applications.v1/components/client-secrets/previous-client-secrets.tsx#L19-L27
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@features/admin.applications.v1/components/client-secrets/delete-client-secret-modal.tsx`
around lines 19 - 21, Migrate delete-client-secret-modal.tsx by replacing
ConfirmationModal and Semantic UI subcomponents with Oxygen Dialog, Checkbox,
and Button while preserving the assertion checkbox, non-dismissible backdrop,
callbacks, and existing data-componentid values. In previous-client-secrets.tsx,
replace ContentLoader, Message, and Popup with Oxygen equivalents, remove
previous-client-secrets.scss and hardcoded dimensions, and use MUI styled or
limited theme-based sx values for styling.
Source: Coding guidelines
| const resolveExpiresAt = (values: GenerateClientSecretFormValues): number | undefined => { | ||
| const nowInSeconds: number = Math.floor(Date.now() / 1000); | ||
|
|
||
| switch (values.expiration) { | ||
| case ClientSecretExpirationOption.NO_EXPIRATION: | ||
| return undefined; | ||
| case ClientSecretExpirationOption.CUSTOM: | ||
| return nowInSeconds + Number(values.customExpiryDays) * SECONDS_PER_DAY; | ||
| default: | ||
| return nowInSeconds + Number(values.expiration) * SECONDS_PER_DAY; | ||
| } | ||
| }; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Reject fractional custom expiry values.
validateForm accepts any numeric string in the range 1 to 3650, including decimals such as 1.5. inputType="number" does not block decimals. resolveExpiresAt then computes nowInSeconds + 1.5 * SECONDS_PER_DAY, which produces a non-integer expiresAt. The API contract defines expiresAt as Unix epoch seconds, so the request carries an invalid value.
Add an integer check in validateForm, and floor the computed timestamp in resolveExpiresAt.
🐛 Proposed fix
case ClientSecretExpirationOption.CUSTOM:
- return nowInSeconds + Number(values.customExpiryDays) * SECONDS_PER_DAY;
+ return nowInSeconds + Math.floor(Number(values.customExpiryDays)) * SECONDS_PER_DAY;- } else if (isNaN(days) || days < CUSTOM_EXPIRY_MIN_DAYS || days > CUSTOM_EXPIRY_MAX_DAYS) {
+ } else if (!Number.isInteger(days) || days < CUSTOM_EXPIRY_MIN_DAYS || days > CUSTOM_EXPIRY_MAX_DAYS) {
errors.customExpiryDays =
t("applications:clientSecrets.wizard.customExpiry.validations.invalid");
}Also applies to: 188-204
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@features/admin.applications.v1/components/client-secrets/generate-client-secret-modal.tsx`
around lines 131 - 142, Update validateForm to reject customExpiryDays values
that are not integers while preserving the existing 1–3650 range validation. In
resolveExpiresAt, floor the computed custom expiration timestamp so expiresAt is
always an integer Unix epoch value.
| const resolveClientSecretsDangerZone = (): ReactElement => { | ||
|
|
||
| const oidcConfig: OIDCDataInterface = inboundProtocolConfig?.[ SupportedAuthProtocolTypes.OIDC ]; | ||
|
|
||
| /* | ||
| * Public clients (SPAs, mobile apps) authenticate without a client secret, so there is | ||
| * nothing to revoke or regenerate for them. System and default applications are excluded | ||
| * as well, to mirror the client secrets listing. | ||
| */ | ||
| if (!isMultipleClientSecretsEnabled | ||
| || isSystemApplication | ||
| || isDefaultApplication | ||
| || !oidcConfig?.clientId | ||
| || oidcConfig?.publicClient | ||
| || oidcConfig?.state === State.REVOKED) { | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| return ( | ||
| <RevokeAllClientSecretsDangerZone | ||
| onRevokeAll={ handleRevokeAllClientSecrets } | ||
| readOnly={ readOnly || !hasApplicationUpdatePermissions } | ||
| data-componentid={ `${ componentId }-revoke-all-danger-zone` } | ||
| /> | ||
| ); | ||
| }; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Scope the danger zone to the OIDC protocol view.
resolveClientSecretsDangerZone gates on the presence of an OIDC inbound config, but Line 1446 renders it unconditionally next to resolveInboundProtocolSettingsForm(). For an application that has both SAML and OIDC inbound protocols, the "Revoke All Client Secrets" danger zone appears while the user views the SAML settings. The action then applies to a protocol that is not on screen.
Add a selectedProtocol check to the guard.
🐛 Proposed guard
const oidcConfig: OIDCDataInterface = inboundProtocolConfig?.[ SupportedAuthProtocolTypes.OIDC ];
+ const isOIDCProtocolSelected: boolean = selectedProtocol === SupportedAuthProtocolTypes.OIDC
+ || selectedProtocol === SupportedAuthProtocolTypes.OAUTH2_OIDC
+ || selectedProtocol === SupportedAuthProtocolTypes.OAUTH2;
+
if (!isMultipleClientSecretsEnabled
+ || !isOIDCProtocolSelected
|| isSystemApplication
|| isDefaultApplication
|| !oidcConfig?.clientId
|| oidcConfig?.publicClient
|| oidcConfig?.state === State.REVOKED) {
return null;
}Also applies to: 1446-1446
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@features/admin.applications.v1/components/settings/access-configuration.tsx`
around lines 1038 - 1064, Update resolveClientSecretsDangerZone to require the
selected protocol to be OIDC in its existing guard, alongside the inbound OIDC
configuration checks. Ensure the RevokeAllClientSecretsDangerZone is not
rendered when viewing SAML settings, while preserving the current exclusions and
behavior for the OIDC view.
| content: "If you delete this client secret, the application will no longer be able to " + | ||
| "authenticate. However, active tokens and authorization codes will not be revoked.", | ||
| header: "Are you sure?", | ||
| message: "This action is irreversible and permanently deletes the client secret. " + | ||
| "Please proceed with caution." |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Correct the delete confirmation scope.
Multiple client secrets can remain valid after this operation. The current text states that the whole application can no longer authenticate.
State that clients using the deleted secret can no longer authenticate. State that other client secrets remain valid.
Proposed fix
- content: "If you delete this client secret, the application will no longer be able to " +
- "authenticate. However, active tokens and authorization codes will not be revoked.",
+ content: "Clients using this client secret will no longer be able to authenticate. " +
+ "Other client secrets remain valid. Active tokens and authorization codes will not be revoked.",📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| content: "If you delete this client secret, the application will no longer be able to " + | |
| "authenticate. However, active tokens and authorization codes will not be revoked.", | |
| header: "Are you sure?", | |
| message: "This action is irreversible and permanently deletes the client secret. " + | |
| "Please proceed with caution." | |
| content: "Clients using this client secret will no longer be able to authenticate. " + | |
| "Other client secrets remain valid. Active tokens and authorization codes will not be revoked.", | |
| header: "Are you sure?", | |
| message: "This action is irreversible and permanently deletes the client secret. " + | |
| "Please proceed with caution." |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@modules/i18n/src/translations/en-US/portals/applications.ts` around lines 199
- 203, Update the delete confirmation content near the client-secret
confirmation strings so it states that clients using the deleted secret can no
longer authenticate, while explicitly clarifying that other client secrets
remain valid; preserve the existing notice about active tokens and authorization
codes unless it conflicts with this scope.
Purpose
Introduces a config-gated UI for managing multiple client secrets per OAuth/OIDC application in the Console, enabling seamless secret rotation (generate a new secret, roll over, then delete/expire the old one) without downtime.
Overview of changes
The feature is surfaced on the OIDC protocol tab of the application edit view and is gated behind a config flag, so the legacy single-secret UI is untouched when the feature is off.
OIDC protocol tab (
ClientSecretsSection)Expires Soon / Expired / Never), matching the connection-status dot styling.
409is the authoritative backstop.Application page
Danger zone
APIs (
api/application.ts,useGetOAuthClientSecrets)GET /inbound-protocols/oidc/secrets— list secrets (SWR hook)POST /inbound-protocols/oidc/secrets— create a secret ({ expiresAt? }, epoch seconds)DELETE /inbound-protocols/oidc/secrets/{id}— delete a secretPermissions
applications.enforceClientSecretPermissionfeature flag is on, and fall back to the existing application read/update permissions otherwise.Configuration
Console config lives under the applications feature (rendered from the toml at server startup). Driven by the framework deployment.toml (defaults: enable = true, max_secret_count = 2):
Screen Recordings
Untitled.mov
Screen.Recording.2026-08-11.at.23.56.43.mov
Related PRs