@@ -11,33 +11,66 @@ import { t, type Static } from 'elysia'
1111/**
1212 * Identity Provider configuration schema for OIDC/SAML providers
1313 */
14+ /**
15+ * The Keycloak identity-provider `config` map.
16+ *
17+ * TWO DEFECTS THIS FIXES, both of which made an OIDC provider impossible to create
18+ * through any caller β the admin UI, the generated client, and the MCP tool alike.
19+ *
20+ * 1. `clientId` and `authorizationUrl` were absent. Two of the four keys Keycloak
21+ * requires for `oidc` could not be named at all, so every create reached Keycloak
22+ * without them and came back 500 `unknown_error`.
23+ * 2. The object was closed, and Elysia STRIPS undeclared body properties rather than
24+ * rejecting them. So a caller that sent `clientId` anyway had it removed silently,
25+ * before the handler ran β no error, no warning, nothing in a log.
26+ *
27+ * Hence `additionalProperties`: the named keys below document what a provider commonly
28+ * needs, and anything else Keycloak supports still travels. The old `additionalConfig`
29+ * sub-object was not that escape hatch β Keycloak has no `additionalConfig` key, so
30+ * anything nested under it was passed to Keycloak and ignored, which reads as an escape
31+ * hatch while being a dead end.
32+ *
33+ * Values are strings because Keycloak stores this map as strings; the few booleans it
34+ * accepts are declared explicitly below.
35+ */
1436export const IdentityProviderConfig = t . Object ( {
15- // Common fields
16- displayName : t . Optional ( t . String ( { description : 'Display name for UI' } ) ) ,
17- enabled : t . Optional ( t . Boolean ( { description : 'Whether the provider is enabled' , default : true } ) ) ,
18-
19- // OIDC/OAuth2 specific fields
20- clientSecret : t . Optional ( t . String ( { description : 'OAuth2 client secret' } ) ) ,
21- tokenUrl : t . Optional ( t . String ( { description : 'Token endpoint URL' } ) ) ,
37+ // ββ OIDC / OAuth2 ββ
38+ clientId : t . Optional ( t . String ( { description : 'OAuth2 client id issued by the provider. REQUIRED for oidc, oauth2, keycloak-oidc and the social providers.' } ) ) ,
39+ clientSecret : t . Optional ( t . String ( { description : 'OAuth2 client secret. REQUIRED for oidc, oauth2, keycloak-oidc and the social providers.' } ) ) ,
40+ authorizationUrl : t . Optional ( t . String ( { description : "The provider's authorization endpoint URL. REQUIRED for oidc, oauth2 and keycloak-oidc." } ) ) ,
41+ tokenUrl : t . Optional ( t . String ( { description : "The provider's token endpoint URL. REQUIRED for oidc, oauth2 and keycloak-oidc." } ) ) ,
2242 userInfoUrl : t . Optional ( t . String ( { description : 'UserInfo endpoint URL' } ) ) ,
23- issuer : t . Optional ( t . String ( { description : 'OIDC issuer URL' } ) ) ,
24- defaultScopes : t . Optional ( t . String ( { description : 'Default OAuth2 scopes' } ) ) ,
43+ issuer : t . Optional ( t . String ( { description : 'OIDC issuer URL, validated against the id token iss claim ' } ) ) ,
44+ defaultScopes : t . Optional ( t . String ( { description : 'Space-separated OAuth2 scopes to request (e.g. "openid profile email") ' } ) ) ,
2545 logoutUrl : t . Optional ( t . String ( { description : 'Logout endpoint URL' } ) ) ,
26-
27- // SAML specific fields
46+ clientAuthMethod : t . Optional ( t . String ( { description : 'Client authentication method (client_secret_post, client_secret_basic, client_secret_jwt, private_key_jwt)' } ) ) ,
47+ validateSignature : t . Optional ( t . Boolean ( { description : 'Validate signatures on tokens/assertions from this provider' } ) ) ,
48+ useJwksUrl : t . Optional ( t . Boolean ( { description : 'Fetch the signing keys from jwksUrl rather than using a static certificate' } ) ) ,
49+ jwksUrl : t . Optional ( t . String ( { description : "The provider's JWKS URL, used when useJwksUrl is true" } ) ) ,
50+ pkceEnabled : t . Optional ( t . Boolean ( { description : 'Send a PKCE challenge on the authorization request' } ) ) ,
51+ pkceMethod : t . Optional ( t . String ( { description : 'PKCE code challenge method (S256 or plain)' } ) ) ,
52+ syncMode : t . Optional ( t . String ( { description : 'How brokered user data is synced on later logins: IMPORT, LEGACY or FORCE' } ) ) ,
53+
54+ // ββ SAML ββ
2855 entityId : t . Optional ( t . String ( { description : 'SAML entity ID' } ) ) ,
29- singleSignOnServiceUrl : t . Optional ( t . String ( { description : 'SAML SSO URL' } ) ) ,
56+ singleSignOnServiceUrl : t . Optional ( t . String ( { description : 'SAML SSO URL. REQUIRED for saml. ' } ) ) ,
3057 singleLogoutServiceUrl : t . Optional ( t . String ( { description : 'SAML SLO URL' } ) ) ,
3158 metadataDescriptorUrl : t . Optional ( t . String ( { description : 'SAML metadata URL' } ) ) ,
3259 signatureAlgorithm : t . Optional ( t . String ( { description : 'SAML signature algorithm' } ) ) ,
3360 nameIdPolicyFormat : t . Optional ( t . String ( { description : 'SAML NameID format' } ) ) ,
3461 signingCertificate : t . Optional ( t . String ( { description : 'SAML signing certificate' } ) ) ,
35- validateSignature : t . Optional ( t . Boolean ( { description : 'Validate SAML signatures' } ) ) ,
3662 wantAuthnRequestsSigned : t . Optional ( t . Boolean ( { description : 'Require signed AuthN requests' } ) ) ,
37-
38- // Allow additional configuration
39- additionalConfig : t . Optional ( t . Record ( t . String ( ) , t . Any ( ) ) )
40- } , { title : 'IdentityProviderConfig' } )
63+
64+ // ββ Kept for the admin UI, which writes it alongside the real keys ββ
65+ displayName : t . Optional ( t . String ( { description : 'Display name for UI' } ) ) ,
66+ } , {
67+ title : 'IdentityProviderConfig' ,
68+ // Any other key this provider type supports, passed through to Keycloak verbatim.
69+ // Booleans and numbers are accepted as well as strings: Keycloak stores the map as
70+ // strings and coerces, and restricting to strings would recreate the dead end this
71+ // replaces for keys whose natural JSON form is not a string.
72+ additionalProperties : t . Union ( [ t . String ( ) , t . Boolean ( ) , t . Number ( ) ] ) ,
73+ } )
4174
4275export const IdentityProvider = t . Object ( {
4376 alias : t . Optional ( t . String ( { description : 'Provider alias (unique identifier)' } ) ) ,
0 commit comments