From a56068e7e2faaee3bd70027429915612fc14e0b3 Mon Sep 17 00:00:00 2001 From: mposolda Date: Mon, 12 Aug 2024 10:03:58 +0200 Subject: [PATCH] Sync admin client from Keycloak main and re-enable testing with 24 closes keycloak/keycloak#32036 Signed-off-by: mposolda --- .github/scripts/sync-keycloak-sources.sh | 5 + .github/workflows/ci.yml | 2 +- README.md | 18 +++ .../java/org/keycloak/OAuth2Constants.java | 2 + .../admin/client/JacksonProvider.java | 14 +++ .../resource/OrganizationMemberResource.java | 13 ++- .../resource/OrganizationMembersResource.java | 11 +- .../idm/MemberRepresentation.java | 39 +++++++ .../representations/idm/MembershipType.java | 30 +++++ .../idm/OrganizationRepresentation.java | 10 +- .../idm/UserRepresentation.java | 37 ++++++ .../java/org/keycloak/OAuth2Constants.java | 2 + .../admin/client/JacksonProvider.java | 14 +++ .../resource/OrganizationMemberResource.java | 13 ++- .../resource/OrganizationMembersResource.java | 11 +- .../idm/MemberRepresentation.java | 39 +++++++ .../representations/idm/MembershipType.java | 30 +++++ .../idm/OrganizationRepresentation.java | 10 +- .../idm/UserRepresentation.java | 37 ++++++ .../java/org/keycloak/OAuth2Constants.java | 2 + .../java/org/keycloak/common/Profile.java | 1 + .../common/crypto/CryptoConstants.java | 4 + .../keycloak/common/enums/SslRequired.java | 15 ++- .../keycloak/common/util/MultiSiteUtils.java | 34 ++++++ .../java/org/keycloak/crypto/Algorithm.java | 5 + .../AsymmetricSignatureVerifierContext.java | 14 ++- .../org/keycloak/crypto/JavaAlgorithm.java | 2 +- .../main/java/org/keycloak/jose/jwe/JWE.java | 7 +- .../org/keycloak/jose/jwe/JWEConstants.java | 4 + .../java/org/keycloak/jose/jwe/JWEHeader.java | 110 +++++++++++++++++- .../org/keycloak/jose/jwe/JWERegistry.java | 2 + .../jose/jwe/alg/DirectAlgorithmProvider.java | 6 +- .../jose/jwe/alg/JWEAlgorithmProvider.java | 6 +- .../org/keycloak/jose/jws/JWSBuilder.java | 51 +++++++- .../java/org/keycloak/jose/jws/JWSHeader.java | 8 ++ .../keycloak/jose/jws/crypto/HashUtils.java | 12 +- .../representations/RefreshToken.java | 20 +++- .../idm/MemberRepresentation.java | 39 +++++++ .../representations/idm/MembershipType.java | 30 +++++ .../idm/OrganizationRepresentation.java | 10 +- .../idm/UserRepresentation.java | 37 ++++++ 41 files changed, 699 insertions(+), 57 deletions(-) create mode 100644 admin-client-jee/src/main/java/org/keycloak/representations/idm/MemberRepresentation.java create mode 100644 admin-client-jee/src/main/java/org/keycloak/representations/idm/MembershipType.java create mode 100644 admin-client/src/main/java/org/keycloak/representations/idm/MemberRepresentation.java create mode 100644 admin-client/src/main/java/org/keycloak/representations/idm/MembershipType.java create mode 100644 authz-client/src/main/java/org/keycloak/common/util/MultiSiteUtils.java create mode 100644 authz-client/src/main/java/org/keycloak/representations/idm/MemberRepresentation.java create mode 100644 authz-client/src/main/java/org/keycloak/representations/idm/MembershipType.java diff --git a/.github/scripts/sync-keycloak-sources.sh b/.github/scripts/sync-keycloak-sources.sh index f87fbec..f524c65 100755 --- a/.github/scripts/sync-keycloak-sources.sh +++ b/.github/scripts/sync-keycloak-sources.sh @@ -21,6 +21,11 @@ function syncFiles() { MODULE=$1; echo_header "Syncing files in the module $MODULE"; cd $MODULE + + # Remove the existing files before sync + rm -rf src/main/java/* + rm -rf src/main/resources/* + mvn clean install -Psync mv target/unpacked/* src/main/java/ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9a95dbd..6e9985d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: timeout-minutes: 30 strategy: matrix: - keycloak_server_version: [ "25.0", "nightly" ] + keycloak_server_version: [ "24.0", "25.0", "nightly" ] steps: - uses: actions/checkout@v4 diff --git a/README.md b/README.md index 284c8aa..ce12435 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,24 @@ Keycloak-client java modules +The files in the modules: + +* [admin-client](admin-client) +* [admin-client-jee](admin-client-jee) +* [authz-client](authz-client) +* [policy-enforcer](policy-enforcer) + +are not "owned" by this repository and hence the Java files should ideally not be directly updated. Those files are "owned" by the [main Keycloak server repository](https://github.com/keycloak/keycloak) +and hence are supposed to be updated there (whenever needed) and synced into this repository by the bash script [sync-keycloak-sources.sh](.github/scripts/sync-keycloak-sources.sh) + +## Syncing the files from Keycloak repository + +* Checkout [main Keycloak server repository](https://github.com/keycloak/keycloak) and build it on your laptop to make sure latest Keycloak stuff available in your local maven repository. + +* Run [sync-keycloak-sources.sh](.github/scripts/sync-keycloak-sources.sh) + +* Send PR with the changes to update corresponding branch (usually `main`) of [Keycloak client repository](https://github.com/keycloak/keycloak-client) + ## Building the project ``` diff --git a/admin-client-jee/src/main/java/org/keycloak/OAuth2Constants.java b/admin-client-jee/src/main/java/org/keycloak/OAuth2Constants.java index 5d469e2..b03dd25 100755 --- a/admin-client-jee/src/main/java/org/keycloak/OAuth2Constants.java +++ b/admin-client-jee/src/main/java/org/keycloak/OAuth2Constants.java @@ -152,6 +152,8 @@ public interface OAuth2Constants { String ISSUER = "iss"; String AUTHENTICATOR_METHOD_REFERENCE = "amr"; + + String CNF = "cnf"; } diff --git a/admin-client-jee/src/main/java/org/keycloak/admin/client/JacksonProvider.java b/admin-client-jee/src/main/java/org/keycloak/admin/client/JacksonProvider.java index 58317b9..39785aa 100644 --- a/admin-client-jee/src/main/java/org/keycloak/admin/client/JacksonProvider.java +++ b/admin-client-jee/src/main/java/org/keycloak/admin/client/JacksonProvider.java @@ -1,6 +1,20 @@ package org.keycloak.admin.client; +import javax.ws.rs.core.MediaType; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; import org.jboss.resteasy.plugins.providers.jackson.ResteasyJackson2Provider; public class JacksonProvider extends ResteasyJackson2Provider { + + @Override + public ObjectMapper locateMapper(Class type, MediaType mediaType) { + ObjectMapper objectMapper = super.locateMapper(type, mediaType); + + // Same like JSONSerialization class. Makes it possible to use admin-client against older versions of Keycloak server where the properties on representations might be different + objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + + return objectMapper; + } } diff --git a/admin-client-jee/src/main/java/org/keycloak/admin/client/resource/OrganizationMemberResource.java b/admin-client-jee/src/main/java/org/keycloak/admin/client/resource/OrganizationMemberResource.java index 64e96a9..5c33120 100644 --- a/admin-client-jee/src/main/java/org/keycloak/admin/client/resource/OrganizationMemberResource.java +++ b/admin-client-jee/src/main/java/org/keycloak/admin/client/resource/OrganizationMemberResource.java @@ -17,19 +17,28 @@ package org.keycloak.admin.client.resource; +import java.util.List; + import javax.ws.rs.DELETE; import javax.ws.rs.GET; +import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; -import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.representations.idm.MemberRepresentation; +import org.keycloak.representations.idm.OrganizationRepresentation; public interface OrganizationMemberResource { @GET @Produces(MediaType.APPLICATION_JSON) - UserRepresentation toRepresentation(); + MemberRepresentation toRepresentation(); @DELETE Response delete(); + + @Path("organizations") + @GET + @Produces(MediaType.APPLICATION_JSON) + List getOrganizations(); } diff --git a/admin-client-jee/src/main/java/org/keycloak/admin/client/resource/OrganizationMembersResource.java b/admin-client-jee/src/main/java/org/keycloak/admin/client/resource/OrganizationMembersResource.java index fc63ba0..6930eb0 100644 --- a/admin-client-jee/src/main/java/org/keycloak/admin/client/resource/OrganizationMembersResource.java +++ b/admin-client-jee/src/main/java/org/keycloak/admin/client/resource/OrganizationMembersResource.java @@ -29,8 +29,8 @@ import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; +import org.keycloak.representations.idm.MemberRepresentation; import org.keycloak.representations.idm.OrganizationRepresentation; -import org.keycloak.representations.idm.UserRepresentation; public interface OrganizationMembersResource { @@ -45,7 +45,7 @@ public interface OrganizationMembersResource { */ @GET @Produces(MediaType.APPLICATION_JSON) - List getAll(); + List getAll(); /** * Return all organization members that match the specified filters. @@ -60,18 +60,13 @@ public interface OrganizationMembersResource { */ @GET @Produces(MediaType.APPLICATION_JSON) - List search( + List search( @QueryParam("search") String search, @QueryParam("exact") Boolean exact, @QueryParam("first") Integer first, @QueryParam("max") Integer max ); - @Path("{id}/organization") - @GET - @Produces(MediaType.APPLICATION_JSON) - OrganizationRepresentation getOrganization(@PathParam("id") String id); - @Path("{id}") OrganizationMemberResource member(@PathParam("id") String id); diff --git a/admin-client-jee/src/main/java/org/keycloak/representations/idm/MemberRepresentation.java b/admin-client-jee/src/main/java/org/keycloak/representations/idm/MemberRepresentation.java new file mode 100644 index 0000000..17b41e7 --- /dev/null +++ b/admin-client-jee/src/main/java/org/keycloak/representations/idm/MemberRepresentation.java @@ -0,0 +1,39 @@ +/* + * Copyright 2024 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.representations.idm; + +public class MemberRepresentation extends UserRepresentation { + + private MembershipType membershipType; + + public MemberRepresentation() { + super(); + } + + public MemberRepresentation(UserRepresentation user) { + super(user); + } + + public MembershipType getMembershipType() { + return membershipType; + } + + public void setMembershipType(MembershipType membershipType) { + this.membershipType = membershipType; + } +} diff --git a/admin-client-jee/src/main/java/org/keycloak/representations/idm/MembershipType.java b/admin-client-jee/src/main/java/org/keycloak/representations/idm/MembershipType.java new file mode 100644 index 0000000..5c89d4e --- /dev/null +++ b/admin-client-jee/src/main/java/org/keycloak/representations/idm/MembershipType.java @@ -0,0 +1,30 @@ +/* + * Copyright 2024 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.representations.idm; + +public enum MembershipType { + + /** + * Indicates that member can exist without group/organization. + */ + UNMANAGED, + + /** + * Indicates that member cannot exist without group/organization. + */ + MANAGED; +} diff --git a/admin-client-jee/src/main/java/org/keycloak/representations/idm/OrganizationRepresentation.java b/admin-client-jee/src/main/java/org/keycloak/representations/idm/OrganizationRepresentation.java index 5e2054a..5d0cf29 100644 --- a/admin-client-jee/src/main/java/org/keycloak/representations/idm/OrganizationRepresentation.java +++ b/admin-client-jee/src/main/java/org/keycloak/representations/idm/OrganizationRepresentation.java @@ -34,7 +34,7 @@ public class OrganizationRepresentation { private String description; private Map> attributes; private Set domains; - private List members; + private List members; private List identityProviders; public String getId() { @@ -119,19 +119,19 @@ public void removeDomain(OrganizationDomainRepresentation domain) { getDomains().remove(domain); } - public List getMembers() { + public List getMembers() { return members; } - public void setMembers(List members) { + public void setMembers(List members) { this.members = members; } - public void addMember(UserRepresentation user) { + public void addMember(MemberRepresentation member) { if (members == null) { members = new ArrayList<>(); } - members.add(user); + members.add(member); } public List getIdentityProviders() { diff --git a/admin-client-jee/src/main/java/org/keycloak/representations/idm/UserRepresentation.java b/admin-client-jee/src/main/java/org/keycloak/representations/idm/UserRepresentation.java index 8ab1110..a4e5ed0 100755 --- a/admin-client-jee/src/main/java/org/keycloak/representations/idm/UserRepresentation.java +++ b/admin-client-jee/src/main/java/org/keycloak/representations/idm/UserRepresentation.java @@ -52,6 +52,43 @@ public class UserRepresentation extends AbstractUserRepresentation{ protected List groups; private Map access; + public UserRepresentation() { + } + + public UserRepresentation(UserRepresentation rep) { + // AbstractUserRepresentation + this.id = rep.getId(); + this.username = rep.getUsername(); + this.firstName = rep.getFirstName(); + this.lastName = rep.getLastName(); + this.email = rep.getEmail(); + this.emailVerified = rep.isEmailVerified(); + this.attributes = rep.getAttributes(); + this.setUserProfileMetadata(rep.getUserProfileMetadata()); + + this.self = rep.getSelf(); + this.origin = rep.getOrigin(); + this.createdTimestamp = rep.getCreatedTimestamp(); + this.enabled = rep.isEnabled(); + this.totp = rep.isTotp(); + this.federationLink = rep.getFederationLink(); + this.serviceAccountClientId = rep.getServiceAccountClientId(); + this.credentials = rep.getCredentials(); + this.disableableCredentialTypes = rep.getDisableableCredentialTypes(); + this.requiredActions = rep.getRequiredActions(); + this.federatedIdentities = rep.getFederatedIdentities(); + this.realmRoles = rep.getRealmRoles(); + this.clientRoles = rep.getClientRoles(); + this.clientConsents = rep.getClientConsents(); + this.notBefore = rep.getNotBefore(); + + this.applicationRoles = rep.getApplicationRoles(); + this.socialLinks = rep.getSocialLinks(); + + this.groups = rep.getGroups(); + this.access = rep.getAccess(); + } + public String getSelf() { return self; } diff --git a/admin-client/src/main/java/org/keycloak/OAuth2Constants.java b/admin-client/src/main/java/org/keycloak/OAuth2Constants.java index 5d469e2..b03dd25 100755 --- a/admin-client/src/main/java/org/keycloak/OAuth2Constants.java +++ b/admin-client/src/main/java/org/keycloak/OAuth2Constants.java @@ -152,6 +152,8 @@ public interface OAuth2Constants { String ISSUER = "iss"; String AUTHENTICATOR_METHOD_REFERENCE = "amr"; + + String CNF = "cnf"; } diff --git a/admin-client/src/main/java/org/keycloak/admin/client/JacksonProvider.java b/admin-client/src/main/java/org/keycloak/admin/client/JacksonProvider.java index 58317b9..bccb178 100644 --- a/admin-client/src/main/java/org/keycloak/admin/client/JacksonProvider.java +++ b/admin-client/src/main/java/org/keycloak/admin/client/JacksonProvider.java @@ -1,6 +1,20 @@ package org.keycloak.admin.client; +import jakarta.ws.rs.core.MediaType; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; import org.jboss.resteasy.plugins.providers.jackson.ResteasyJackson2Provider; public class JacksonProvider extends ResteasyJackson2Provider { + + @Override + public ObjectMapper locateMapper(Class type, MediaType mediaType) { + ObjectMapper objectMapper = super.locateMapper(type, mediaType); + + // Same like JSONSerialization class. Makes it possible to use admin-client against older versions of Keycloak server where the properties on representations might be different + objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + + return objectMapper; + } } diff --git a/admin-client/src/main/java/org/keycloak/admin/client/resource/OrganizationMemberResource.java b/admin-client/src/main/java/org/keycloak/admin/client/resource/OrganizationMemberResource.java index f8d2d48..74c1ba6 100644 --- a/admin-client/src/main/java/org/keycloak/admin/client/resource/OrganizationMemberResource.java +++ b/admin-client/src/main/java/org/keycloak/admin/client/resource/OrganizationMemberResource.java @@ -17,19 +17,28 @@ package org.keycloak.admin.client.resource; +import java.util.List; + import jakarta.ws.rs.DELETE; import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; -import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.representations.idm.MemberRepresentation; +import org.keycloak.representations.idm.OrganizationRepresentation; public interface OrganizationMemberResource { @GET @Produces(MediaType.APPLICATION_JSON) - UserRepresentation toRepresentation(); + MemberRepresentation toRepresentation(); @DELETE Response delete(); + + @Path("organizations") + @GET + @Produces(MediaType.APPLICATION_JSON) + List getOrganizations(); } diff --git a/admin-client/src/main/java/org/keycloak/admin/client/resource/OrganizationMembersResource.java b/admin-client/src/main/java/org/keycloak/admin/client/resource/OrganizationMembersResource.java index 42b5d36..bdd3fc5 100644 --- a/admin-client/src/main/java/org/keycloak/admin/client/resource/OrganizationMembersResource.java +++ b/admin-client/src/main/java/org/keycloak/admin/client/resource/OrganizationMembersResource.java @@ -29,8 +29,8 @@ import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; +import org.keycloak.representations.idm.MemberRepresentation; import org.keycloak.representations.idm.OrganizationRepresentation; -import org.keycloak.representations.idm.UserRepresentation; public interface OrganizationMembersResource { @@ -45,7 +45,7 @@ public interface OrganizationMembersResource { */ @GET @Produces(MediaType.APPLICATION_JSON) - List getAll(); + List getAll(); /** * Return all organization members that match the specified filters. @@ -60,18 +60,13 @@ public interface OrganizationMembersResource { */ @GET @Produces(MediaType.APPLICATION_JSON) - List search( + List search( @QueryParam("search") String search, @QueryParam("exact") Boolean exact, @QueryParam("first") Integer first, @QueryParam("max") Integer max ); - @Path("{id}/organization") - @GET - @Produces(MediaType.APPLICATION_JSON) - OrganizationRepresentation getOrganization(@PathParam("id") String id); - @Path("{id}") OrganizationMemberResource member(@PathParam("id") String id); diff --git a/admin-client/src/main/java/org/keycloak/representations/idm/MemberRepresentation.java b/admin-client/src/main/java/org/keycloak/representations/idm/MemberRepresentation.java new file mode 100644 index 0000000..17b41e7 --- /dev/null +++ b/admin-client/src/main/java/org/keycloak/representations/idm/MemberRepresentation.java @@ -0,0 +1,39 @@ +/* + * Copyright 2024 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.representations.idm; + +public class MemberRepresentation extends UserRepresentation { + + private MembershipType membershipType; + + public MemberRepresentation() { + super(); + } + + public MemberRepresentation(UserRepresentation user) { + super(user); + } + + public MembershipType getMembershipType() { + return membershipType; + } + + public void setMembershipType(MembershipType membershipType) { + this.membershipType = membershipType; + } +} diff --git a/admin-client/src/main/java/org/keycloak/representations/idm/MembershipType.java b/admin-client/src/main/java/org/keycloak/representations/idm/MembershipType.java new file mode 100644 index 0000000..5c89d4e --- /dev/null +++ b/admin-client/src/main/java/org/keycloak/representations/idm/MembershipType.java @@ -0,0 +1,30 @@ +/* + * Copyright 2024 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.representations.idm; + +public enum MembershipType { + + /** + * Indicates that member can exist without group/organization. + */ + UNMANAGED, + + /** + * Indicates that member cannot exist without group/organization. + */ + MANAGED; +} diff --git a/admin-client/src/main/java/org/keycloak/representations/idm/OrganizationRepresentation.java b/admin-client/src/main/java/org/keycloak/representations/idm/OrganizationRepresentation.java index 5e2054a..5d0cf29 100644 --- a/admin-client/src/main/java/org/keycloak/representations/idm/OrganizationRepresentation.java +++ b/admin-client/src/main/java/org/keycloak/representations/idm/OrganizationRepresentation.java @@ -34,7 +34,7 @@ public class OrganizationRepresentation { private String description; private Map> attributes; private Set domains; - private List members; + private List members; private List identityProviders; public String getId() { @@ -119,19 +119,19 @@ public void removeDomain(OrganizationDomainRepresentation domain) { getDomains().remove(domain); } - public List getMembers() { + public List getMembers() { return members; } - public void setMembers(List members) { + public void setMembers(List members) { this.members = members; } - public void addMember(UserRepresentation user) { + public void addMember(MemberRepresentation member) { if (members == null) { members = new ArrayList<>(); } - members.add(user); + members.add(member); } public List getIdentityProviders() { diff --git a/admin-client/src/main/java/org/keycloak/representations/idm/UserRepresentation.java b/admin-client/src/main/java/org/keycloak/representations/idm/UserRepresentation.java index 8ab1110..a4e5ed0 100755 --- a/admin-client/src/main/java/org/keycloak/representations/idm/UserRepresentation.java +++ b/admin-client/src/main/java/org/keycloak/representations/idm/UserRepresentation.java @@ -52,6 +52,43 @@ public class UserRepresentation extends AbstractUserRepresentation{ protected List groups; private Map access; + public UserRepresentation() { + } + + public UserRepresentation(UserRepresentation rep) { + // AbstractUserRepresentation + this.id = rep.getId(); + this.username = rep.getUsername(); + this.firstName = rep.getFirstName(); + this.lastName = rep.getLastName(); + this.email = rep.getEmail(); + this.emailVerified = rep.isEmailVerified(); + this.attributes = rep.getAttributes(); + this.setUserProfileMetadata(rep.getUserProfileMetadata()); + + this.self = rep.getSelf(); + this.origin = rep.getOrigin(); + this.createdTimestamp = rep.getCreatedTimestamp(); + this.enabled = rep.isEnabled(); + this.totp = rep.isTotp(); + this.federationLink = rep.getFederationLink(); + this.serviceAccountClientId = rep.getServiceAccountClientId(); + this.credentials = rep.getCredentials(); + this.disableableCredentialTypes = rep.getDisableableCredentialTypes(); + this.requiredActions = rep.getRequiredActions(); + this.federatedIdentities = rep.getFederatedIdentities(); + this.realmRoles = rep.getRealmRoles(); + this.clientRoles = rep.getClientRoles(); + this.clientConsents = rep.getClientConsents(); + this.notBefore = rep.getNotBefore(); + + this.applicationRoles = rep.getApplicationRoles(); + this.socialLinks = rep.getSocialLinks(); + + this.groups = rep.getGroups(); + this.access = rep.getAccess(); + } + public String getSelf() { return self; } diff --git a/authz-client/src/main/java/org/keycloak/OAuth2Constants.java b/authz-client/src/main/java/org/keycloak/OAuth2Constants.java index 5d469e2..b03dd25 100755 --- a/authz-client/src/main/java/org/keycloak/OAuth2Constants.java +++ b/authz-client/src/main/java/org/keycloak/OAuth2Constants.java @@ -152,6 +152,8 @@ public interface OAuth2Constants { String ISSUER = "iss"; String AUTHENTICATOR_METHOD_REFERENCE = "amr"; + + String CNF = "cnf"; } diff --git a/authz-client/src/main/java/org/keycloak/common/Profile.java b/authz-client/src/main/java/org/keycloak/common/Profile.java index 73d6574..7d87f6e 100755 --- a/authz-client/src/main/java/org/keycloak/common/Profile.java +++ b/authz-client/src/main/java/org/keycloak/common/Profile.java @@ -103,6 +103,7 @@ public enum Feature { TRANSIENT_USERS("Transient users for brokering", Type.EXPERIMENTAL), MULTI_SITE("Multi-site support", Type.DISABLED_BY_DEFAULT), + REMOTE_CACHE("Remote caches support. Requires Multi-site support to be enabled as well.", Type.EXPERIMENTAL), CLIENT_TYPES("Client Types", Type.EXPERIMENTAL), diff --git a/authz-client/src/main/java/org/keycloak/common/crypto/CryptoConstants.java b/authz-client/src/main/java/org/keycloak/common/crypto/CryptoConstants.java index 90a06e2..1c53e61 100644 --- a/authz-client/src/main/java/org/keycloak/common/crypto/CryptoConstants.java +++ b/authz-client/src/main/java/org/keycloak/common/crypto/CryptoConstants.java @@ -10,6 +10,10 @@ public class CryptoConstants { public static final String RSA1_5 = "RSA1_5"; public static final String RSA_OAEP = "RSA-OAEP"; public static final String RSA_OAEP_256 = "RSA-OAEP-256"; + public static final String ECDH_ES = "ECDH-ES"; + public static final String ECDH_ES_A128KW = "ECDH-ES+A128KW"; + public static final String ECDH_ES_A192KW = "ECDH-ES+A192KW"; + public static final String ECDH_ES_A256KW = "ECDH-ES+A256KW"; // Constant for the OCSP provider // public static final String OCSP = "OCSP"; diff --git a/authz-client/src/main/java/org/keycloak/common/enums/SslRequired.java b/authz-client/src/main/java/org/keycloak/common/enums/SslRequired.java index 19d30cc..4c5519d 100644 --- a/authz-client/src/main/java/org/keycloak/common/enums/SslRequired.java +++ b/authz-client/src/main/java/org/keycloak/common/enums/SslRequired.java @@ -51,10 +51,23 @@ public boolean isRequired(String address) { private boolean isLocal(String remoteAddress) { try { InetAddress inetAddress = InetAddress.getByName(remoteAddress); - return inetAddress.isAnyLocalAddress() || inetAddress.isLoopbackAddress() || inetAddress.isSiteLocalAddress(); + return inetAddress.isAnyLocalAddress() || inetAddress.isLoopbackAddress() || inetAddress.isSiteLocalAddress() || inetAddress.isLinkLocalAddress() || isUniqueLocal(inetAddress); } catch (UnknownHostException e) { return false; } } + /** + * Check if the address is within IPv6 unique local address (ULA) range RFC4193. + */ + private boolean isUniqueLocal(InetAddress address) { + if (address instanceof java.net.Inet6Address) { + byte[] addr = address.getAddress(); + // Check if address is in unique local range fc00::/7 + return ((byte) (addr[0] & 0b11111110)) == (byte) 0xFC; + } + + return false; + } + } diff --git a/authz-client/src/main/java/org/keycloak/common/util/MultiSiteUtils.java b/authz-client/src/main/java/org/keycloak/common/util/MultiSiteUtils.java new file mode 100644 index 0000000..eca522b --- /dev/null +++ b/authz-client/src/main/java/org/keycloak/common/util/MultiSiteUtils.java @@ -0,0 +1,34 @@ +/* + * Copyright 2024 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.common.util; + +import org.keycloak.common.Profile; + +public class MultiSiteUtils { + + public static boolean isMultiSiteEnabled() { + return Profile.isFeatureEnabled(Profile.Feature.MULTI_SITE); + } + + /** + * @return true when user sessions are stored in the database. In multi-site setup this is false when REMOTE_CACHE feature is enabled + */ + public static boolean isPersistentSessionsEnabled() { + return Profile.isFeatureEnabled(Profile.Feature.PERSISTENT_USER_SESSIONS) || (isMultiSiteEnabled() && !Profile.isFeatureEnabled(Profile.Feature.REMOTE_CACHE)); + } +} diff --git a/authz-client/src/main/java/org/keycloak/crypto/Algorithm.java b/authz-client/src/main/java/org/keycloak/crypto/Algorithm.java index 7aa20a2..ab6efb6 100755 --- a/authz-client/src/main/java/org/keycloak/crypto/Algorithm.java +++ b/authz-client/src/main/java/org/keycloak/crypto/Algorithm.java @@ -49,4 +49,9 @@ public interface Algorithm { /* AES */ String AES = "AES"; + + String ECDH_ES = CryptoConstants.ECDH_ES; + String ECDH_ES_A128KW = CryptoConstants.ECDH_ES_A128KW; + String ECDH_ES_A192KW = CryptoConstants.ECDH_ES_A192KW; + String ECDH_ES_A256KW = CryptoConstants.ECDH_ES_A256KW; } diff --git a/authz-client/src/main/java/org/keycloak/crypto/AsymmetricSignatureVerifierContext.java b/authz-client/src/main/java/org/keycloak/crypto/AsymmetricSignatureVerifierContext.java index 76f1733..202d19d 100644 --- a/authz-client/src/main/java/org/keycloak/crypto/AsymmetricSignatureVerifierContext.java +++ b/authz-client/src/main/java/org/keycloak/crypto/AsymmetricSignatureVerifierContext.java @@ -17,7 +17,10 @@ package org.keycloak.crypto; import org.keycloak.common.VerificationException; +import org.keycloak.common.crypto.CryptoIntegration; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; import java.security.PublicKey; import java.security.Signature; @@ -42,7 +45,7 @@ public String getAlgorithm() { @Override public boolean verify(byte[] data, byte[] signature) throws VerificationException { try { - Signature verifier = Signature.getInstance(JavaAlgorithm.getJavaAlgorithm(key.getAlgorithmOrDefault(), key.getCurve())); + Signature verifier = getSignature(); verifier.initVerify((PublicKey) key.getPublicKey()); verifier.update(data); return verifier.verify(signature); @@ -51,4 +54,13 @@ public boolean verify(byte[] data, byte[] signature) throws VerificationExceptio } } + private Signature getSignature() + throws NoSuchAlgorithmException, NoSuchProviderException { + try { + return Signature.getInstance(JavaAlgorithm.getJavaAlgorithm(key.getAlgorithmOrDefault(), key.getCurve())); + } catch (NoSuchAlgorithmException e) { + // Retry using the current crypto provider's override implementation + return CryptoIntegration.getProvider().getSignature(key.getAlgorithmOrDefault()); + } + } } diff --git a/authz-client/src/main/java/org/keycloak/crypto/JavaAlgorithm.java b/authz-client/src/main/java/org/keycloak/crypto/JavaAlgorithm.java index 3ee487e..682ac61 100644 --- a/authz-client/src/main/java/org/keycloak/crypto/JavaAlgorithm.java +++ b/authz-client/src/main/java/org/keycloak/crypto/JavaAlgorithm.java @@ -37,7 +37,7 @@ public class JavaAlgorithm { public static final String SHA256 = "SHA-256"; public static final String SHA384 = "SHA-384"; public static final String SHA512 = "SHA-512"; - public static final String SHAKE256 = "SHAKE-256"; + public static final String SHAKE256 = "SHAKE256"; public static String getJavaAlgorithm(String algorithm) { return getJavaAlgorithm(algorithm, null); diff --git a/authz-client/src/main/java/org/keycloak/jose/jwe/JWE.java b/authz-client/src/main/java/org/keycloak/jose/jwe/JWE.java index e3f7eba..99d3d10 100644 --- a/authz-client/src/main/java/org/keycloak/jose/jwe/JWE.java +++ b/authz-client/src/main/java/org/keycloak/jose/jwe/JWE.java @@ -20,6 +20,7 @@ import org.keycloak.common.util.Base64Url; import org.keycloak.jose.JOSE; import org.keycloak.jose.JOSEHeader; +import org.keycloak.jose.jwe.JWEHeader.JWEHeaderBuilder; import org.keycloak.jose.jwe.alg.JWEAlgorithmProvider; import org.keycloak.jose.jwe.enc.JWEEncryptionProvider; import org.keycloak.util.JsonSerialization; @@ -143,8 +144,10 @@ public String encodeJwe(JWEAlgorithmProvider algorithmProvider, JWEEncryptionPro keyStorage.setEncryptionProvider(encryptionProvider); keyStorage.getCEKKey(JWEKeyStorage.KeyUse.ENCRYPTION, true); // Will generate CEK if it's not already present - byte[] encodedCEK = algorithmProvider.encodeCek(encryptionProvider, keyStorage, keyStorage.getEncryptionKey()); + JWEHeaderBuilder headerBuilder = header.toBuilder(); + byte[] encodedCEK = algorithmProvider.encodeCek(encryptionProvider, keyStorage, keyStorage.getEncryptionKey(), headerBuilder); base64Cek = Base64Url.encode(encodedCEK); + header = headerBuilder.build(); encryptionProvider.encodeJwe(this); @@ -191,7 +194,7 @@ private JWE getProcessedJWE(JWEAlgorithmProvider algorithmProvider, JWEEncryptio keyStorage.setEncryptionProvider(encryptionProvider); - byte[] decodedCek = algorithmProvider.decodeCek(Base64Url.decode(base64Cek), keyStorage.getDecryptionKey()); + byte[] decodedCek = algorithmProvider.decodeCek(Base64Url.decode(base64Cek), keyStorage.getDecryptionKey(), this.header, encryptionProvider); keyStorage.setCEKBytes(decodedCek); encryptionProvider.verifyAndDecodeJwe(this); diff --git a/authz-client/src/main/java/org/keycloak/jose/jwe/JWEConstants.java b/authz-client/src/main/java/org/keycloak/jose/jwe/JWEConstants.java index ad33e73..89406e4 100644 --- a/authz-client/src/main/java/org/keycloak/jose/jwe/JWEConstants.java +++ b/authz-client/src/main/java/org/keycloak/jose/jwe/JWEConstants.java @@ -29,6 +29,10 @@ public class JWEConstants { public static final String RSA1_5 = CryptoConstants.RSA1_5; public static final String RSA_OAEP = CryptoConstants.RSA_OAEP; public static final String RSA_OAEP_256 = CryptoConstants.RSA_OAEP_256; + public static final String ECDH_ES = CryptoConstants.ECDH_ES; + public static final String ECDH_ES_A128KW = CryptoConstants.ECDH_ES_A128KW; + public static final String ECDH_ES_A192KW = CryptoConstants.ECDH_ES_A192KW; + public static final String ECDH_ES_A256KW = CryptoConstants.ECDH_ES_A256KW; public static final String A128CBC_HS256 = "A128CBC-HS256"; public static final String A192CBC_HS384 = "A192CBC-HS384"; diff --git a/authz-client/src/main/java/org/keycloak/jose/jwe/JWEHeader.java b/authz-client/src/main/java/org/keycloak/jose/jwe/JWEHeader.java index 55cb782..c3b8401 100644 --- a/authz-client/src/main/java/org/keycloak/jose/jwe/JWEHeader.java +++ b/authz-client/src/main/java/org/keycloak/jose/jwe/JWEHeader.java @@ -18,6 +18,7 @@ package org.keycloak.jose.jwe; import java.io.IOException; +import java.io.UncheckedIOException; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; @@ -25,6 +26,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.ObjectMapper; import org.keycloak.jose.JOSEHeader; +import org.keycloak.jose.jwk.ECPublicJWK; /** * @author Marek Posolda @@ -41,7 +43,6 @@ public class JWEHeader implements JOSEHeader { @JsonProperty("zip") private String compressionAlgorithm; - @JsonProperty("typ") private String type; @@ -51,6 +52,15 @@ public class JWEHeader implements JOSEHeader { @JsonProperty("kid") private String keyId; + @JsonProperty("epk") + private ECPublicJWK ephemeralPublicKey; + + @JsonProperty("apu") + private String agreementPartyUInfo; + + @JsonProperty("apv") + private String agreementPartyVInfo; + public JWEHeader() { } @@ -75,6 +85,19 @@ public JWEHeader(String algorithm, String encryptionAlgorithm, String compressio this.contentType = contentType; } + public JWEHeader(String algorithm, String encryptionAlgorithm, String compressionAlgorithm, String keyId, String contentType, + String type, ECPublicJWK ephemeralPublicKey, String agreementPartyUInfo, String agreementPartyVInfo) { + this.algorithm = algorithm; + this.encryptionAlgorithm = encryptionAlgorithm; + this.compressionAlgorithm = compressionAlgorithm; + this.keyId = keyId; + this.type = type; + this.contentType = contentType; + this.ephemeralPublicKey = ephemeralPublicKey; + this.agreementPartyUInfo = agreementPartyUInfo; + this.agreementPartyVInfo = agreementPartyVInfo; + } + public String getAlgorithm() { return algorithm; } @@ -105,21 +128,102 @@ public String getKeyId() { return keyId; } + public ECPublicJWK getEphemeralPublicKey() { + return ephemeralPublicKey; + } + + public String getAgreementPartyUInfo() { + return agreementPartyUInfo; + } + + public String getAgreementPartyVInfo() { + return agreementPartyVInfo; + } + private static final ObjectMapper mapper = new ObjectMapper(); static { mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); - } public String toString() { try { return mapper.writeValueAsString(this); } catch (IOException e) { - throw new RuntimeException(e); + throw new UncheckedIOException(e); } + } + public JWEHeaderBuilder toBuilder() { + return builder().algorithm(algorithm).encryptionAlgorithm(encryptionAlgorithm) + .compressionAlgorithm(compressionAlgorithm).type(type).contentType(contentType) + .keyId(keyId).ephemeralPublicKey(ephemeralPublicKey).agreementPartyUInfo(agreementPartyUInfo) + .agreementPartyVInfo(agreementPartyVInfo); + } + public static JWEHeaderBuilder builder() { + return new JWEHeaderBuilder(); } + public static class JWEHeaderBuilder { + private String algorithm = null; + private String encryptionAlgorithm = null; + private String compressionAlgorithm = null; + private String type = null; + private String contentType = null; + private String keyId = null; + private ECPublicJWK ephemeralPublicKey = null; + private String agreementPartyUInfo = null; + private String agreementPartyVInfo = null; + + public JWEHeaderBuilder algorithm(String algorithm) { + this.algorithm = algorithm; + return this; + } + + public JWEHeaderBuilder encryptionAlgorithm(String encryptionAlgorithm) { + this.encryptionAlgorithm = encryptionAlgorithm; + return this; + } + + public JWEHeaderBuilder compressionAlgorithm(String compressionAlgorithm) { + this.compressionAlgorithm = compressionAlgorithm; + return this; + } + + public JWEHeaderBuilder type(String type) { + this.type = type; + return this; + } + + public JWEHeaderBuilder contentType(String contentType) { + this.contentType = contentType; + return this; + } + + public JWEHeaderBuilder keyId(String keyId) { + this.keyId = keyId; + return this; + } + + public JWEHeaderBuilder ephemeralPublicKey(ECPublicJWK ephemeralPublicKey) { + this.ephemeralPublicKey = ephemeralPublicKey; + return this; + } + + public JWEHeaderBuilder agreementPartyUInfo(String agreementPartyUInfo) { + this.agreementPartyUInfo = agreementPartyUInfo; + return this; + } + + public JWEHeaderBuilder agreementPartyVInfo(String agreementPartyVInfo) { + this.agreementPartyVInfo = agreementPartyVInfo; + return this; + } + + public JWEHeader build() { + return new JWEHeader(algorithm, encryptionAlgorithm, compressionAlgorithm, keyId, contentType, + type, ephemeralPublicKey, agreementPartyUInfo, agreementPartyVInfo); + } + } } diff --git a/authz-client/src/main/java/org/keycloak/jose/jwe/JWERegistry.java b/authz-client/src/main/java/org/keycloak/jose/jwe/JWERegistry.java index dc9aa1f..2785f33 100644 --- a/authz-client/src/main/java/org/keycloak/jose/jwe/JWERegistry.java +++ b/authz-client/src/main/java/org/keycloak/jose/jwe/JWERegistry.java @@ -38,6 +38,8 @@ class JWERegistry { private static final Map ENC_PROVIDERS = new HashMap<>(); static { + ENC_PROVIDERS.put(JWEConstants.A128GCM, new AesGcmJWEEncryptionProvider(JWEConstants.A128GCM)); + ENC_PROVIDERS.put(JWEConstants.A192GCM, new AesGcmJWEEncryptionProvider(JWEConstants.A192GCM)); ENC_PROVIDERS.put(JWEConstants.A256GCM, new AesGcmJWEEncryptionProvider(JWEConstants.A256GCM)); ENC_PROVIDERS.put(JWEConstants.A128CBC_HS256, new AesCbcHmacShaEncryptionProvider.Aes128CbcHmacSha256Provider()); ENC_PROVIDERS.put(JWEConstants.A192CBC_HS384, new AesCbcHmacShaEncryptionProvider.Aes192CbcHmacSha384Provider()); diff --git a/authz-client/src/main/java/org/keycloak/jose/jwe/alg/DirectAlgorithmProvider.java b/authz-client/src/main/java/org/keycloak/jose/jwe/alg/DirectAlgorithmProvider.java index ef3ca02..ed949bb 100644 --- a/authz-client/src/main/java/org/keycloak/jose/jwe/alg/DirectAlgorithmProvider.java +++ b/authz-client/src/main/java/org/keycloak/jose/jwe/alg/DirectAlgorithmProvider.java @@ -19,6 +19,8 @@ import java.security.Key; +import org.keycloak.jose.jwe.JWEHeader; +import org.keycloak.jose.jwe.JWEHeader.JWEHeaderBuilder; import org.keycloak.jose.jwe.JWEKeyStorage; import org.keycloak.jose.jwe.enc.JWEEncryptionProvider; @@ -28,12 +30,12 @@ public class DirectAlgorithmProvider implements JWEAlgorithmProvider { @Override - public byte[] decodeCek(byte[] encodedCek, Key encryptionKey) { + public byte[] decodeCek(byte[] encodedCek, Key encryptionKey, JWEHeader header, JWEEncryptionProvider encryptionProvider) { return new byte[0]; } @Override - public byte[] encodeCek(JWEEncryptionProvider encryptionProvider, JWEKeyStorage keyStorage, Key encryptionKey) { + public byte[] encodeCek(JWEEncryptionProvider encryptionProvider, JWEKeyStorage keyStorage, Key encryptionKey, JWEHeaderBuilder headerBuilder) { return new byte[0]; } } diff --git a/authz-client/src/main/java/org/keycloak/jose/jwe/alg/JWEAlgorithmProvider.java b/authz-client/src/main/java/org/keycloak/jose/jwe/alg/JWEAlgorithmProvider.java index 80347f8..4ad5653 100644 --- a/authz-client/src/main/java/org/keycloak/jose/jwe/alg/JWEAlgorithmProvider.java +++ b/authz-client/src/main/java/org/keycloak/jose/jwe/alg/JWEAlgorithmProvider.java @@ -19,7 +19,9 @@ import java.security.Key; +import org.keycloak.jose.jwe.JWEHeader; import org.keycloak.jose.jwe.JWEKeyStorage; +import org.keycloak.jose.jwe.JWEHeader.JWEHeaderBuilder; import org.keycloak.jose.jwe.enc.JWEEncryptionProvider; /** @@ -27,8 +29,8 @@ */ public interface JWEAlgorithmProvider { - byte[] decodeCek(byte[] encodedCek, Key encryptionKey) throws Exception; + byte[] decodeCek(byte[] encodedCek, Key encryptionKey, JWEHeader header, JWEEncryptionProvider encryptionProvider) throws Exception; - byte[] encodeCek(JWEEncryptionProvider encryptionProvider, JWEKeyStorage keyStorage, Key encryptionKey) throws Exception; + byte[] encodeCek(JWEEncryptionProvider encryptionProvider, JWEKeyStorage keyStorage, Key encryptionKey, JWEHeaderBuilder headerBuilder) throws Exception; } diff --git a/authz-client/src/main/java/org/keycloak/jose/jws/JWSBuilder.java b/authz-client/src/main/java/org/keycloak/jose/jws/JWSBuilder.java index c96fc81..bf13bc7 100755 --- a/authz-client/src/main/java/org/keycloak/jose/jws/JWSBuilder.java +++ b/authz-client/src/main/java/org/keycloak/jose/jws/JWSBuilder.java @@ -17,9 +17,11 @@ package org.keycloak.jose.jws; +import com.fasterxml.jackson.core.JsonProcessingException; +import org.keycloak.common.util.Base64; import org.keycloak.common.util.Base64Url; -import org.keycloak.crypto.JavaAlgorithm; import org.keycloak.crypto.SignatureSignerContext; +import org.keycloak.jose.jwk.JWK; import org.keycloak.jose.jws.crypto.HMACProvider; import org.keycloak.jose.jws.crypto.RSAProvider; import org.keycloak.util.JsonSerialization; @@ -28,17 +30,22 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.security.PrivateKey; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.util.List; /** * @author Bill Burke * @version $Revision: 1 $ */ public class JWSBuilder { - String type; - String kid; - String x5t; - String contentType; - byte[] contentBytes; + protected String type; + protected String kid; + protected String x5t; + protected JWK jwk; + protected List x5c; + protected String contentType; + protected byte[] contentBytes; public JWSBuilder type(String type) { this.type = type; @@ -55,6 +62,16 @@ public JWSBuilder x5t(String x5t) { return this; } + public JWSBuilder jwk(JWK jwk) { + this.jwk = jwk; + return this; + } + + public JWSBuilder x5c(List x5c) { + this.x5c = x5c; + return this; + } + public JWSBuilder contentType(String type) { this.contentType = type; return this; @@ -88,6 +105,28 @@ protected String encodeHeader(String sigAlgName) { if (type != null) builder.append(",\"typ\" : \"").append(type).append("\""); if (kid != null) builder.append(",\"kid\" : \"").append(kid).append("\""); if (x5t != null) builder.append(",\"x5t\" : \"").append(x5t).append("\""); + if (x5c != null && !x5c.isEmpty()) { + builder.append(",\"x5c\" : ["); + for (int i = 0; i < x5c.size(); i++) { + X509Certificate certificate = x5c.get(i); + if (i > 0) { + builder.append(","); + } + try { + builder.append("\"").append(Base64.encodeBytes(certificate.getEncoded())).append("\""); + } catch (CertificateEncodingException e) { + throw new RuntimeException(e); + } + } + builder.append("]"); + } + if (jwk != null) { + try { + builder.append(",\"jwk\" : ").append(JsonSerialization.mapper.writeValueAsString(jwk)); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } if (contentType != null) builder.append(",\"cty\":\"").append(contentType).append("\""); builder.append("}"); return Base64Url.encode(builder.toString().getBytes(StandardCharsets.UTF_8)); diff --git a/authz-client/src/main/java/org/keycloak/jose/jws/JWSHeader.java b/authz-client/src/main/java/org/keycloak/jose/jws/JWSHeader.java index 99045b1..16a1aa2 100755 --- a/authz-client/src/main/java/org/keycloak/jose/jws/JWSHeader.java +++ b/authz-client/src/main/java/org/keycloak/jose/jws/JWSHeader.java @@ -27,6 +27,7 @@ import org.keycloak.jose.jwk.JWK; import java.io.IOException; +import java.util.List; /** * @author Bill Burke @@ -49,6 +50,9 @@ public class JWSHeader implements JOSEHeader { @JsonProperty("jwk") private JWK key; + @JsonProperty("x5c") + private List x5c; + public JWSHeader() { } @@ -91,6 +95,10 @@ public JWK getKey() { return key; } + public List getX5c() { + return x5c; + } + private static final ObjectMapper mapper = new ObjectMapper(); static { diff --git a/authz-client/src/main/java/org/keycloak/jose/jws/crypto/HashUtils.java b/authz-client/src/main/java/org/keycloak/jose/jws/crypto/HashUtils.java index 47084ae..4de91f5 100644 --- a/authz-client/src/main/java/org/keycloak/jose/jws/crypto/HashUtils.java +++ b/authz-client/src/main/java/org/keycloak/jose/jws/crypto/HashUtils.java @@ -34,15 +34,23 @@ public class HashUtils { // - "at_hash" and "c_hash" in OIDC specification (full = false) // - "ath" in DPoP specification (full = true) public static String accessTokenHash(String jwtAlgorithmName, String input, boolean full) { + return accessTokenHash(jwtAlgorithmName, null, input, full); + } + + public static String accessTokenHash(String jwtAlgorithmName, String curve, String input, boolean full) { byte[] inputBytes = input.getBytes(StandardCharsets.UTF_8); - String javaAlgName = JavaAlgorithm.getJavaAlgorithmForHash(jwtAlgorithmName); + String javaAlgName = JavaAlgorithm.getJavaAlgorithmForHash(jwtAlgorithmName, curve); byte[] hash = hash(javaAlgName, inputBytes); return encodeHashToOIDC(hash, full); } public static String accessTokenHash(String jwtAlgorithmName, String input) { - return HashUtils.accessTokenHash(jwtAlgorithmName, input, false); + return HashUtils.accessTokenHash(jwtAlgorithmName, null, input, false); + } + + public static String accessTokenHash(String jwtAlgorithmName, String curve, String input) { + return HashUtils.accessTokenHash(jwtAlgorithmName, curve, input, false); } public static byte[] hash(String javaAlgorithmName, byte[] inputBytes) { diff --git a/authz-client/src/main/java/org/keycloak/representations/RefreshToken.java b/authz-client/src/main/java/org/keycloak/representations/RefreshToken.java index 2f6b5a1..d449450 100755 --- a/authz-client/src/main/java/org/keycloak/representations/RefreshToken.java +++ b/authz-client/src/main/java/org/keycloak/representations/RefreshToken.java @@ -36,7 +36,6 @@ private RefreshToken() { /** * Deep copies issuer, subject, issuedFor, sessionState from AccessToken. * - * @param token */ public RefreshToken(AccessToken token) { this(); @@ -49,6 +48,25 @@ public RefreshToken(AccessToken token) { this.scope = token.scope; } + /** + * Deep copies issuer, subject, issuedFor, sessionState from AccessToken. + * + * @param token + * @param confirmation optional confirmation parameter that might be processed during authentication but should not + * always be included in the response + */ + public RefreshToken(AccessToken token, Confirmation confirmation) { + this(); + this.issuer = token.issuer; + this.subject = token.subject; + this.issuedFor = token.issuedFor; + this.sessionId = token.sessionId; + this.nonce = token.nonce; + this.audience = new String[] { token.issuer }; + this.scope = token.scope; + this.confirmation = confirmation; + } + @Override public TokenCategory getCategory() { return TokenCategory.INTERNAL; diff --git a/authz-client/src/main/java/org/keycloak/representations/idm/MemberRepresentation.java b/authz-client/src/main/java/org/keycloak/representations/idm/MemberRepresentation.java new file mode 100644 index 0000000..17b41e7 --- /dev/null +++ b/authz-client/src/main/java/org/keycloak/representations/idm/MemberRepresentation.java @@ -0,0 +1,39 @@ +/* + * Copyright 2024 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.representations.idm; + +public class MemberRepresentation extends UserRepresentation { + + private MembershipType membershipType; + + public MemberRepresentation() { + super(); + } + + public MemberRepresentation(UserRepresentation user) { + super(user); + } + + public MembershipType getMembershipType() { + return membershipType; + } + + public void setMembershipType(MembershipType membershipType) { + this.membershipType = membershipType; + } +} diff --git a/authz-client/src/main/java/org/keycloak/representations/idm/MembershipType.java b/authz-client/src/main/java/org/keycloak/representations/idm/MembershipType.java new file mode 100644 index 0000000..5c89d4e --- /dev/null +++ b/authz-client/src/main/java/org/keycloak/representations/idm/MembershipType.java @@ -0,0 +1,30 @@ +/* + * Copyright 2024 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.representations.idm; + +public enum MembershipType { + + /** + * Indicates that member can exist without group/organization. + */ + UNMANAGED, + + /** + * Indicates that member cannot exist without group/organization. + */ + MANAGED; +} diff --git a/authz-client/src/main/java/org/keycloak/representations/idm/OrganizationRepresentation.java b/authz-client/src/main/java/org/keycloak/representations/idm/OrganizationRepresentation.java index 5e2054a..5d0cf29 100644 --- a/authz-client/src/main/java/org/keycloak/representations/idm/OrganizationRepresentation.java +++ b/authz-client/src/main/java/org/keycloak/representations/idm/OrganizationRepresentation.java @@ -34,7 +34,7 @@ public class OrganizationRepresentation { private String description; private Map> attributes; private Set domains; - private List members; + private List members; private List identityProviders; public String getId() { @@ -119,19 +119,19 @@ public void removeDomain(OrganizationDomainRepresentation domain) { getDomains().remove(domain); } - public List getMembers() { + public List getMembers() { return members; } - public void setMembers(List members) { + public void setMembers(List members) { this.members = members; } - public void addMember(UserRepresentation user) { + public void addMember(MemberRepresentation member) { if (members == null) { members = new ArrayList<>(); } - members.add(user); + members.add(member); } public List getIdentityProviders() { diff --git a/authz-client/src/main/java/org/keycloak/representations/idm/UserRepresentation.java b/authz-client/src/main/java/org/keycloak/representations/idm/UserRepresentation.java index 8ab1110..a4e5ed0 100755 --- a/authz-client/src/main/java/org/keycloak/representations/idm/UserRepresentation.java +++ b/authz-client/src/main/java/org/keycloak/representations/idm/UserRepresentation.java @@ -52,6 +52,43 @@ public class UserRepresentation extends AbstractUserRepresentation{ protected List groups; private Map access; + public UserRepresentation() { + } + + public UserRepresentation(UserRepresentation rep) { + // AbstractUserRepresentation + this.id = rep.getId(); + this.username = rep.getUsername(); + this.firstName = rep.getFirstName(); + this.lastName = rep.getLastName(); + this.email = rep.getEmail(); + this.emailVerified = rep.isEmailVerified(); + this.attributes = rep.getAttributes(); + this.setUserProfileMetadata(rep.getUserProfileMetadata()); + + this.self = rep.getSelf(); + this.origin = rep.getOrigin(); + this.createdTimestamp = rep.getCreatedTimestamp(); + this.enabled = rep.isEnabled(); + this.totp = rep.isTotp(); + this.federationLink = rep.getFederationLink(); + this.serviceAccountClientId = rep.getServiceAccountClientId(); + this.credentials = rep.getCredentials(); + this.disableableCredentialTypes = rep.getDisableableCredentialTypes(); + this.requiredActions = rep.getRequiredActions(); + this.federatedIdentities = rep.getFederatedIdentities(); + this.realmRoles = rep.getRealmRoles(); + this.clientRoles = rep.getClientRoles(); + this.clientConsents = rep.getClientConsents(); + this.notBefore = rep.getNotBefore(); + + this.applicationRoles = rep.getApplicationRoles(); + this.socialLinks = rep.getSocialLinks(); + + this.groups = rep.getGroups(); + this.access = rep.getAccess(); + } + public String getSelf() { return self; }