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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 74 additions & 23 deletions js/libs/keycloak-admin-client/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,26 @@ components:
schemas:
Auth:
type: object
required:
- method
properties:
method:
type: string
description: Which authentication method is used for this client
pattern: \S
description: "Which authentication method is used for this client. Validation:\
\ must not be blank; valid client authenticator type is required"
secret:
type: string
minLength: 6
maxLength: 255
description: "Secret used to authenticate this client with Secret authentication.\
\ Validation: on update: Client secret must not be blank"
\ Validation: size must be between 6 and 255; on update: Client secret\
\ must not be blank"
certificate:
type: string
description: Public key used to authenticate this client with Signed JWT
authentication
maxLength: 65536
description: "Public key used to authenticate this client with Signed JWT\
\ authentication. Validation: size must be between 0 and 65536"
BaseClientRepresentation:
description: Base client representation with common properties for all client
types
Expand All @@ -29,38 +37,52 @@ components:
clientId:
type: string
pattern: \S
minLength: 1
maxLength: 255
description: "ID uniquely identifying this client. Validation: must not\
\ be blank"
\ be blank; size must be between 1 and 255"
displayName:
type: string
description: Human readable name of the client
maxLength: 255
description: "Human readable name of the client. Validation: size must be\
\ between 0 and 255"
description:
type: string
description: Human readable description of the client
maxLength: 255
description: "Human readable description of the client. Validation: size\
\ must be between 0 and 255"
enabled:
type: boolean
description: Whether this client is enabled
appUrl:
type: string
maxLength: 255
description: "URL to the application's homepage that is represented by this\
\ client. Validation: must be a valid URL"
\ client. Validation: size must be between 0 and 255; must be a valid\
\ URL"
format: uri
redirectUris:
type: array
uniqueItems: true
items:
type: string
pattern: \S
maxLength: 255
maxItems: 100
description: "URIs that the browser can redirect to after login. Validation:\
\ each element must not be blank; Invalid redirect URI"
\ size must be between 0 and 100; each element must not be blank, size\
\ must be between 0 and 255; Invalid redirect URI"
roles:
type: array
uniqueItems: true
items:
type: string
pattern: \S
description: "Roles associated with this client. Validation: each element\
\ must not be blank"
maxLength: 255
maxItems: 300
description: "Roles associated with this client. Validation: size must be\
\ between 0 and 300; each element must not be blank, size must be between\
\ 0 and 255"
protocol:
type: string
pattern: \S
Expand All @@ -84,6 +106,13 @@ components:
- TOKEN_EXCHANGE
- DEVICE
- CIBA
NameIdFormat:
type: string
enum:
- username
- email
- persistent
- transient
OIDCClientRepresentation:
type: object
properties:
Expand All @@ -92,7 +121,10 @@ components:
uniqueItems: true
items:
$ref: "#/components/schemas/Flow"
description: Login flows that are enabled for this client
description: "Login flows that are enabled for this client. Validation:\
\ SERVICE_ACCOUNT and TOKEN_EXCHANGE flows require a confidential client\
\ (auth must be specified); STANDARD and IMPLICIT flows require at least\
\ one redirect URI"
auth:
$ref: "#/components/schemas/Auth"
description: Authentication configuration for this client
Expand All @@ -102,16 +134,24 @@ components:
items:
type: string
pattern: \S
maxLength: 255
maxItems: 100
description: "Web origins that are allowed to make requests to this client.\
\ Validation: each element must not be blank"
\ Validation: size must be between 0 and 100; each element must not be\
\ blank, size must be between 0 and 255, must be a valid web origin (scheme://host[:port]),\
\ or '+' to derive from redirect URIs, or '*' to allow all"
serviceAccountRoles:
type: array
uniqueItems: true
items:
type: string
pattern: \S
description: "Roles assigned to the service account. Validation: each element\
\ must not be blank"
maxLength: 255
maxItems: 300
description: "Roles assigned to the service account. Validation: size must\
\ be between 0 and 300; each element must not be blank, size must be between\
\ 0 and 255; serviceAccountRoles can only be set when SERVICE_ACCOUNT\
\ flow is enabled"
protocol:
type: string
allOf:
Expand All @@ -121,9 +161,8 @@ components:
type: object
properties:
nameIdFormat:
type: string
description: "Name ID format to use for the subject (e.g., 'username', 'email',\
\ 'transient', 'persistent')"
$ref: "#/components/schemas/NameIdFormat"
description: Name ID format to use for the subject
forceNameIdFormat:
type: boolean
description: Force the specified Name ID format even if the client requests
Expand All @@ -147,22 +186,34 @@ components:
type: boolean
description: Use front-channel logout (browser redirect)
signatureAlgorithm:
type: string
description: "Signature algorithm for signing SAML documents (e.g., 'RSA_SHA256',\
\ 'RSA_SHA512')"
$ref: "#/components/schemas/SignatureAlgorithm"
description: Signature algorithm for signing SAML documents
signatureCanonicalizationMethod:
type: string
description: Canonicalization method for XML signatures
description: "Canonicalization method for XML signatures. Validation: must\
\ be a valid XML canonicalization method URI (see javax.xml.crypto.dsig.CanonicalizationMethod\
\ constants)"
signingCertificate:
type: string
description: "X.509 certificate for signing (PEM format, without headers)"
maxLength: 65536
description: "X.509 certificate for signing (PEM format, without headers).\
\ Validation: size must be between 0 and 65536"
allowEcpFlow:
type: boolean
description: Allow ECP (Enhanced Client or Proxy) flow
protocol:
type: string
allOf:
- $ref: "#/components/schemas/BaseClientRepresentation"
SignatureAlgorithm:
type: string
enum:
- RSA_SHA1
- RSA_SHA256
- RSA_SHA256_MGF1
- RSA_SHA512
- RSA_SHA512_MGF1
- DSA_SHA1
securitySchemes:
bearer-auth:
type: http
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import java.util.Set;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

import org.keycloak.representations.admin.v2.validation.PatchClient;
import org.keycloak.representations.admin.v2.validation.ProtocolUnmodified;
Expand Down Expand Up @@ -40,29 +41,35 @@ public abstract class BaseClientRepresentation extends BaseRepresentation implem
protected String uuid;

@NotBlank
@Size(min = 1, max = 255)
@JsonPropertyDescription("ID uniquely identifying this client")
protected String clientId;

@Size(max = 255)
@JsonPropertyDescription("Human readable name of the client")
private String displayName;

@Size(max = 255)
@JsonPropertyDescription("Human readable description of the client")
private String description;

@JsonPropertyDescription("Whether this client is enabled")
private Boolean enabled;

@URL
@Size(max = 255)
@JsonPropertyDescription("URL to the application's homepage that is represented by this client")
private String appUrl;

@Size(max = 100)
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@JsonPropertyDescription("URIs that the browser can redirect to after login")
private Set<@NotBlank String> redirectUris = new LinkedHashSet<>();
private Set<@NotBlank @Size(max = 255) String> redirectUris = new LinkedHashSet<>();

@Size(max = 300)
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@JsonPropertyDescription("Roles associated with this client")
private Set<@NotBlank String> roles = new LinkedHashSet<>();
private Set<@NotBlank @Size(max = 255) String> roles = new LinkedHashSet<>();

@Override
public String getUuid() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,25 @@

import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

import org.keycloak.representations.admin.v2.validation.ClientSecretNotBlank;
import org.keycloak.representations.admin.v2.validation.ConfidentialFlowsRequireAuth;
import org.keycloak.representations.admin.v2.validation.PutClient;
import org.keycloak.representations.admin.v2.validation.RedirectFlowsRequireUris;
import org.keycloak.representations.admin.v2.validation.ServiceAccountRolesRequireFlow;
import org.keycloak.representations.admin.v2.validation.ValidAuthMethod;
import org.keycloak.representations.admin.v2.validation.ValidWebOrigin;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonMerge;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import org.eclipse.microprofile.openapi.annotations.media.Schema;

@Schema
@ConfidentialFlowsRequireAuth
@RedirectFlowsRequireUris
@ServiceAccountRolesRequireFlow
public class OIDCClientRepresentation extends BaseClientRepresentation {
public static final String PROTOCOL = "openid-connect";

Expand All @@ -38,13 +47,15 @@ public enum Flow {
@JsonPropertyDescription("Authentication configuration for this client")
private Auth auth;

@Size(max = 100)
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@JsonPropertyDescription("Web origins that are allowed to make requests to this client")
private Set<@NotBlank String> webOrigins = new LinkedHashSet<>();
private Set<@NotBlank @Size(max = 255) @ValidWebOrigin String> webOrigins = new LinkedHashSet<>();

@Size(max = 300)
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@JsonPropertyDescription("Roles assigned to the service account")
private Set<@NotBlank String> serviceAccountRoles = new LinkedHashSet<>();
private Set<@NotBlank @Size(max = 255) String> serviceAccountRoles = new LinkedHashSet<>();

public OIDCClientRepresentation() {}

Expand Down Expand Up @@ -92,12 +103,16 @@ public String getProtocol() {
@ClientSecretNotBlank(groups = PutClient.class, affectedFieldNames = {"secret"})
public static class Auth extends BaseRepresentation {

@NotBlank
@ValidAuthMethod
@JsonPropertyDescription("Which authentication method is used for this client")
private String method;

@Size(min = 6, max = 255)
@JsonPropertyDescription("Secret used to authenticate this client with Secret authentication")
private String secret;

@Size(max = 65536)
@JsonPropertyDescription("Public key used to authenticate this client with Signed JWT authentication")
private String certificate;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,13 @@

import java.util.Objects;

import jakarta.validation.constraints.Size;

import org.keycloak.representations.admin.v2.validation.ValidCanonicalizationMethod;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import com.fasterxml.jackson.annotation.JsonValue;
import org.eclipse.microprofile.openapi.annotations.media.Schema;

/**
Expand All @@ -30,8 +36,31 @@
public class SAMLClientRepresentation extends BaseClientRepresentation {
public static final String PROTOCOL = "saml";

@JsonPropertyDescription("Name ID format to use for the subject (e.g., 'username', 'email', 'transient', 'persistent')")
private String nameIdFormat;
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);
}
}
Comment on lines +39 to +60
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.


@JsonPropertyDescription("Name ID format to use for the subject")
private NameIdFormat nameIdFormat;

@JsonPropertyDescription("Force the specified Name ID format even if the client requests a different one")
private Boolean forceNameIdFormat;
Expand All @@ -54,12 +83,14 @@ public class SAMLClientRepresentation extends BaseClientRepresentation {
@JsonPropertyDescription("Use front-channel logout (browser redirect)")
private Boolean frontChannelLogout;

@JsonPropertyDescription("Signature algorithm for signing SAML documents (e.g., 'RSA_SHA256', 'RSA_SHA512')")
private String signatureAlgorithm;
@JsonPropertyDescription("Signature algorithm for signing SAML documents")
private SignatureAlgorithm signatureAlgorithm;

@ValidCanonicalizationMethod
@JsonPropertyDescription("Canonicalization method for XML signatures")
private String signatureCanonicalizationMethod;

@Size(max = 65536)
@JsonPropertyDescription("X.509 certificate for signing (PEM format, without headers)")
private String signingCertificate;

Expand All @@ -71,11 +102,11 @@ public String getProtocol() {
return PROTOCOL;
}

public String getNameIdFormat() {
public NameIdFormat getNameIdFormat() {
return nameIdFormat;
}

public void setNameIdFormat(String nameIdFormat) {
public void setNameIdFormat(NameIdFormat nameIdFormat) {
this.nameIdFormat = nameIdFormat;
}

Expand Down Expand Up @@ -135,11 +166,11 @@ public void setFrontChannelLogout(Boolean frontChannelLogout) {
this.frontChannelLogout = frontChannelLogout;
}

public String getSignatureAlgorithm() {
public SignatureAlgorithm getSignatureAlgorithm() {
return signatureAlgorithm;
}

public void setSignatureAlgorithm(String signatureAlgorithm) {
public void setSignatureAlgorithm(SignatureAlgorithm signatureAlgorithm) {
this.signatureAlgorithm = signatureAlgorithm;
}

Expand Down
Loading
Loading
0