8000 Admin API v2: add stricter validation constrains for Client resource by michalvavrik · Pull Request #48935 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content

Admin API v2: add stricter validation constrains for Client resource - #48935

Merged
vmuzikar merged 1 commit into
keycloak:mainfrom
michalvavrik:feature/issues/48786
May 25, 2026
Merged

vmuzikar merged 1 commit into
keycloak:mainfrom
michalvavrik:feature/issues/48786

Conversation

@michalvavrik
@michalvavrik michalvavrik commented May 12, 2026 •
Copy link
Copy Markdown
Member
Schema#Property New Validation Constraint Reasoning
BaseClientRepresentation#clientId @Size(min=1, max=255) DB VARCHAR(255)
BaseClientRepresentation#displayName @Size(max=255) DB NVARCHAR(255)
BaseClientRepresentation#description @Size(max=255) DB NVARCHAR(255)
BaseClientRepresentation#appUrl @Size(max=255) DB VARCHAR(255)
BaseClientRepresentation#redirectUris @Size(max=100) on set, @Size(max=255) on elements DB VARCHAR(255) per element, bound collection
BaseClientRepresentation#roles @Size(max=300) on set, @Size(max=255) on elements DB VARCHAR(255) per element, bound collection
OIDCClientRepresentation#webOrigins @Size(max=100) on set, @Size(max=255) on elements DB VARCHAR(255) per element, bound collection
OIDCClientRepresentation#webOrigins @ValidWebOrigin RFC 6454 format, invalid origins silently break CORS
OIDCClientRepresentation#serviceAccountRoles @Size(max=300) on set, @Size(max=255) on elements DB VARCHAR(255) per element, bound collection
OIDCClientRepresentation.Auth#method @NotBlank must be specified when auth object is present
OIDCClientRepresentation.Auth#method @ValidAuthMethod open SPI, dynamic check against registered providers
OIDCClientRepresentation.Auth#secret @Size(min=6, max=255) DB VARCHAR(255), min=6 prevents trivially weak secrets
OIDCClientRepresentation.Auth#certificate @Size(max=65536) NCLOB unlimited, adding at least upper bound
OIDCClientRepresentation (class-level) @ConfidentialFlowsRequireAuth SERVICE_ACCOUNT/TOKEN_EXCHANGE fail at runtime without auth
OIDCClientRepresentation (class-level) @RedirectFlowsRequireUris STANDARD/IMPLICIT fail at runtime without redirect URIs
OIDCClientRepresentation (class-level) @ServiceAccountRolesRequireFlow roles silently ignored without SERVICE_ACCOUNT flow
SAMLClientRepresentation#nameIdFormat type changed to enum NameIdFormat SAML 2.0 spec frozen since 2005, unlikely to change
SAMLClientRepresentation#signatureAlgorithm type changed to enum SignatureAlgorithm PQC may require enum update, but it is easy
SAMLClientRepresentation#signatureCanonicalizationMethod @ValidCanonicalizationMethod finite set from JDK constants, W3C standard unchanged since 2008
SAMLClientRepresentation#signingCertificate @Size(max=65536) NCLOB unlimited, adding at least an upper bound

I have full analysis of constrains I have considered but did not add if anyone is interested, shout.

@shawkins
Copy link
Copy Markdown
Contributor

@michalvavrik @vmuzikar I think what you have here looks good.

What's here does not look like representation specific validation - it's based upon existing logic / constraints, correct?

I think there was also a mention that v2 may want stricter validation than what v1 allows, but that would need its own issue as that creates situations where v1 clients would not be valid in v2, and we need to have a complete story around that in particular for the admin ui.

Related to #48786 (comment) - having the validations captured in annotations definitely works well with what we're already doing in admin api v2 given the usage of the hibernate validator and the exposure of the validation metadata in the openapi, but it is not reusable by non-v2 scenarios. What kind of follow-ups would be good to capture? Things like - being able to reuse the validation annotations directly on the Entities, a way to introspect the hibernate persistence metadata, enhancing ModelValidationException and the general exception mapping logic (EntityManagerProxy.convert) to make the mapping to representation easier, etc.

@michalvavrik
Copy link
Copy Markdown
Member Author

What's here does not look like representation specific validation - it's based upon existing logic / constraints, correct?

Yes, I have tried to identify states that are allowed by our API but fails later, like when you try to use the client. Or which fails down on the database later, which doesn't provide as clear validation exceptions as we do (typically I got 500 when DB insert failed due to exceeded value of the column length). However, if someone does updates / patches in steps (like first set standard flow, then remember redirect URIs). That is intentional, because we want to prevent illegal states that won't work.

I think there was also a mention that v2 may want stricter validation than what v1 allows, but that would need its own issue as that creates situations where v1 clients would not be valid in v2, and we need to have a complete story around that in particular for the admin ui.

I agree. At minimal, we need to ensure that validation messages at showed in the Admin UI (when user tries to save wrong values), but ideally we could be step ahead and adjust UI parts (like input max length, min length and so on). Sounds like a follow-up.

Related to #48786 (comment) - having the validations captured in annotations definitely works well with what we're already doing in admin api v2 given the usage of the hibernate validator and the exposure of the validation metadata in the openapi, but it is not reusable by non-v2 scenarios. What kind of follow-ups would be good to capture? Things like - being able to reuse the validation annotations directly on the Entities, a way to introspect the hibernate persistence metadata, enhancing ModelValidationException and the general exception mapping logic (EntityManagerProxy.convert) to make the mapping to representation easier, etc.

I will defer this note to @vmuzikar .

@michalvavrik
michalvavrik force-pushed the feature/issues/48786 branch from fe56b31 to f2bf75c Compare May 15, 2026 10:02
@michalvavrik
michalvavrik requested a review from shawkins May 15, 2026 11:00
shawkins
shawkins previously approved these changes May 18, 2026
@shawkins shawkins left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Pepo48
Pepo48 previously approved these changes May 18, 2026
@Pepo48 Pepo48 left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR, Michal! Just two nits to consider (but nothing serious that would block this). Thus, approving.

@michalvavrik
michalvavrik dismissed stale reviews from Pepo48 and shawkins via ed8991e May 18, 2026 21:04
@michalvavrik
michalvavrik force-pushed the feature/issues/48786 branch from f2bf75c to ed8991e Compare May 18, 2026 21:04
@vmuzikar
Copy link
Copy Markdown
Contributor

I'd like to look into this ASAP as well.

Pepo48
Pepo48 previously approved these changes May 19, 2026
@vmuzikar vmuzikar left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@michalvavrik Overall looks good, nice work. Added some minor comments.

Comment on lines +39 to +60
public enum NameIdFormat {
USERNAME, EMAIL, PERSISTENT, TRANSIENT;

@JsonValue
public String toJson() {
return name().toLowerCase();
}

@JsonCreator
public static NameIdFormat fromJson(String value) {
return value == null ? null : valueOf(value.toUpperCase());
}
}

public enum SignatureAlgorithm {
RSA_SHA1, RSA_SHA256, RSA_SHA256_MGF1, RSA_SHA512, RSA_SHA512_MGF1, DSA_SHA1;

@JsonCreator
public static SignatureAlgorithm fromJson(String value) {
return value == null ? null : valueOf(value);
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a note. Obviously the enums should be somewhere in the core codebase, the API layer should not be the source of truth for this. But it's ok for the MVP, let's keep as is.

Comment on lines +18 to +19
CanonicalizationMethod.INCLUSIVE_11,
CanonicalizationMethod.INCLUSIVE_11_WITH_COMMENTS
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is INCLUSIVE_11 coming from? It doesn't seem to be possible to set this value in the UI?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is INCLUSIVE_11 coming from? It doesn't seem to be possible to set this value in the UI?

My understanding is that Keycloak does not restrict valid values. It stores the canonicalization method and passes it to this code:

CanonicalizationMethod canonicalizationMethod = fac.newCanonicalizationMethod(canonicalizationMethodType,

then what is valid value is determined by XML DSIG provider and the default provider registers all 6 constants that I listed in here. If you want to restrict it to UI only, I can do that. However, even of the 4 listed values in UI, but I only found tests for 2 of them (EXCLUSIVE_WITH_COMMENTS and EXCLUSIVE), so I don't have explicit argument against the ones I included (like argument it is not tested...which it is not as well).

It is your call, let me know.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's keep as is. :)

@michalvavrik
michalvavrik force-pushed the feature/issues/48786 branch 2 times, most recently from ef0a1b7 to 9529a54 Compare May 20, 2026 22:38
@stianst stianst removed the team/ui label May 21, 2026
@michalvavrik
michalvavrik requested a review from vmuzikar May 22, 2026 10:00
@vmuzikar vmuzikar left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, thanks.

@vmuzikar
vmuzikar force-pushed the feature/issues/48786 branch from 9529a54 to b57b0ef Compare May 22, 2026 13:55
@vmuzikar
vmuzikar enabled auto-merge (squash) May 22, 2026 13:55
* Closes: keycloak#48786
* Changes:

| Schema#Property | New Validation Constraint | Reasoning |
|-----------------|--------------------------|-----------|
| BaseClientRepresentation#clientId | `@Size(min=1, max=255)` | DB VARCHAR(255) |
| BaseClientRepresentation#displayName | `@Size(max=255)` | DB NVARCHAR(255) |
| BaseClientRepresentation#description | `@Size(max=255)` | DB NVARCHAR(255) |
| BaseClientRepresentation#appUrl | `@Size(max=255)` | DB VARCHAR(255) |
| BaseClientRepresentation#redirectUris | `@Size(max=100)` on set, `@Size(max=255)` on elements | DB VARCHAR(255) per element, bound collection |
| BaseClientRepresentation#roles | `@Size(max=300)` on set, `@Size(max=255)` on elements | DB VARCHAR(255) per element, bound collection |
| OIDCClientRepresentation#webOrigins | `@Size(max=100)` on set, `@Size(max=255)` on elements | DB VARCHAR(255) per element, bound collection |
| OIDCClientRepresentation#webOrigins | `@ValidWebOrigin` | RFC 6454 format, invalid origins silently break CORS |
| OIDCClientRepresentation#serviceAccountRoles | `@Size(max=300)` on set, `@Size(max=255)` on elements | DB VARCHAR(255) per element, bound collection |
| OIDCClientRepresentation.Auth#method | `@NotBlank` | must be specified when auth object is present |
| OIDCClientRepresentation.Auth#method | `@ValidAuthMethod` | open SPI, dynamic check against registered providers |
| OIDCClientRepresentation.Auth#secret | `@Size(min=6, max=255)` | DB VARCHAR(255), min=6 prevents trivially weak secrets |
| OIDCClientRepresentation.Auth#certificate | `@Size(max=65536)` | NCLOB unlimited, practical upper bound |
| OIDCClientRepresentation (class-level) | `@ConfidentialFlowsRequireAuth` | SERVICE_ACCOUNT/TOKEN_EXCHANGE fail at runtime without auth |
| OIDCClientRepresentation (class-level) | `@RedirectFlowsRequireUris` | STANDARD/IMPLICIT fail at runtime without redirect URIs |
| OIDCClientRepresentation (class-level) | `@ServiceAccountRolesRequireFlow` | roles silently ignored without SERVICE_ACCOUNT flow |
| SAMLClientRepresentation#nameIdFormat | type changed to enum `NameIdFormat` | SAML 2.0 spec frozen since 2005, unlikely to change |
| SAMLClientRepresentation#signatureAlgorithm | type changed to enum | enum could change due to PQC, but we can simply update enum |
| SAMLClientRepresentation#signatureCanonicalizationMethod | `@ValidCanonicalizationMethod` | finite set from JDK constants, W3C standard unchanged since 2008 |
| SAMLClientRepresentation#signingCertificate | `@Size(max=65536)` | NCLOB unlimited, practical upper bound |

Signed-off-by: Michal Vavřík <michal.vavrik@aol.com>
@vmuzikar
vmuzikar force-pushed the feature/issues/48786 branch from b57b0ef to 0b164b9 Compare May 25, 2026 06:48
@vmuzikar
vmuzikar merged commit 8171999 into keycloak:main May 25, 2026
91 checks passed
@michalvavrik
michalvavrik deleted the feature/issues/48786 branch May 25, 2026 07:55
@stianst stianst mentioned this pull request Jun 1, 2026
sashyo pushed a commit to tide-foundation/keycloak-IGA that referenced this pull request Jul 20, 2026
* Closes: keycloak#48786
* Changes:

| Schema#Property | New Validation Constraint | Reasoning |
|-----------------|--------------------------|-----------|
| BaseClientRepresentation#clientId | `@Size(min=1, max=255)` | DB VARCHAR(255) |
| BaseClientRepresentation#displayName | `@Size(max=255)` | DB NVARCHAR(255) |
| BaseClientRepresentation#description | `@Size(max=255)` | DB NVARCHAR(255) |
| BaseClientRepresentation#appUrl | `@Size(max=255)` | DB VARCHAR(255) |
| BaseClientRepresentation#redirectUris | `@Size(max=100)` on set, `@Size(max=255)` on elements | DB VARCHAR(255) per element, bound collection |
| BaseClientRepresentation#roles | `@Size(max=300)` on set, `@Size(max=255)` on elements | DB VARCHAR(255) per element, bound collection |
| OIDCClientRepresentation#webOrigins | `@Size(max=100)` on set, `@Size(max=255)` on elements | DB VARCHAR(255) per element, bound collection |
| OIDCClientRepresentation#webOrigins | `@ValidWebOrigin` | RFC 6454 format, invalid origins silently break CORS |
| OIDCClientRepresentation#serviceAccountRoles | `@Size(max=300)` on set, `@Size(max=255)` on elements | DB VARCHAR(255) per element, bound collection |
| OIDCClientRepresentation.Auth#method | `@NotBlank` | must be specified when auth object is present |
| OIDCClientRepresentation.Auth#method | `@ValidAuthMethod` | open SPI, dynamic check against registered providers |
| OIDCClientRepresentation.Auth#secret | `@Size(min=6, max=255)` | DB VARCHAR(255), min=6 prevents trivially weak secrets |
| OIDCClientRepresentation.Auth#certificate | `@Size(max=65536)` | NCLOB unlimited, practical upper bound |
| OIDCClientRepresentation (class-level) | `@ConfidentialFlowsRequireAuth` | SERVICE_ACCOUNT/TOKEN_EXCHANGE fail at runtime without auth |
| OIDCClientRepresentation (class-level) | `@RedirectFlowsRequireUris` | STANDARD/IMPLICIT fail at runtime without redirect URIs |
| OIDCClientRepresentation (class-level) | `@ServiceAccountRolesRequireFlow` | roles silently ignored without SERVICE_ACCOUNT flow |
| SAMLClientRepresentation#nameIdFormat | type changed to enum `NameIdFormat` | SAML 2.0 spec frozen since 2005, unlikely to change |
| SAMLClientRepresentation#signatureAlgorithm | type changed to enum | enum could change due to PQC, but we can simply update enum |
| SAMLClientRepresentation#signatureCanonicalizationMethod | `@ValidCanonicalizationMethod` | finite set from JDK constants, W3C standard unchanged since 2008 |
| SAMLClientRepresentation#signingCertificate | `@Size(max=65536)` | NCLOB unlimited, practical upper bound |

Signed-off-by: Michal Vavřík <michal.vavrik@aol.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Strict contract on Client v2

5 participants

0