Skip to content

Commit

Permalink
feature: add persistence support for private_key_jwt
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
strehle committed Aug 22, 2023
1 parent 4021783 commit 596feb0
Show file tree
Hide file tree
Showing 10 changed files with 423 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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<String, Object> 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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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<String, Object> 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;
Expand Down
Loading

0 comments on commit 596feb0

Please sign in to comment.