@@ -23,6 +23,7 @@ import type { SsoRegistrationBody } from '@/lib/api/contracts/auth'
2323import { useSession } from '@/lib/auth/auth-client'
2424import { isEnterprise } from '@/lib/billing/plan-helpers'
2525import { isBillingEnabled } from '@/lib/core/config/env-flags'
26+ import { REDACTED_MARKER } from '@/lib/core/security/redaction'
2627import { getBaseUrl } from '@/lib/core/utils/urls'
2728import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state'
2829import { SettingsPanel } from '@/app/workspace/[workspaceId]/settings/components/settings-panel'
@@ -144,6 +145,13 @@ function OrganizationSsoSettings({ organizationId }: SSOProps) {
144145 const [ errors , setErrors ] = useState < Record < string , string [ ] > > ( DEFAULT_ERRORS )
145146 const [ showErrors , setShowErrors ] = useState ( false )
146147
148+ /**
149+ * Editing an OIDC provider always means a secret is stored — the contract
150+ * requires one to register, and the API returns only its sentinel, never the
151+ * value. Leaving the field blank therefore means "keep it", not "clear it".
152+ */
153+ const hasStoredClientSecret = isEditing && existingProvider ?. providerType === 'oidc'
154+
147155 const hasChanges = ( Object . keys ( formData ) as ( keyof typeof formData ) [ ] ) . some (
148156 ( k ) => formData [ k ] !== originalFormData [ k ]
149157 )
@@ -227,7 +235,9 @@ function OrganizationSsoSettings({ organizationId }: SSOProps) {
227235
228236 if ( providerType === 'oidc' ) {
229237 newErrors . clientId = validateRequired ( 'Client ID' , data . clientId )
230- newErrors . clientSecret = validateRequired ( 'Client Secret' , data . clientSecret )
238+ newErrors . clientSecret = hasStoredClientSecret
239+ ? [ ]
240+ : validateRequired ( 'Client Secret' , data . clientSecret )
231241 if ( ! data . scopes || ! data . scopes . trim ( ) ) {
232242 newErrors . scopes = [ 'Scopes are required for OIDC providers' ]
233243 }
@@ -282,7 +292,12 @@ function OrganizationSsoSettings({ organizationId }: SSOProps) {
282292 image : OIDC_DEFAULT_MAPPING . image ,
283293 } ,
284294 clientId : formData . clientId ,
285- clientSecret : formData . clientSecret ,
295+ // Blank on an edit means the admin did not retype it: send the
296+ // sentinel so the server keeps the stored secret.
297+ clientSecret :
298+ hasStoredClientSecret && ! formData . clientSecret
299+ ? REDACTED_MARKER
300+ : formData . clientSecret ,
286301 scopes : formData . scopes . split ( ',' ) . map ( ( s ) => s . trim ( ) ) ,
287302 ...( formData . authorizationEndpoint . trim ( )
288303 ? { authorizationEndpoint : formData . authorizationEndpoint . trim ( ) }
@@ -373,7 +388,10 @@ function OrganizationSsoSettings({ organizationId }: SSOProps) {
373388 if ( existingProvider . providerType === 'oidc' && existingProvider . oidcConfig ) {
374389 const config = JSON . parse ( existingProvider . oidcConfig )
375390 clientId = config . clientId || ''
376- clientSecret = config . clientSecret || ''
391+ // The API returns the sentinel, never the secret. Showing it verbatim put
392+ // the literal "[REDACTED]" in the field; blanking it lets the placeholder
393+ // say a secret is stored, and submit re-sends the sentinel to keep it.
394+ clientSecret = config . clientSecret === REDACTED_MARKER ? '' : config . clientSecret || ''
377395 scopes = config . scopes ?. join ( ',' ) || 'openid,profile,email'
378396 mapping = config . mapping ?? { }
379397 authorizationEndpoint = config . authorizationEndpoint || ''
@@ -665,6 +683,11 @@ function OrganizationSsoSettings({ organizationId }: SSOProps) {
665683
666684 < SettingRow
667685 label = 'Client Secret'
686+ description = {
687+ hasStoredClientSecret
688+ ? 'Your saved secret is never shown again. Leave this blank to keep it, or type a new one to replace it.'
689+ : undefined
690+ }
668691 error = {
669692 showErrors && errors . clientSecret . length > 0
670693 ? errors . clientSecret . join ( ' ' )
@@ -674,7 +697,7 @@ function OrganizationSsoSettings({ organizationId }: SSOProps) {
674697 < ChipInput
675698 id = 'sso-client-secret'
676699 type = 'text'
677- placeholder = ' Enter Client Secret'
700+ placeholder = { hasStoredClientSecret ? '••••••••••••' : ' Enter Client Secret'}
678701 value = { formData . clientSecret }
679702 name = 'sso_client_key'
680703 autoComplete = 'off'
0 commit comments