From c7fd6882ff35d91034a3a410dbda547c781be3bc Mon Sep 17 00:00:00 2001 From: strehle Date: Tue, 22 Aug 2023 18:18:40 +0200 Subject: [PATCH] feature: add persistence support for private_key_jwt Allow to setup jwks_uri and jwks, similar to OIDC proxy mode with tokenKeyUrl and tokenKey. The private_key_jwt metadata is stored in additional_information (could be switched to own column) The setup can be done from REST and yaml. --- .../uaa/oauth/client/ClientConstants.java | 1 + .../oauth/client/ClientDetailsCreation.java | 22 +++ .../oauth/client/PrivateKeyChangeRequest.java | 93 +++++++++ .../uaa/client/ClientAdminBootstrap.java | 21 ++- .../uaa/client/ClientAdminEndpoints.java | 50 +++++ .../client/ClientAdminEndpointsValidator.java | 19 +- .../client/PrivateKeyJwtConfiguration.java | 177 ++++++++++++++++++ .../uaa/zone/MultitenantClientServices.java | 6 + .../MultitenantJdbcClientDetailsService.java | 44 +++++ .../InMemoryMultitenantClientServices.java | 15 ++ 10 files changed, 445 insertions(+), 3 deletions(-) create mode 100644 model/src/main/java/org/cloudfoundry/identity/uaa/oauth/client/PrivateKeyChangeRequest.java create mode 100644 server/src/main/java/org/cloudfoundry/identity/uaa/client/PrivateKeyJwtConfiguration.java diff --git a/model/src/main/java/org/cloudfoundry/identity/uaa/oauth/client/ClientConstants.java b/model/src/main/java/org/cloudfoundry/identity/uaa/oauth/client/ClientConstants.java index 79d333b8da5..6cc4dde5cc9 100644 --- a/model/src/main/java/org/cloudfoundry/identity/uaa/oauth/client/ClientConstants.java +++ b/model/src/main/java/org/cloudfoundry/identity/uaa/oauth/client/ClientConstants.java @@ -21,5 +21,6 @@ public class ClientConstants { public static final String APPROVALS_DELETED = "approvals_deleted"; public static final String TOKEN_SALT = "token_salt"; public static final String REQUIRED_USER_GROUPS = "required_user_groups"; + public static final String PRIVATE_KEY_CONFIG = "private_key_config"; public static final String LAST_MODIFIED = "lastModified"; } diff --git a/model/src/main/java/org/cloudfoundry/identity/uaa/oauth/client/ClientDetailsCreation.java b/model/src/main/java/org/cloudfoundry/identity/uaa/oauth/client/ClientDetailsCreation.java index 5e0d6d249b2..a44697e6a5a 100644 --- a/model/src/main/java/org/cloudfoundry/identity/uaa/oauth/client/ClientDetailsCreation.java +++ b/model/src/main/java/org/cloudfoundry/identity/uaa/oauth/client/ClientDetailsCreation.java @@ -13,6 +13,12 @@ public class ClientDetailsCreation extends BaseClientDetails { @JsonProperty("secondary_client_secret") private String secondaryClientSecret; + @JsonProperty("private_key_url") + private String privateKeyUrl; + + @JsonProperty("private_key_set") + private String privateKeySet; + @JsonIgnore public String getSecondaryClientSecret() { return secondaryClientSecret; @@ -21,4 +27,20 @@ public String getSecondaryClientSecret() { public void setSecondaryClientSecret(final String secondaryClientSecret) { this.secondaryClientSecret = secondaryClientSecret; } + + public String getPrivateKeyUrl() { + return privateKeyUrl; + } + + public void setPrivateKeyUrl(String privateKeyUrl) { + this.privateKeyUrl = privateKeyUrl; + } + + public String getPrivateKeySet() { + return privateKeySet; + } + + public void setPrivateKeySet(String privateKeySet) { + this.privateKeySet = privateKeySet; + } } diff --git a/model/src/main/java/org/cloudfoundry/identity/uaa/oauth/client/PrivateKeyChangeRequest.java b/model/src/main/java/org/cloudfoundry/identity/uaa/oauth/client/PrivateKeyChangeRequest.java new file mode 100644 index 00000000000..86103b8fdaf --- /dev/null +++ b/model/src/main/java/org/cloudfoundry/identity/uaa/oauth/client/PrivateKeyChangeRequest.java @@ -0,0 +1,93 @@ +/******************************************************************************* + * Cloud Foundry + * Copyright (c) [2009-2016] Pivotal Software, Inc. All Rights Reserved. + * + * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * You may not use this product except in compliance with the License. + * + * This product includes a number of subcomponents with + * separate copyright notices and license terms. Your use of these + * subcomponents is subject to the terms and conditions of the + * subcomponent's license, as noted in the LICENSE file. + *******************************************************************************/ +package org.cloudfoundry.identity.uaa.oauth.client; + + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import static org.cloudfoundry.identity.uaa.oauth.client.PrivateKeyChangeRequest.ChangeMode.ADD; + +/** + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class PrivateKeyChangeRequest { + + public enum ChangeMode { + UPDATE, + ADD, + DELETE + } + @JsonProperty("kid") + private String keyId; + @JsonProperty("jwks_uri") + private String keyUrl; + @JsonProperty("jwks") + private String keyConfig; + @JsonProperty("client_id") + private String clientId; + private ChangeMode changeMode = ADD; + + public PrivateKeyChangeRequest() { + } + + public PrivateKeyChangeRequest(String clientId, String keyUrl, String keyConfig) { + this.keyUrl = keyUrl; + this.keyConfig = keyConfig; + this.clientId = clientId; + } + + public String getKeyUrl() { + return keyUrl; + } + + public void setKeyUrl(String keyUrl) { + this.keyUrl = keyUrl; + } + + public String getKeyConfig() { + return keyConfig; + } + + public void setKeyConfig(String keyConfig) { + this.keyConfig = keyConfig; + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public ChangeMode getChangeMode() { + return changeMode; + } + + public void setChangeMode(ChangeMode changeMode) { + this.changeMode = changeMode; + } + + public String getKeyId() { return keyId;} + + public void setKeyId(String keyId) { + this.keyId = keyId; + } + + public String getKey() { + return keyUrl != null ? keyUrl : keyConfig; + } +} diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientAdminBootstrap.java b/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientAdminBootstrap.java index e1bbc625833..98a66fe2151 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientAdminBootstrap.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientAdminBootstrap.java @@ -6,6 +6,7 @@ import static org.cloudfoundry.identity.uaa.oauth.token.TokenConstants.GRANT_TYPE_REFRESH_TOKEN; import java.net.MalformedURLException; +import java.net.URI; import java.net.URL; import java.util.Arrays; import java.util.Collection; @@ -19,6 +20,7 @@ import org.cloudfoundry.identity.uaa.authentication.SystemAuthentication; import org.cloudfoundry.identity.uaa.oauth.client.ClientConstants; import org.cloudfoundry.identity.uaa.user.UaaAuthority; +import org.cloudfoundry.identity.uaa.util.UaaUrlUtils; import org.cloudfoundry.identity.uaa.zone.IdentityZone; import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; import org.cloudfoundry.identity.uaa.zone.MultitenantClientServices; @@ -204,7 +206,7 @@ private void addNewClients() { } for (String key : Arrays.asList("resource-ids", "scope", "authorized-grant-types", "authorities", "redirect-uri", "secret", "id", "override", "access-token-validity", - "refresh-token-validity", "show-on-homepage", "app-launch-url", "app-icon")) { + "refresh-token-validity", "show-on-homepage", "app-launch-url", "app-icon", "jwks", "jwks_uri")) { info.remove(key); } @@ -235,6 +237,23 @@ private void addNewClients() { } } + if (map.get("jwks_uri") instanceof String) { + String jwksUri = (String) map.get("jwks_uri"); + URI jwksUriObject = URI.create(jwksUri); + if (jwksUriObject.isAbsolute() && ("https".startsWith(jwksUriObject.getScheme()) || ("http".startsWith(jwksUriObject.getScheme()) && jwksUriObject.getHost().contains("localhost")))) { + PrivateKeyJwtConfiguration keyConfig = PrivateKeyJwtConfiguration.parse(UaaUrlUtils.normalizeUri(jwksUri), null); + if (keyConfig != null && keyConfig.getCleanString() != null) { + clientRegistrationService.addClientKeyConfig(clientId, keyConfig.getPrivateKeyJwtUrl(), IdentityZone.getUaaZoneId()); + } + } + } else if (map.get("jwks") instanceof String) { + String jwks = (String) map.get("jwks"); + PrivateKeyJwtConfiguration keyConfig = PrivateKeyJwtConfiguration.parse(null, jwks); + if (keyConfig != null && keyConfig.getCleanString() != null) { + clientRegistrationService.addClientKeyConfig(clientId, keyConfig.getCleanString(), IdentityZone.getUaaZoneId()); + } + } + ClientMetadata clientMetadata = buildClientMetadata(map, clientId); clientMetadataProvisioning.update(clientMetadata, IdentityZone.getUaaZoneId()); } diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientAdminEndpoints.java b/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientAdminEndpoints.java index cf98e995a61..ac51799945d 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientAdminEndpoints.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientAdminEndpoints.java @@ -20,6 +20,7 @@ import org.cloudfoundry.identity.uaa.oauth.client.ClientConstants; import org.cloudfoundry.identity.uaa.oauth.client.ClientDetailsCreation; import org.cloudfoundry.identity.uaa.oauth.client.ClientDetailsModification; +import org.cloudfoundry.identity.uaa.oauth.client.PrivateKeyChangeRequest; import org.cloudfoundry.identity.uaa.oauth.client.SecretChangeRequest; import org.cloudfoundry.identity.uaa.resources.ActionResult; import org.cloudfoundry.identity.uaa.resources.AttributeNameMapper; @@ -81,6 +82,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; @@ -535,6 +537,54 @@ public ActionResult changeSecret(@PathVariable String client_id, @RequestBody Se return result; } + @RequestMapping(value = "/oauth/clients/{client_id}/privatekey", method = RequestMethod.PUT) + @ResponseBody + public ActionResult changePrivateKey(@PathVariable String client_id, @RequestBody PrivateKeyChangeRequest change) { + + ClientDetails clientDetails; + try { + clientDetails = clientDetailsService.retrieve(client_id, IdentityZoneHolder.get().getId()); + } catch (InvalidClientException e) { + throw new NoSuchClientException("No such client: " + client_id); + } + + try { + checkPasswordChangeIsAllowed(clientDetails, ""); + } catch (IllegalStateException e) { + throw new InvalidClientDetailsException(e.getMessage()); + } + + HashMap additionalInformation = Optional.ofNullable(clientDetails.getAdditionalInformation()).map(HashMap::new).orElse(new HashMap<>()); + PrivateKeyJwtConfiguration clientKeyConfig = PrivateKeyJwtConfiguration.parseJSONString((String) additionalInformation.get(ClientConstants.PRIVATE_KEY_CONFIG)); + + ActionResult result; + switch (change.getChangeMode()){ + case ADD : + if (change.getKey() != null) { + clientRegistrationService.addClientKeyConfig(client_id, change.getKey(), IdentityZoneHolder.get().getId()); + result = new ActionResult("ok", "Private key is added"); + } else { + result = new ActionResult("ok", "No key added"); + } + break; + + case DELETE : + String deleteString = change.getKeyId() == null ? change.getKey(): change.getKeyId(); + if (clientKeyConfig != null && deleteString != null) { + clientRegistrationService.deleteClientKeyConfig(client_id, deleteString, IdentityZoneHolder.get().getId()); + } + result = new ActionResult("ok", "Private key is deleted"); + break; + + default: + clientRegistrationService.updateClientKeyConfig(client_id, change.getKey(), IdentityZoneHolder.get().getId()); + result = new ActionResult("ok", "Private key updated"); + } + clientSecretChanges.incrementAndGet(); + + return result; + } + private boolean validateCurrentClientSecretAdd(String clientSecret) { return clientSecret == null || clientSecret.split(" ").length == 1; } diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientAdminEndpointsValidator.java b/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientAdminEndpointsValidator.java index 804dd5bfc3e..91830c70e4e 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientAdminEndpointsValidator.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientAdminEndpointsValidator.java @@ -12,14 +12,16 @@ *******************************************************************************/ package org.cloudfoundry.identity.uaa.client; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.cloudfoundry.identity.uaa.constants.OriginKeys; +import org.cloudfoundry.identity.uaa.oauth.client.ClientConstants; +import org.cloudfoundry.identity.uaa.oauth.client.ClientDetailsCreation; import org.cloudfoundry.identity.uaa.resources.QueryableResourceManager; import org.cloudfoundry.identity.uaa.security.beans.SecurityContextAccessor; import org.cloudfoundry.identity.uaa.util.UaaUrlUtils; import org.cloudfoundry.identity.uaa.zone.ClientSecretValidator; import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.InitializingBean; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.oauth2.provider.ClientDetails; @@ -30,7 +32,9 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; +import java.util.Optional; import java.util.Set; import static org.cloudfoundry.identity.uaa.oauth.token.TokenConstants.GRANT_TYPE_AUTHORIZATION_CODE; @@ -245,6 +249,17 @@ public ClientDetails validate(ClientDetails prototype, boolean create, boolean c } clientSecretValidator.validate(client.getClientSecret()); } + + if (prototype instanceof ClientDetailsCreation) { + ClientDetailsCreation clientDetailsCreation = (ClientDetailsCreation) prototype; + HashMap additionalInformation = Optional.ofNullable(prototype.getAdditionalInformation()).map(HashMap::new).orElse(new HashMap<>()); + PrivateKeyJwtConfiguration privateKeyJwtConfiguration = PrivateKeyJwtConfiguration.parse(clientDetailsCreation.getPrivateKeyUrl(), + clientDetailsCreation.getPrivateKeySet()); + if (privateKeyJwtConfiguration != null) { + additionalInformation.put(ClientConstants.PRIVATE_KEY_CONFIG, privateKeyJwtConfiguration.getJSONString()); + client.setAdditionalInformation(additionalInformation); + } + } } return client; diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/client/PrivateKeyJwtConfiguration.java b/server/src/main/java/org/cloudfoundry/identity/uaa/client/PrivateKeyJwtConfiguration.java new file mode 100644 index 00000000000..76b5fccaf3b --- /dev/null +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/client/PrivateKeyJwtConfiguration.java @@ -0,0 +1,177 @@ +package org.cloudfoundry.identity.uaa.client; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.JWKSet; +import org.cloudfoundry.identity.uaa.oauth.jwk.JsonWebKey; +import org.cloudfoundry.identity.uaa.oauth.jwk.JsonWebKeyHelper; +import org.cloudfoundry.identity.uaa.oauth.jwk.JsonWebKeySet; +import org.cloudfoundry.identity.uaa.provider.oauth.OidcMetadataFetcher; +import org.cloudfoundry.identity.uaa.provider.oauth.OidcMetadataFetchingException; +import org.cloudfoundry.identity.uaa.util.JsonUtils; +import org.cloudfoundry.identity.uaa.util.UaaUrlUtils; +import org.springframework.util.ObjectUtils; + +import java.text.ParseException; +import java.util.HashMap; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class PrivateKeyJwtConfiguration { + + @JsonProperty("jwks_uri") + private String privateKeyJwtUrl; + + @JsonProperty("jwks") + private JsonWebKeySet privateKeyJwt; + + public PrivateKeyJwtConfiguration(final String privateKeyJwtUrl, final JsonWebKeySet webKeySet) { + this.privateKeyJwtUrl = privateKeyJwtUrl; + privateKeyJwt = webKeySet; + } + + public String getPrivateKeyJwtUrl() { + return this.privateKeyJwtUrl; + } + + public void setPrivateKeyJwtUrl(final String privateKeyJwtUrl) { + this.privateKeyJwtUrl = privateKeyJwtUrl; + } + + public JsonWebKeySet getPrivateKeyJwt() { + return this.privateKeyJwt; + } + + public void setPrivateKeyJwt(final JsonWebKeySet privateKeyJwt) { + this.privateKeyJwt = privateKeyJwt; + } + + @JsonIgnore + public String getJSONString() { + return JsonUtils.writeValueAsString(this); + } + + @JsonIgnore + public String getCleanString() { + try { + if (UaaUrlUtils.isUrl(this.privateKeyJwtUrl)) { + return this.privateKeyJwtUrl; + } else if (this.privateKeyJwt != null && !ObjectUtils.isEmpty(this.privateKeyJwt.getKeySetMap())) { + return JWKSet.parse(this.privateKeyJwt.getKeySetMap()).toString(true); + } + } catch (IllegalStateException | JsonUtils.JsonUtilException | ParseException e) { + // ignore + } + return null; + } + + @JsonIgnore + public static PrivateKeyJwtConfiguration parse(String privateKeyConfig) { + if (UaaUrlUtils.isUrl(privateKeyConfig)) { + return parse(privateKeyConfig, null); + } else { + return parse(null, privateKeyConfig); + } + } + + @JsonIgnore + public static PrivateKeyJwtConfiguration parse(String privateKeyUrl, String privateKeyJwt) { + PrivateKeyJwtConfiguration privateKeyJwtConfiguration = null; + if (UaaUrlUtils.isUrl(privateKeyUrl)) { + privateKeyJwtConfiguration = new PrivateKeyJwtConfiguration(privateKeyUrl, null); + } else if (privateKeyJwt != null && privateKeyJwt.contains("{") && privateKeyJwt.contains("}")) { + HashMap jsonMap = JsonUtils.readValue(privateKeyJwt, HashMap.class); + String cleanJwtString; + try { + if (jsonMap.containsKey("keys")) { + cleanJwtString = JWKSet.parse(jsonMap).toString(true); + } else { + cleanJwtString = JWK.parse(jsonMap).toPublicJWK().toString(); + } + privateKeyJwtConfiguration = new PrivateKeyJwtConfiguration(null, JsonWebKeyHelper.deserialize(cleanJwtString)); + } catch (ParseException e) { + throw new InvalidClientDetailsException("Private key cannot be parsed", e); + } + } + return privateKeyJwtConfiguration; + } + + @JsonIgnore + public static PrivateKeyJwtConfiguration parseJSONString(String json) { + if (json == null) { + return null; + } + return JsonUtils.readValue(json, PrivateKeyJwtConfiguration.class); + } + + @JsonIgnore + public static JWKSet getJWKSet(String json, OidcMetadataFetcher oidcMetadataFetcher) { + PrivateKeyJwtConfiguration jwtConfiguration; + JWKSet jwkSet; + try { + jwtConfiguration = Optional.ofNullable(parseJSONString(json)).orElse(new PrivateKeyJwtConfiguration(null, null)); + if (UaaUrlUtils.isUrl(jwtConfiguration.privateKeyJwtUrl)) { + jwkSet = JWKSet.parse(oidcMetadataFetcher.fetchWebKeySet(jwtConfiguration.privateKeyJwtUrl).getKeySetMap()); + } else if (jwtConfiguration.privateKeyJwt != null && !ObjectUtils.isEmpty(jwtConfiguration.privateKeyJwt.getKeySetMap())) { + jwkSet = JWKSet.parse(jwtConfiguration.privateKeyJwt.getKeySetMap()); + } else { + jwkSet = new JWKSet(); + } + } catch (OidcMetadataFetchingException | IllegalStateException | JsonUtils.JsonUtilException | ParseException e) { + jwkSet = new JWKSet(); + } + return jwkSet; + } + + @JsonIgnore + public static PrivateKeyJwtConfiguration merge(PrivateKeyJwtConfiguration existingConfig, PrivateKeyJwtConfiguration newConfig) { + if (existingConfig == null) { + return newConfig; + } + if (newConfig == null) { + return existingConfig; + } + PrivateKeyJwtConfiguration result = null; + if (newConfig.privateKeyJwtUrl != null) { + result = new PrivateKeyJwtConfiguration(newConfig.privateKeyJwtUrl, null); + } + if (newConfig.privateKeyJwt != null) { + if (existingConfig.privateKeyJwt == null) { + result = new PrivateKeyJwtConfiguration(null, newConfig.privateKeyJwt); + } else { + JsonWebKeySet existingKeySet = existingConfig.privateKeyJwt; + existingKeySet.getKeySetMap().putAll(newConfig.getPrivateKeyJwt().getKeySetMap()); + result = new PrivateKeyJwtConfiguration(null, existingKeySet); + } + } + return result; + } + + @JsonIgnore + public static PrivateKeyJwtConfiguration delete(PrivateKeyJwtConfiguration existingConfig, PrivateKeyJwtConfiguration tobeDeleted) { + if (existingConfig == null) { + return null; + } + if (tobeDeleted == null) { + return existingConfig; + } + PrivateKeyJwtConfiguration result = null; + if (existingConfig.privateKeyJwt != null && tobeDeleted.privateKeyJwtUrl != null) { + JsonWebKeySet existingKeySet = existingConfig.privateKeyJwt; + List keys = existingKeySet.getKeys().stream().filter(k -> !existingConfig.privateKeyJwt.equals(k.getKid())).collect(Collectors.toList()); + result = new PrivateKeyJwtConfiguration(null, new JsonWebKeySet<>(keys)); + } + if (existingConfig.privateKeyJwt != null && tobeDeleted.privateKeyJwt != null) { + JsonWebKeySet existingKeySet = existingConfig.privateKeyJwt; + tobeDeleted.privateKeyJwt.getKeySetMap().forEach(existingKeySet.getKeySetMap()::remove); + result = new PrivateKeyJwtConfiguration(null, existingKeySet); + } + return result; + } +} diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/zone/MultitenantClientServices.java b/server/src/main/java/org/cloudfoundry/identity/uaa/zone/MultitenantClientServices.java index 528ba225c7e..fd5321c1dda 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/zone/MultitenantClientServices.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/zone/MultitenantClientServices.java @@ -30,6 +30,12 @@ interface MultitenantClientSecretService { void addClientSecret(String clientId, String newSecret, String zoneId) throws NoSuchClientException; void deleteClientSecret(String clientId, String zoneId) throws NoSuchClientException; + + void addClientKeyConfig(String clientId, String keyConfig, String zoneId) throws NoSuchClientException; + + void updateClientKeyConfig(String clientId, String keyConfig, String zoneId) throws NoSuchClientException; + + void deleteClientKeyConfig(String clientId, String keyConfig, String zoneId) throws NoSuchClientException; } public abstract class MultitenantClientServices implements diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/zone/MultitenantJdbcClientDetailsService.java b/server/src/main/java/org/cloudfoundry/identity/uaa/zone/MultitenantJdbcClientDetailsService.java index bc7e3922807..4cc4db99d1c 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/zone/MultitenantJdbcClientDetailsService.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/zone/MultitenantJdbcClientDetailsService.java @@ -2,6 +2,7 @@ import org.cloudfoundry.identity.uaa.audit.event.SystemDeletable; import org.cloudfoundry.identity.uaa.authentication.UaaPrincipal; +import org.cloudfoundry.identity.uaa.client.PrivateKeyJwtConfiguration; import org.cloudfoundry.identity.uaa.oauth.client.ClientConstants; import org.cloudfoundry.identity.uaa.resources.ResourceMonitor; import org.cloudfoundry.identity.uaa.security.ContextSensitiveOAuth2SecurityExpressionMethods; @@ -39,6 +40,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import static java.util.Collections.emptySet; @@ -276,6 +278,48 @@ public void deleteClientSecret(String clientId, String zoneId) throws NoSuchClie } } + @Override + public void addClientKeyConfig(String clientId, String keyConfig, String zoneId) throws NoSuchClientException { + PrivateKeyJwtConfiguration privateKeyJwtConfiguration = PrivateKeyJwtConfiguration.parse(keyConfig); + if (privateKeyJwtConfiguration != null) { + BaseClientDetails clientDetails = (BaseClientDetails) loadClientByClientId(clientId, zoneId); + HashMap additionalInformation = Optional.ofNullable(clientDetails.getAdditionalInformation()).map(HashMap::new).orElse(new HashMap<>()); + PrivateKeyJwtConfiguration existingConfig = PrivateKeyJwtConfiguration.parseJSONString((String) additionalInformation.get(ClientConstants.PRIVATE_KEY_CONFIG)); + additionalInformation.put(ClientConstants.PRIVATE_KEY_CONFIG, PrivateKeyJwtConfiguration.merge(existingConfig, privateKeyJwtConfiguration).getJSONString()); + clientDetails.setAdditionalInformation(additionalInformation); + updateClientDetails(clientDetails, zoneId); + } + } + + @Override + public void updateClientKeyConfig(String clientId, String keyConfig, String zoneId) throws NoSuchClientException { + PrivateKeyJwtConfiguration privateKeyJwtConfiguration = PrivateKeyJwtConfiguration.parse(keyConfig); + if (privateKeyJwtConfiguration != null) { + BaseClientDetails clientDetails = (BaseClientDetails) loadClientByClientId(clientId, zoneId); + HashMap additionalInformation = Optional.ofNullable(clientDetails.getAdditionalInformation()).map(HashMap::new).orElse(new HashMap<>()); + additionalInformation.put(ClientConstants.PRIVATE_KEY_CONFIG, privateKeyJwtConfiguration.getJSONString()); + clientDetails.setAdditionalInformation(additionalInformation); + updateClientDetails(clientDetails, zoneId); + } + } + + @Override + public void deleteClientKeyConfig(String clientId, String keyConfig, String zoneId) throws NoSuchClientException { + PrivateKeyJwtConfiguration privateKeyJwtConfiguration = PrivateKeyJwtConfiguration.parse(keyConfig); + if (privateKeyJwtConfiguration != null) { + BaseClientDetails clientDetails = (BaseClientDetails) loadClientByClientId(clientId, zoneId); + HashMap additionalInformation = Optional.ofNullable(clientDetails.getAdditionalInformation()).map(HashMap::new).orElse(new HashMap<>()); + PrivateKeyJwtConfiguration existingConfig = PrivateKeyJwtConfiguration.parseJSONString((String) additionalInformation.get(ClientConstants.PRIVATE_KEY_CONFIG)); + PrivateKeyJwtConfiguration result = PrivateKeyJwtConfiguration.delete(existingConfig, privateKeyJwtConfiguration); + if (result != null) { + additionalInformation.put(ClientConstants.PRIVATE_KEY_CONFIG, result); + } else { + additionalInformation.remove(ClientConstants.PRIVATE_KEY_CONFIG); + } + clientDetails.setAdditionalInformation(additionalInformation); + updateClientDetails(clientDetails, zoneId); + } + } /** * Row mapper for ClientDetails. diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/zone/InMemoryMultitenantClientServices.java b/server/src/test/java/org/cloudfoundry/identity/uaa/zone/InMemoryMultitenantClientServices.java index c46478fafe5..be325d003e2 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/zone/InMemoryMultitenantClientServices.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/zone/InMemoryMultitenantClientServices.java @@ -48,6 +48,21 @@ public void deleteClientSecret(String clientId, String zoneId) throws NoSuchClie throw new UnsupportedOperationException(); } + @Override + public void addClientKeyConfig(String clientId, String keyConfig, String zoneId) throws NoSuchClientException { + throw new UnsupportedOperationException(); + } + + @Override + public void updateClientKeyConfig(String clientId, String keyConfig, String zoneId) throws NoSuchClientException { + throw new UnsupportedOperationException(); + } + + @Override + public void deleteClientKeyConfig(String clientId, String keyConfig, String zoneId) throws NoSuchClientException { + throw new UnsupportedOperationException(); + } + @Override public void addClientDetails(ClientDetails clientDetails, String zoneId) throws ClientAlreadyExistsException { getInMemoryService(zoneId).put(clientDetails.getClientId(), (BaseClientDetails) clientDetails);