Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: add persistence support for private_key_jwt client authentication #2449

Merged
merged 33 commits into from
Sep 26, 2023
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
5f72461
refactor: prepare for private_key_jwt in oauth_client_details
strehle Aug 7, 2023
bf61c5d
feature: add persistence support for private_key_jwt
strehle Aug 22, 2023
8530edc
Merge branch 'develop' of github.com:cloudfoundry/uaa into feature/is…
strehle Aug 23, 2023
e72194a
more tests
strehle Aug 23, 2023
154589c
refactorings
strehle Aug 24, 2023
5376079
add tests
strehle Aug 24, 2023
a9cba87
Renamed
strehle Aug 24, 2023
c69b748
Renamed
strehle Aug 24, 2023
97c06aa
Renamed
strehle Aug 24, 2023
ea97594
Merge branch 'develop' of github.com:cloudfoundry/uaa into feature/is…
strehle Aug 24, 2023
187dbb9
Add column client_jwt_config and do some refactoring for UaaClientDet…
strehle Aug 24, 2023
73599e8
Merge branch 'develop' of github.com:cloudfoundry/uaa into feature/is…
strehle Aug 24, 2023
82a5857
Sonar findings
strehle Aug 24, 2023
bf6f7b2
cleanup
strehle Aug 24, 2023
127d570
Merge branch 'develop' of github.com:cloudfoundry/uaa into feature/is…
strehle Aug 24, 2023
e4ea431
Merge
strehle Aug 24, 2023
415983b
Refactoring because of usage of client_jwt_config now from oauth_clie…
strehle Aug 24, 2023
62025d8
Merge branch 'develop' of github.com:cloudfoundry/uaa into feature/is…
strehle Aug 25, 2023
aace8e0
remove not needed method.
strehle Aug 25, 2023
a7d62ac
Merge branch 'develop' of github.com:cloudfoundry/uaa into feature/is…
strehle Sep 8, 2023
aecb645
review
strehle Sep 8, 2023
6a6403b
Merge branch 'feature/issue/2235/refactorClient' of github.com:cloudf…
strehle Sep 8, 2023
3dc5760
Merge branches 'feature/issue/2235/jwtTrustConfig' and 'develop' of g…
strehle Sep 14, 2023
4310cfd
Merge branch 'develop' of github.com:cloudfoundry/uaa into feature/is…
strehle Sep 15, 2023
e7c0bc2
own events for jwt client configuration
strehle Sep 18, 2023
fe955c9
review
strehle Sep 18, 2023
4c1bf39
doc: Add documentation
strehle Sep 18, 2023
6650eb3
Add new scope clients.trust
strehle Sep 18, 2023
ae0654d
Merge branch 'develop' of github.com:cloudfoundry/uaa into feature/is…
strehle Sep 18, 2023
b3c3ea5
review
strehle Sep 18, 2023
2782dfa
more tests
strehle Sep 18, 2023
379fe73
sonar findings
strehle Sep 18, 2023
5a85e54
fix sonar issues
strehle Sep 19, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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("jwks_uri")
private String jsonWebKeyUri;

@JsonProperty("jwks")
private String jsonWebKeySet;

@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 getJsonWebKeyUri() {
return jsonWebKeyUri;
}

public void setJsonWebKeyUri(String jsonWebKeyUri) {
this.jsonWebKeyUri = jsonWebKeyUri;
}

public String getJsonWebKeySet() {
return jsonWebKeySet;
}

public void setJsonWebKeySet(String jsonWebKeySet) {
this.jsonWebKeySet = jsonWebKeySet;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
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.ClientJwtChangeRequest.ChangeMode.ADD;

/**
*/
strehle marked this conversation as resolved.
Show resolved Hide resolved
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
public class ClientJwtChangeRequest {

public enum ChangeMode {
UPDATE,
ADD,
DELETE
}
@JsonProperty("kid")
private String keyId;
@JsonProperty("jwks_uri")
private String jsonWebKeyUri;
@JsonProperty("jwks")
private String jsonWebKeySet;
@JsonProperty("client_id")
private String clientId;
private ChangeMode changeMode = ADD;

public ClientJwtChangeRequest() {
}

public ClientJwtChangeRequest(String clientId, String jsonWebKeyUri, String jsonWebKeySet) {
this.jsonWebKeyUri = jsonWebKeyUri;
this.jsonWebKeySet = jsonWebKeySet;
this.clientId = clientId;
}

public String getJsonWebKeyUri() {
return jsonWebKeyUri;
}

public void setJsonWebKeyUri(String jsonWebKeyUri) {
this.jsonWebKeyUri = jsonWebKeyUri;
}

public String getJsonWebKeySet() {
return jsonWebKeySet;
}

public void setJsonWebKeySet(String jsonWebKeySet) {
this.jsonWebKeySet = jsonWebKeySet;
}

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 jsonWebKeyUri != null ? jsonWebKeyUri : jsonWebKeySet;
strehle marked this conversation as resolved.
Show resolved Hide resolved
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package org.cloudfoundry.identity.uaa.oauth.client;

import org.cloudfoundry.identity.uaa.util.JsonUtils;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertNotEquals;

class ClientJwtChangeRequestTest {

@Test
void testRequestSerialization() {
ClientJwtChangeRequest def = new ClientJwtChangeRequest(null, null, null);
def.setKeyId("key-1");
def.setChangeMode(ClientJwtChangeRequest.ChangeMode.DELETE);
def.setJsonWebKeyUri("http://localhost:8080/uaa/token_key");
def.setJsonWebKeySet("{}");
def.setClientId("admin");
String jsonRequest = JsonUtils.writeValueAsString(def);
ClientJwtChangeRequest request = JsonUtils.readValue(jsonRequest, ClientJwtChangeRequest.class);
assertNotEquals(def, request);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,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 @@ -158,7 +159,7 @@ private void addNewClients() {
if (map.get("authorized-grant-types") == null) {
throw new InvalidClientDetailsException("Client must have at least one authorized-grant-type. client ID: " + clientId);
}
BaseClientDetails client = new BaseClientDetails(clientId, (String) map.get("resource-ids"),
UaaClientDetails client = new UaaClientDetails(clientId, (String) map.get("resource-ids"),
(String) map.get("scope"), (String) map.get("authorized-grant-types"),
(String) map.get("authorities"), getRedirectUris(map));

Expand Down Expand Up @@ -204,11 +205,26 @@ 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);
}

client.setAdditionalInformation(info);

if (map.get("jwks_uri") instanceof String) {
String jwksUri = (String) map.get("jwks_uri");
ClientJwtConfiguration keyConfig = ClientJwtConfiguration.parse(UaaUrlUtils.normalizeUri(jwksUri), null);
if (keyConfig != null && keyConfig.getCleanString() != null) {
keyConfig.writeValue(client);
}
} else if (map.get("jwks") instanceof String) {
String jwks = (String) map.get("jwks");
ClientJwtConfiguration keyConfig = ClientJwtConfiguration.parse(null, jwks);
if (keyConfig != null && keyConfig.getCleanString() != null) {
keyConfig.writeValue(client);
}
}
hsinn0 marked this conversation as resolved.
Show resolved Hide resolved

try {
clientRegistrationService.addClientDetails(client, IdentityZone.getUaaZoneId());
if (secondSecret != null) {
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.ClientJwtChangeRequest;
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 @@ -535,6 +536,53 @@ public ActionResult changeSecret(@PathVariable String client_id, @RequestBody Se
return result;
}

@RequestMapping(value = "/oauth/clients/{client_id}/clientjwt", method = RequestMethod.PUT)
@ResponseBody
public ActionResult changeClientJwt(@PathVariable String client_id, @RequestBody ClientJwtChangeRequest change) {
strehle marked this conversation as resolved.
Show resolved Hide resolved

UaaClientDetails uaaClientDetails;
try {
uaaClientDetails = (UaaClientDetails) clientDetailsService.retrieve(client_id, IdentityZoneHolder.get().getId());
} catch (InvalidClientException e) {
throw new NoSuchClientException("No such client: " + client_id);
}

try {
checkPasswordChangeIsAllowed(uaaClientDetails, "");
} catch (IllegalStateException e) {
throw new InvalidClientDetailsException(e.getMessage());
}

ClientJwtConfiguration clientKeyConfig = ClientJwtConfiguration.readValue(uaaClientDetails);

ActionResult result;
switch (change.getChangeMode()){
case ADD :
if (change.getKey() != null) {
clientRegistrationService.addClientJwtConfig(client_id, change.getKey(), IdentityZoneHolder.get().getId(), false);
result = new ActionResult("ok", "Client jwt configuration 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.deleteClientJwtConfig(client_id, deleteString, IdentityZoneHolder.get().getId());
}
result = new ActionResult("ok", "Client jwt configuration is deleted");
break;

default:
clientRegistrationService.addClientJwtConfig(client_id, change.getKey(), IdentityZoneHolder.get().getId(), true);
result = new ActionResult("ok", "Client jwt configuration 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,15 @@
*******************************************************************************/
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.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 Down Expand Up @@ -245,6 +246,19 @@ public ClientDetails validate(ClientDetails prototype, boolean create, boolean c
}
clientSecretValidator.validate(client.getClientSecret());
}

if (prototype instanceof ClientDetailsCreation) {
ClientDetailsCreation clientDetailsCreation = (ClientDetailsCreation) prototype;
if (StringUtils.hasText(clientDetailsCreation.getJsonWebKeyUri()) || StringUtils.hasText(clientDetailsCreation.getJsonWebKeySet())) {
ClientJwtConfiguration clientJwtConfiguration = ClientJwtConfiguration.parse(clientDetailsCreation.getJsonWebKeyUri(),
clientDetailsCreation.getJsonWebKeySet());
if (clientJwtConfiguration != null) {
clientJwtConfiguration.writeValue(client);
} else {
logger.warn("Client with client jwt configuration not valid");
strehle marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
}

return client;
Expand Down
Loading