arazzo: 1.0.1 info: title: Keycloak Rotate a Client Secret summary: Resolve a client by clientId, capture the outgoing secret, regenerate it, and verify the new value. description: >- Credential rotation as an executable artifact. The workflow resolves the internal client UUID from the clientId, captures the outgoing secret so the operator can recognise which value is being retired and roll back the deployment side if needed, regenerates the secret, and then reads it back to confirm the new value is live. Rotation is immediate and unrecoverable on the Keycloak side: the moment regeneration succeeds the previous secret stops authenticating, so the new value must reach the application's configuration before the next token request. Every step spells out its request inline so the flow can be read and executed without opening the underlying OpenAPI description. version: 1.0.0 sourceDescriptions: - name: keycloakAdminApi url: ../openapi/keycloak-admin-rest-api-openapi.yml type: openapi workflows: - workflowId: rotate-client-secret summary: Regenerate a confidential client's secret and confirm the new value. description: >- Resolves a client by its clientId, records the current secret, regenerates it, and verifies that the stored secret matches the newly issued value. inputs: type: object required: - realm - clientId properties: realm: type: string description: The name of the realm the client is registered in. clientId: type: string description: The OAuth client identifier whose secret should be rotated. steps: - stepId: resolveClient description: >- Exchange the clientId for the internal client UUID. The secret endpoints are keyed on the UUID, and this lookup is the only way to get it. operationId: getClients parameters: - name: realm in: path value: $inputs.realm - name: clientId in: query value: $inputs.clientId - name: max in: query value: 1 successCriteria: - condition: $statusCode == 200 - context: $response.body condition: $.length > 0 type: jsonpath outputs: clientUuid: $response.body#/0/id resolvedClientId: $response.body#/0/clientId - stepId: loadClient description: >- Read the client representation to confirm it is confidential before rotating. A public client has no secret to rotate, so this step is the guard that keeps the flow from doing something meaningless. operationId: getClient parameters: - name: realm in: path value: $inputs.realm - name: clientUuid in: path value: $steps.resolveClient.outputs.clientUuid successCriteria: - condition: $statusCode == 200 - context: $response.body condition: $.publicClient == false type: jsonpath outputs: clientEnabled: $response.body#/enabled publicClient: $response.body#/publicClient clientAuthenticatorType: $response.body#/clientAuthenticatorType - stepId: captureOutgoingSecret description: >- Record the secret that is about to be retired, so the operator can tell which value was replaced and confirm the rotation actually changed it. operationId: getClientSecret parameters: - name: realm in: path value: $inputs.realm - name: clientUuid in: path value: $steps.resolveClient.outputs.clientUuid successCriteria: - condition: $statusCode == 200 outputs: previousSecret: $response.body#/value - stepId: regenerateSecret description: >- Issue a new client secret. This takes effect immediately and invalidates the previous value, so the application configuration must be updated with the new secret before its next token request. operationId: regenerateClientSecret parameters: - name: realm in: path value: $inputs.realm - name: clientUuid in: path value: $steps.resolveClient.outputs.clientUuid successCriteria: - condition: $statusCode == 200 outputs: newSecretType: $response.body#/type newSecret: $response.body#/value - stepId: verifyRotation description: >- Read the stored secret back and assert it no longer matches the value captured before rotation, proving the new credential is the live one. operationId: getClientSecret parameters: - name: realm in: path value: $inputs.realm - name: clientUuid in: path value: $steps.resolveClient.outputs.clientUuid successCriteria: - condition: $statusCode == 200 - condition: $response.body#/value != $steps.captureOutgoingSecret.outputs.previousSecret outputs: confirmedSecret: $response.body#/value outputs: clientUuid: $steps.resolveClient.outputs.clientUuid clientId: $steps.resolveClient.outputs.resolvedClientId previousSecret: $steps.captureOutgoingSecret.outputs.previousSecret newSecret: $steps.verifyRotation.outputs.confirmedSecret