[KEYCLOAK-18723] - Configurable constraints for request object encryption
This commit is contained in:
parent
730d4e8ac9
commit
396a78bcc4
13 changed files with 253 additions and 28 deletions
|
@ -75,6 +75,22 @@ public class OIDCAdvancedConfigWrapper {
|
||||||
setAttribute(OIDCConfigAttributes.REQUEST_OBJECT_SIGNATURE_ALG, algStr);
|
setAttribute(OIDCConfigAttributes.REQUEST_OBJECT_SIGNATURE_ALG, algStr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setRequestObjectEncryptionAlg(String algorithm) {
|
||||||
|
setAttribute(OIDCConfigAttributes.REQUEST_OBJECT_ENCRYPTION_ALG, algorithm);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRequestObjectEncryptionAlg() {
|
||||||
|
return getAttribute(OIDCConfigAttributes.REQUEST_OBJECT_ENCRYPTION_ALG);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRequestObjectEncryptionEnc() {
|
||||||
|
return getAttribute(OIDCConfigAttributes.REQUEST_OBJECT_ENCRYPTION_ENC);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRequestObjectEncryptionEnc(String algorithm) {
|
||||||
|
setAttribute(OIDCConfigAttributes.REQUEST_OBJECT_ENCRYPTION_ENC, algorithm);
|
||||||
|
}
|
||||||
|
|
||||||
public String getRequestObjectRequired() {
|
public String getRequestObjectRequired() {
|
||||||
return getAttribute(OIDCConfigAttributes.REQUEST_OBJECT_REQUIRED);
|
return getAttribute(OIDCConfigAttributes.REQUEST_OBJECT_REQUIRED);
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,8 @@ public final class OIDCConfigAttributes {
|
||||||
public static final String USER_INFO_RESPONSE_SIGNATURE_ALG = "user.info.response.signature.alg";
|
public static final String USER_INFO_RESPONSE_SIGNATURE_ALG = "user.info.response.signature.alg";
|
||||||
|
|
||||||
public static final String REQUEST_OBJECT_SIGNATURE_ALG = "request.object.signature.alg";
|
public static final String REQUEST_OBJECT_SIGNATURE_ALG = "request.object.signature.alg";
|
||||||
|
public static final String REQUEST_OBJECT_ENCRYPTION_ALG = "request.object.encryption.alg";
|
||||||
|
public static final String REQUEST_OBJECT_ENCRYPTION_ENC = "request.object.encryption.enc";
|
||||||
|
|
||||||
public static final String REQUEST_OBJECT_REQUIRED = "request.object.required";
|
public static final String REQUEST_OBJECT_REQUIRED = "request.object.required";
|
||||||
public static final String REQUEST_OBJECT_REQUIRED_REQUEST_OR_REQUEST_URI = "request or request_uri";
|
public static final String REQUEST_OBJECT_REQUIRED_REQUEST_OR_REQUEST_URI = "request or request_uri";
|
||||||
|
|
|
@ -19,10 +19,13 @@ package org.keycloak.protocol.oidc.endpoints.request;
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.function.BiConsumer;
|
||||||
|
|
||||||
import org.keycloak.OAuth2Constants;
|
import org.keycloak.OAuth2Constants;
|
||||||
import org.keycloak.jose.JOSEHeader;
|
import org.keycloak.jose.JOSEHeader;
|
||||||
import org.keycloak.jose.JOSE;
|
import org.keycloak.jose.JOSE;
|
||||||
|
import org.keycloak.jose.jwe.JWE;
|
||||||
|
import org.keycloak.jose.jwe.JWEHeader;
|
||||||
import org.keycloak.jose.jws.Algorithm;
|
import org.keycloak.jose.jws.Algorithm;
|
||||||
import org.keycloak.jose.jws.JWSInput;
|
import org.keycloak.jose.jws.JWSInput;
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
|
@ -36,28 +39,10 @@ import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
|
||||||
*/
|
*/
|
||||||
public class AuthzEndpointRequestObjectParser extends AuthzEndpointRequestParser {
|
public class AuthzEndpointRequestObjectParser extends AuthzEndpointRequestParser {
|
||||||
|
|
||||||
private static void validateAlgorithm(JOSE jwt, ClientModel clientModel) {
|
|
||||||
if (jwt instanceof JWSInput) {
|
|
||||||
JOSEHeader header = jwt.getHeader();
|
|
||||||
String headerAlgorithm = header.getRawAlgorithm();
|
|
||||||
|
|
||||||
if (headerAlgorithm == null) {
|
|
||||||
throw new RuntimeException("Request object signed algorithm not specified");
|
|
||||||
}
|
|
||||||
|
|
||||||
Algorithm requestedSignatureAlgorithm = OIDCAdvancedConfigWrapper.fromClientModel(clientModel)
|
|
||||||
.getRequestObjectSignatureAlg();
|
|
||||||
|
|
||||||
if (requestedSignatureAlgorithm != null && !requestedSignatureAlgorithm.name().equals(headerAlgorithm)) {
|
|
||||||
throw new RuntimeException("Request object signed with different algorithm than client requested algorithm");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private final JsonNode requestParams;
|
private final JsonNode requestParams;
|
||||||
|
|
||||||
public AuthzEndpointRequestObjectParser(KeycloakSession session, String requestObject, ClientModel client) {
|
public AuthzEndpointRequestObjectParser(KeycloakSession session, String requestObject, ClientModel client) {
|
||||||
this.requestParams = session.tokens().decodeClientJWT(requestObject, client, AuthzEndpointRequestObjectParser::validateAlgorithm, JsonNode.class);
|
this.requestParams = session.tokens().decodeClientJWT(requestObject, client, createRequestObjectValidator(session), JsonNode.class);
|
||||||
|
|
||||||
if (this.requestParams == null) {
|
if (this.requestParams == null) {
|
||||||
throw new RuntimeException("Failed to verify signature on 'request' object");
|
throw new RuntimeException("Failed to verify signature on 'request' object");
|
||||||
|
@ -101,6 +86,48 @@ public class AuthzEndpointRequestObjectParser extends AuthzEndpointRequestParser
|
||||||
return keys;
|
return keys;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private BiConsumer<JOSE, ClientModel> createRequestObjectValidator(KeycloakSession session) {
|
||||||
|
return (jwt, clientModel) -> {
|
||||||
|
if (jwt instanceof JWSInput) {
|
||||||
|
JOSEHeader header = jwt.getHeader();
|
||||||
|
String headerAlgorithm = header.getRawAlgorithm();
|
||||||
|
|
||||||
|
if (headerAlgorithm == null) {
|
||||||
|
throw new RuntimeException("Request object signed algorithm not specified");
|
||||||
|
}
|
||||||
|
|
||||||
|
Algorithm requestedSignatureAlgorithm = OIDCAdvancedConfigWrapper.fromClientModel(clientModel)
|
||||||
|
.getRequestObjectSignatureAlg();
|
||||||
|
|
||||||
|
if (requestedSignatureAlgorithm != null && !requestedSignatureAlgorithm.name().equals(headerAlgorithm)) {
|
||||||
|
throw new RuntimeException(
|
||||||
|
"Request object signed with different algorithm than client requested algorithm");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
String encryptionAlg = OIDCAdvancedConfigWrapper.fromClientModel(clientModel).getRequestObjectEncryptionAlg();
|
||||||
|
|
||||||
|
if (encryptionAlg != null) {
|
||||||
|
if (!encryptionAlg.equals(jwt.getHeader().getRawAlgorithm())) {
|
||||||
|
throw new RuntimeException("Request object encrypted with different algorithm than client requested algorithm");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String encryptionEncAlg = OIDCAdvancedConfigWrapper.fromClientModel(clientModel).getRequestObjectEncryptionEnc();
|
||||||
|
|
||||||
|
if (encryptionEncAlg != null) {
|
||||||
|
JWE jwe = (JWE) jwt;
|
||||||
|
JWEHeader header = (JWEHeader) jwe.getHeader();
|
||||||
|
|
||||||
|
if (!encryptionEncAlg.equals(header.getEncryptionAlgorithm())) {
|
||||||
|
throw new RuntimeException("Request object content encrypted with different algorithm than client requested algorithm");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
session.setAttribute(AuthzEndpointRequestParser.AUTHZ_REQUEST_OBJECT_ENCRYPTED, jwt);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected <T> T replaceIfNotNull(T previousVal, T newVal) {
|
protected <T> T replaceIfNotNull(T previousVal, T newVal) {
|
||||||
// force parameters values from request object as per spec any parameter set directly should be ignored
|
// force parameters values from request object as per spec any parameter set directly should be ignored
|
||||||
|
|
|
@ -47,6 +47,7 @@ public abstract class AuthzEndpointRequestParser {
|
||||||
public static final int ADDITIONAL_REQ_PARAMS_MAX_SIZE = 200;
|
public static final int ADDITIONAL_REQ_PARAMS_MAX_SIZE = 200;
|
||||||
|
|
||||||
public static final String AUTHZ_REQUEST_OBJECT = "ParsedRequestObject";
|
public static final String AUTHZ_REQUEST_OBJECT = "ParsedRequestObject";
|
||||||
|
public static final String AUTHZ_REQUEST_OBJECT_ENCRYPTED = "EncryptedRequestObject";
|
||||||
|
|
||||||
/** Set of known protocol GET params not to be stored into additionalReqParams} */
|
/** Set of known protocol GET params not to be stored into additionalReqParams} */
|
||||||
public static final Set<String> KNOWN_REQ_PARAMS = new HashSet<>();
|
public static final Set<String> KNOWN_REQ_PARAMS = new HashSet<>();
|
||||||
|
|
|
@ -63,6 +63,7 @@ public class SecureRequestObjectExecutor implements ClientPolicyExecutorProvider
|
||||||
configuration = new Configuration();
|
configuration = new Configuration();
|
||||||
configuration.setVerifyNbf(Boolean.TRUE);
|
configuration.setVerifyNbf(Boolean.TRUE);
|
||||||
configuration.setAvailablePeriod(DEFAULT_AVAILABLE_PERIOD);
|
configuration.setAvailablePeriod(DEFAULT_AVAILABLE_PERIOD);
|
||||||
|
configuration.setEncryptionRequired(Boolean.FALSE);
|
||||||
} else {
|
} else {
|
||||||
configuration = config;
|
configuration = config;
|
||||||
if (config.isVerifyNbf() == null) {
|
if (config.isVerifyNbf() == null) {
|
||||||
|
@ -71,6 +72,9 @@ public class SecureRequestObjectExecutor implements ClientPolicyExecutorProvider
|
||||||
if (config.getAvailablePeriod() == null) {
|
if (config.getAvailablePeriod() == null) {
|
||||||
configuration.setAvailablePeriod(DEFAULT_AVAILABLE_PERIOD);
|
configuration.setAvailablePeriod(DEFAULT_AVAILABLE_PERIOD);
|
||||||
}
|
}
|
||||||
|
if (config.isEncryptionRequired() == null) {
|
||||||
|
configuration.setEncryptionRequired(Boolean.FALSE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -84,6 +88,8 @@ public class SecureRequestObjectExecutor implements ClientPolicyExecutorProvider
|
||||||
protected Integer availablePeriod;
|
protected Integer availablePeriod;
|
||||||
@JsonProperty("verify-nbf")
|
@JsonProperty("verify-nbf")
|
||||||
protected Boolean verifyNbf;
|
protected Boolean verifyNbf;
|
||||||
|
@JsonProperty(SecureRequestObjectExecutorFactory.ENCRYPTION_REQUIRED)
|
||||||
|
private Boolean encryptionRequired;
|
||||||
|
|
||||||
public Integer getAvailablePeriod() {
|
public Integer getAvailablePeriod() {
|
||||||
return availablePeriod;
|
return availablePeriod;
|
||||||
|
@ -100,6 +106,14 @@ public class SecureRequestObjectExecutor implements ClientPolicyExecutorProvider
|
||||||
public void setVerifyNbf(Boolean verifyNbf) {
|
public void setVerifyNbf(Boolean verifyNbf) {
|
||||||
this.verifyNbf = verifyNbf;
|
this.verifyNbf = verifyNbf;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setEncryptionRequired(Boolean encryptionRequired) {
|
||||||
|
this.encryptionRequired = encryptionRequired;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean isEncryptionRequired() {
|
||||||
|
return encryptionRequired;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -229,6 +243,12 @@ public class SecureRequestObjectExecutor implements ClientPolicyExecutorProvider
|
||||||
throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, "Invalid parameter. Parameters in 'request' object not matching with request parameters");
|
throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, "Invalid parameter. Parameters in 'request' object not matching with request parameters");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Boolean encryptionRequired = Optional.ofNullable(configuration.isEncryptionRequired()).orElse(Boolean.FALSE);
|
||||||
|
if (encryptionRequired && session.getAttribute(AuthzEndpointRequestParser.AUTHZ_REQUEST_OBJECT_ENCRYPTED) == null) {
|
||||||
|
logger.trace("request object's not encrypted.");
|
||||||
|
throw new ClientPolicyException(INVALID_REQUEST_OBJECT, "Request object not encrypted");
|
||||||
|
}
|
||||||
|
|
||||||
logger.trace("Passed.");
|
logger.trace("Passed.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -41,11 +41,16 @@ public class SecureRequestObjectExecutorFactory implements ClientPolicyExecutorP
|
||||||
"claim and this claim will be validated", ProviderConfigProperty.BOOLEAN_TYPE, true);
|
"claim and this claim will be validated", ProviderConfigProperty.BOOLEAN_TYPE, true);
|
||||||
|
|
||||||
public static final String AVAILABLE_PERIOD = "available-period";
|
public static final String AVAILABLE_PERIOD = "available-period";
|
||||||
|
public static final String ENCRYPTION_REQUIRED = "encryption-required";
|
||||||
|
|
||||||
private static final ProviderConfigProperty AVAILABLE_PERIOD_PROPERTY = new ProviderConfigProperty(
|
private static final ProviderConfigProperty AVAILABLE_PERIOD_PROPERTY = new ProviderConfigProperty(
|
||||||
AVAILABLE_PERIOD, "Available Period", "The maximum period in seconds for which the 'request' object used in OIDC authorization request is considered valid. " +
|
AVAILABLE_PERIOD, "Available Period", "The maximum period in seconds for which the 'request' object used in OIDC authorization request is considered valid. " +
|
||||||
"It is used if 'Verify Not-Before' is ON.", ProviderConfigProperty.STRING_TYPE, "3600");
|
"It is used if 'Verify Not-Before' is ON.", ProviderConfigProperty.STRING_TYPE, "3600");
|
||||||
|
|
||||||
|
private static final ProviderConfigProperty ENCRYPTION_REQUIRED_PROPERTY = new ProviderConfigProperty(
|
||||||
|
ENCRYPTION_REQUIRED, "Encryption Required", "Whether request object encryption is required. If enabled, request objects must be encrypted. Otherwise, encryption is optional.",
|
||||||
|
ProviderConfigProperty.BOOLEAN_TYPE, Boolean.FALSE);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ClientPolicyExecutorProvider create(KeycloakSession session) {
|
public ClientPolicyExecutorProvider create(KeycloakSession session) {
|
||||||
return new SecureRequestObjectExecutor(session);
|
return new SecureRequestObjectExecutor(session);
|
||||||
|
@ -75,7 +80,7 @@ public class SecureRequestObjectExecutorFactory implements ClientPolicyExecutorP
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<ProviderConfigProperty> getConfigProperties() {
|
public List<ProviderConfigProperty> getConfigProperties() {
|
||||||
return new ArrayList<>(Arrays.asList(VERIFY_NBF_PROPERTY, AVAILABLE_PERIOD_PROPERTY));
|
return new ArrayList<>(Arrays.asList(VERIFY_NBF_PROPERTY, AVAILABLE_PERIOD_PROPERTY, ENCRYPTION_REQUIRED_PROPERTY));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -320,6 +320,12 @@ public class DescriptionConverter {
|
||||||
if (config.getRequestObjectSignatureAlg() != null) {
|
if (config.getRequestObjectSignatureAlg() != null) {
|
||||||
response.setRequestObjectSigningAlg(config.getRequestObjectSignatureAlg().toString());
|
response.setRequestObjectSigningAlg(config.getRequestObjectSignatureAlg().toString());
|
||||||
}
|
}
|
||||||
|
if (config.getRequestObjectEncryptionAlg() != null) {
|
||||||
|
response.setRequestObjectEncryptionAlg(config.getRequestObjectEncryptionAlg());
|
||||||
|
}
|
||||||
|
if (config.getRequestObjectEncryptionEnc() != null) {
|
||||||
|
response.setRequestObjectEncryptionEnc(config.getRequestObjectEncryptionEnc());
|
||||||
|
}
|
||||||
if (config.isUseJwksUrl()) {
|
if (config.isUseJwksUrl()) {
|
||||||
response.setJwksUri(config.getJwksUrl());
|
response.setJwksUri(config.getJwksUrl());
|
||||||
}
|
}
|
||||||
|
|
|
@ -1168,7 +1168,7 @@ public class ClientPoliciesTest extends AbstractClientPoliciesTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSecureRequestObjectExecutor() throws Exception, URISyntaxException, IOException {
|
public void testSecureRequestObjectExecutor() throws Exception {
|
||||||
Integer availablePeriod = Integer.valueOf(SecureRequestObjectExecutor.DEFAULT_AVAILABLE_PERIOD + 400);
|
Integer availablePeriod = Integer.valueOf(SecureRequestObjectExecutor.DEFAULT_AVAILABLE_PERIOD + 400);
|
||||||
// register profiles
|
// register profiles
|
||||||
String json = (new ClientProfilesBuilder()).addProfile(
|
String json = (new ClientProfilesBuilder()).addProfile(
|
||||||
|
@ -1362,6 +1362,19 @@ public class ClientPoliciesTest extends AbstractClientPoliciesTest {
|
||||||
registerRequestObject(requestObject, clientId, Algorithm.ES256, false);
|
registerRequestObject(requestObject, clientId, Algorithm.ES256, false);
|
||||||
successfulLoginAndLogout(clientId, clientSecret);
|
successfulLoginAndLogout(clientId, clientSecret);
|
||||||
|
|
||||||
|
// update profile : force request object encryption
|
||||||
|
json = (new ClientProfilesBuilder()).addProfile(
|
||||||
|
(new ClientProfileBuilder()).createProfile(PROFILE_NAME, "Prvy Profil")
|
||||||
|
.addExecutor(SecureRequestObjectExecutorFactory.PROVIDER_ID, createSecureRequestObjectExecutorConfig(null, null, true))
|
||||||
|
.toRepresentation()
|
||||||
|
).toString();
|
||||||
|
updateProfiles(json);
|
||||||
|
|
||||||
|
requestObject = createValidRequestObjectForSecureRequestObjectExecutor(clientId);
|
||||||
|
registerRequestObject(requestObject, clientId, Algorithm.ES256, false);
|
||||||
|
oauth.openLoginForm();
|
||||||
|
assertEquals(SecureRequestObjectExecutor.INVALID_REQUEST_OBJECT, oauth.getCurrentQuery().get(OAuth2Constants.ERROR));
|
||||||
|
assertEquals("Request object not encrypted", oauth.getCurrentQuery().get(OAuth2Constants.ERROR_DESCRIPTION));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -105,6 +105,7 @@ import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.junit.Assert.assertNull;
|
import static org.junit.Assert.assertNull;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.junit.Assert.fail;
|
||||||
import static org.keycloak.jose.jwe.JWEConstants.RSA_OAEP;
|
import static org.keycloak.jose.jwe.JWEConstants.RSA_OAEP;
|
||||||
import static org.keycloak.jose.jwe.JWEConstants.RSA_OAEP_256;
|
import static org.keycloak.jose.jwe.JWEConstants.RSA_OAEP_256;
|
||||||
import static org.keycloak.testsuite.admin.ApiUtil.findClientResourceByClientId;
|
import static org.keycloak.testsuite.admin.ApiUtil.findClientResourceByClientId;
|
||||||
|
@ -1296,8 +1297,84 @@ public class OIDCAdvancedRequestParamsTest extends AbstractTestRealmKeycloakTest
|
||||||
events.expectLogin().assertEvent();
|
events.expectLogin().assertEvent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testWrongEncryptionAlgorithm() throws Exception {
|
||||||
|
try {
|
||||||
|
ClientResource clientResource = ApiUtil.findClientByClientId(adminClient.realm(oauth.getRealm()), oauth.getClientId());
|
||||||
|
ClientRepresentation clientRep = clientResource.toRepresentation();
|
||||||
|
OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setRequestObjectEncryptionAlg(RSA_OAEP_256);
|
||||||
|
clientResource.update(clientRep);
|
||||||
|
oauth.request(createEncryptedRequestObject(RSA_OAEP));
|
||||||
|
oauth.doLogin("test-user@localhost", "password");
|
||||||
|
fail("Should fail due to invalid encryption algorithm");
|
||||||
|
} catch (Exception ignore) {
|
||||||
|
assertTrue(errorPage.isCurrent());
|
||||||
|
oauth.request(createEncryptedRequestObject(RSA_OAEP_256));
|
||||||
|
oauth.doLogin("test-user@localhost", "password");
|
||||||
|
assertTrue(appPage.isCurrent());
|
||||||
|
} finally {
|
||||||
|
ClientResource clientResource = ApiUtil.findClientByClientId(adminClient.realm(oauth.getRealm()), oauth.getClientId());
|
||||||
|
ClientRepresentation clientRep = clientResource.toRepresentation();
|
||||||
|
OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setRequestObjectEncryptionAlg(null);
|
||||||
|
clientResource.update(clientRep);
|
||||||
|
}
|
||||||
|
|
||||||
|
oauth.openLogout();
|
||||||
|
oauth = oauth.request(createEncryptedRequestObject(RSA_OAEP_256));
|
||||||
|
oauth.doLogin("test-user@localhost", "password");
|
||||||
|
assertTrue(appPage.isCurrent());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testWrongContentEncryptionAlgorithm() throws Exception {
|
||||||
|
ClientResource clientResource = ApiUtil.findClientByClientId(adminClient.realm(oauth.getRealm()), oauth.getClientId());
|
||||||
|
ClientRepresentation clientRep = clientResource.toRepresentation();
|
||||||
|
|
||||||
|
try {
|
||||||
|
OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setRequestObjectEncryptionAlg(RSA_OAEP_256);
|
||||||
|
OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setRequestObjectEncryptionEnc(JWEConstants.A192GCM);
|
||||||
|
clientResource.update(clientRep);
|
||||||
|
clientRep = clientResource.toRepresentation();
|
||||||
|
assertEquals(JWEConstants.A192GCM, OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).getRequestObjectEncryptionEnc());
|
||||||
|
oauth.request(createEncryptedRequestObject(RSA_OAEP_256));
|
||||||
|
oauth.doLogin("test-user@localhost", "password");
|
||||||
|
fail("Should fail due to invalid content encryption algorithm");
|
||||||
|
} catch (Exception ignore) {
|
||||||
|
assertTrue(errorPage.isCurrent());
|
||||||
|
oauth.request(createEncryptedRequestObject(RSA_OAEP_256));
|
||||||
|
clientRep = clientResource.toRepresentation();
|
||||||
|
OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setRequestObjectEncryptionEnc(JWEConstants.A256GCM);
|
||||||
|
clientResource.update(clientRep);
|
||||||
|
clientRep = clientResource.toRepresentation();
|
||||||
|
assertEquals(JWEConstants.A256GCM, OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).getRequestObjectEncryptionEnc());
|
||||||
|
oauth.doLogin("test-user@localhost", "password");
|
||||||
|
assertTrue(appPage.isCurrent());
|
||||||
|
} finally {
|
||||||
|
clientResource = ApiUtil.findClientByClientId(adminClient.realm(oauth.getRealm()), oauth.getClientId());
|
||||||
|
clientRep = clientResource.toRepresentation();
|
||||||
|
OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setRequestObjectEncryptionAlg(null);
|
||||||
|
OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setRequestObjectEncryptionEnc(null);
|
||||||
|
clientResource.update(clientRep);
|
||||||
|
}
|
||||||
|
|
||||||
|
oauth.openLogout();
|
||||||
|
oauth = oauth.request(createEncryptedRequestObject(RSA_OAEP_256));
|
||||||
|
oauth.doLogin("test-user@localhost", "password");
|
||||||
|
assertTrue(appPage.isCurrent());
|
||||||
|
|
||||||
|
clientRep = clientResource.toRepresentation();
|
||||||
|
assertNull(OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).getRequestObjectEncryptionAlg());
|
||||||
|
assertNull(OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).getRequestObjectEncryptionEnc());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSignedAndEncryptedRequestObject() throws IOException, JWEException {
|
public void testSignedAndEncryptedRequestObject() throws IOException, JWEException {
|
||||||
|
oauth = oauth.request(createEncryptedRequestObject(RSA_OAEP_256));
|
||||||
|
oauth.doLogin("test-user@localhost", "password");
|
||||||
|
events.expectLogin().assertEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String createEncryptedRequestObject(String encAlg) throws IOException, JWEException {
|
||||||
try (CloseableHttpClient httpClient = HttpClientBuilder.create().build()) {
|
try (CloseableHttpClient httpClient = HttpClientBuilder.create().build()) {
|
||||||
OIDCConfigurationRepresentation representation = SimpleHttp
|
OIDCConfigurationRepresentation representation = SimpleHttp
|
||||||
.doGet(getAuthServerRoot().toString() + "realms/" + oauth.getRealm() + "/.well-known/openid-configuration",
|
.doGet(getAuthServerRoot().toString() + "realms/" + oauth.getRealm() + "/.well-known/openid-configuration",
|
||||||
|
@ -1308,22 +1385,21 @@ public class OIDCAdvancedRequestParamsTest extends AbstractTestRealmKeycloakTest
|
||||||
String keyId = null;
|
String keyId = null;
|
||||||
|
|
||||||
if (keyId == null) {
|
if (keyId == null) {
|
||||||
KeysMetadataRepresentation.KeyMetadataRepresentation encKey = KeyUtils.getActiveEncKey(testRealm().keys().getKeyMetadata(),
|
KeysMetadataRepresentation.KeyMetadataRepresentation encKey = KeyUtils
|
||||||
|
.getActiveEncKey(testRealm().keys().getKeyMetadata(),
|
||||||
org.keycloak.crypto.Algorithm.PS256);
|
org.keycloak.crypto.Algorithm.PS256);
|
||||||
keyId = encKey.getKid();
|
keyId = encKey.getKid();
|
||||||
}
|
}
|
||||||
|
|
||||||
PublicKey decryptionKEK = keysForUse.get(keyId);
|
PublicKey decryptionKEK = keysForUse.get(keyId);
|
||||||
JWE jwe = new JWE()
|
JWE jwe = new JWE()
|
||||||
.header(new JWEHeader(RSA_OAEP_256, JWEConstants.A256GCM, null))
|
.header(new JWEHeader(encAlg, JWEConstants.A256GCM, null))
|
||||||
.content(createAndSignRequestObject().getBytes());
|
.content(createAndSignRequestObject().getBytes());
|
||||||
|
|
||||||
jwe.getKeyStorage()
|
jwe.getKeyStorage()
|
||||||
.setEncryptionKey(decryptionKEK);
|
.setEncryptionKey(decryptionKEK);
|
||||||
|
|
||||||
oauth = oauth.request(jwe.encodeJwe());
|
return jwe.encodeJwe();
|
||||||
oauth.doLogin("test-user@localhost", "password");
|
|
||||||
events.expectLogin().assertEvent();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -168,9 +168,14 @@ public final class ClientPoliciesUtil {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static SecureRequestObjectExecutor.Configuration createSecureRequestObjectExecutorConfig(Integer availablePeriod, Boolean verifyNbf) {
|
public static SecureRequestObjectExecutor.Configuration createSecureRequestObjectExecutorConfig(Integer availablePeriod, Boolean verifyNbf) {
|
||||||
|
return createSecureRequestObjectExecutorConfig(availablePeriod, verifyNbf, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SecureRequestObjectExecutor.Configuration createSecureRequestObjectExecutorConfig(Integer availablePeriod, Boolean verifyNbf, Boolean encryptionRequired) {
|
||||||
SecureRequestObjectExecutor.Configuration config = new SecureRequestObjectExecutor.Configuration();
|
SecureRequestObjectExecutor.Configuration config = new SecureRequestObjectExecutor.Configuration();
|
||||||
if (availablePeriod != null) config.setAvailablePeriod(availablePeriod);
|
if (availablePeriod != null) config.setAvailablePeriod(availablePeriod);
|
||||||
if (verifyNbf != null) config.setVerifyNbf(verifyNbf);
|
if (verifyNbf != null) config.setVerifyNbf(verifyNbf);
|
||||||
|
if (encryptionRequired != null) config.setEncryptionRequired(encryptionRequired);
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -403,6 +403,10 @@ request-object-signature-alg=Request Object Signature Algorithm
|
||||||
request-object-signature-alg.tooltip=JWA algorithm, which client needs to use when sending OIDC request object specified by 'request' or 'request_uri' parameters. If set to 'any', Request object can be signed by any algorithm (including 'none' ).
|
request-object-signature-alg.tooltip=JWA algorithm, which client needs to use when sending OIDC request object specified by 'request' or 'request_uri' parameters. If set to 'any', Request object can be signed by any algorithm (including 'none' ).
|
||||||
request-object-required=Request Object Required
|
request-object-required=Request Object Required
|
||||||
request-object-required.tooltip=Specifies if the client needs to provide a request object with their authorization requests, and what method they can use for this. If set to "not required", providing a request object is optional. In all other cases, providing a request object is mandatory. If set to "request", the request object must be provided by value. If set to "request_uri", the request object must be provided by reference. If set to "request or request_uri", either method can be used.
|
request-object-required.tooltip=Specifies if the client needs to provide a request object with their authorization requests, and what method they can use for this. If set to "not required", providing a request object is optional. In all other cases, providing a request object is mandatory. If set to "request", the request object must be provided by value. If set to "request_uri", the request object must be provided by reference. If set to "request or request_uri", either method can be used.
|
||||||
|
request-object-encryption-alg=Request Object Encryption Algorithm
|
||||||
|
request-object-encryption-alg.tooltip=JWE algorithm, which client needs to use when sending OIDC request object specified by 'request' or 'request_uri' parameters. If set to 'any', encryption is optional and any algorithm is allowed.
|
||||||
|
request-object-encryption-enc=Request Object Content Encryption Algorithm
|
||||||
|
request-object-encryption-enc.tooltip=JWE algorithm, which client needs to use when encrypting the content of the OIDC request object specified by 'request' or 'request_uri' parameters. If set to 'any', any algorithm is allowed.
|
||||||
ciba-backchannel-auth-request-signing-alg=CIBA Backchannel Authentication Request Signature Algorithm
|
ciba-backchannel-auth-request-signing-alg=CIBA Backchannel Authentication Request Signature Algorithm
|
||||||
ciba-backchannel-auth-request-signing-alg.tooltip=JWA algorithm, which client needs to use when sending CIBA backchannel authentication request specified by 'request' or 'request_uri' parameters.
|
ciba-backchannel-auth-request-signing-alg.tooltip=JWA algorithm, which client needs to use when sending CIBA backchannel authentication request specified by 'request' or 'request_uri' parameters.
|
||||||
request-uris=Valid Request URIs
|
request-uris=Valid Request URIs
|
||||||
|
|
|
@ -1314,6 +1314,12 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, flows, $ro
|
||||||
var attrVal5 = $scope.client.attributes['ciba.backchannel.auth.request.signing.alg'];
|
var attrVal5 = $scope.client.attributes['ciba.backchannel.auth.request.signing.alg'];
|
||||||
$scope.cibaBackchannelAuthRequestSigningAlg = attrVal5==null ? 'none' : attrVal5;
|
$scope.cibaBackchannelAuthRequestSigningAlg = attrVal5==null ? 'none' : attrVal5;
|
||||||
|
|
||||||
|
var attrVal6 = $scope.client.attributes['request.object.encryption.alg'];
|
||||||
|
$scope.requestObjectEncryptionAlg = attrVal6==null ? 'any' : attrVal6;
|
||||||
|
|
||||||
|
var attrVal7 = $scope.client.attributes['request.object.encryption.enc'];
|
||||||
|
$scope.requestObjectEncryptionEnc = attrVal7==null ? 'any' : attrVal7;
|
||||||
|
|
||||||
if ($scope.client.attributes["exclude.session.state.from.auth.response"]) {
|
if ($scope.client.attributes["exclude.session.state.from.auth.response"]) {
|
||||||
if ($scope.client.attributes["exclude.session.state.from.auth.response"] == "true") {
|
if ($scope.client.attributes["exclude.session.state.from.auth.response"] == "true") {
|
||||||
$scope.excludeSessionStateFromAuthResponse = true;
|
$scope.excludeSessionStateFromAuthResponse = true;
|
||||||
|
@ -1527,6 +1533,22 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, flows, $ro
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.changeRequestObjectEncryptionAlg = function() {
|
||||||
|
if ($scope.requestObjectEncryptionAlg === 'any') {
|
||||||
|
$scope.clientEdit.attributes['request.object.encryption.alg'] = null;
|
||||||
|
} else {
|
||||||
|
$scope.clientEdit.attributes['request.object.encryption.alg'] = $scope.requestObjectEncryptionAlg;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.changeRequestObjectEncryptionEnc = function() {
|
||||||
|
if ($scope.requestObjectEncryptionEnc === 'any') {
|
||||||
|
$scope.clientEdit.attributes['request.object.encryption.enc'] = null;
|
||||||
|
} else {
|
||||||
|
$scope.clientEdit.attributes['request.object.encryption.enc'] = $scope.requestObjectEncryptionEnc;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
$scope.changePkceCodeChallengeMethod = function() {
|
$scope.changePkceCodeChallengeMethod = function() {
|
||||||
$scope.clientEdit.attributes['pkce.code.challenge.method'] = $scope.pkceCodeChallengeMethod;
|
$scope.clientEdit.attributes['pkce.code.challenge.method'] = $scope.pkceCodeChallengeMethod;
|
||||||
};
|
};
|
||||||
|
|
|
@ -559,6 +559,34 @@
|
||||||
</div>
|
</div>
|
||||||
<kc-tooltip>{{:: 'request-object-signature-alg.tooltip' | translate}}</kc-tooltip>
|
<kc-tooltip>{{:: 'request-object-signature-alg.tooltip' | translate}}</kc-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group clearfix block" data-ng-show="protocol == 'openid-connect'">
|
||||||
|
<label class="col-md-2 control-label" for="requestObjectEncryptionAlg">{{:: 'request-object-encryption-alg' | translate}}</label>
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<div>
|
||||||
|
<select class="form-control" id="requestObjectEncryptionAlg"
|
||||||
|
ng-change="changeRequestObjectEncryptionAlg()"
|
||||||
|
ng-model="requestObjectEncryptionAlg">
|
||||||
|
<option value="any">any</option>
|
||||||
|
<option ng-repeat="provider in serverInfo.listProviderIds('cekmanagement')" value="{{provider}}">{{provider}}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<kc-tooltip>{{:: 'request-object-encryption-alg.tooltip' | translate}}</kc-tooltip>
|
||||||
|
</div>
|
||||||
|
<div class="form-group clearfix block" data-ng-show="protocol == 'openid-connect'">
|
||||||
|
<label class="col-md-2 control-label" for="requestObjectEncryptionEnc">{{:: 'request-object-encryption-enc' | translate}}</label>
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<div>
|
||||||
|
<select class="form-control" id="requestObjectEncryptionEnc"
|
||||||
|
ng-change="changeRequestObjectEncryptionEnc()"
|
||||||
|
ng-model="requestObjectEncryptionEnc">
|
||||||
|
<option value="any">any</option>
|
||||||
|
<option ng-repeat="provider in serverInfo.listProviderIds('contentencryption')" value="{{provider}}">{{provider}}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<kc-tooltip>{{:: 'request-object-encryption-enc.tooltip' | translate}}</kc-tooltip>
|
||||||
|
</div>
|
||||||
<div class="form-group clearfix block" data-ng-show="protocol == 'openid-connect'">
|
<div class="form-group clearfix block" data-ng-show="protocol == 'openid-connect'">
|
||||||
<label class="col-md-2 control-label" for="changeRequestObjectRequired">{{:: 'request-object-required' | translate}}</label>
|
<label class="col-md-2 control-label" for="changeRequestObjectRequired">{{:: 'request-object-required' | translate}}</label>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
|
|
Loading…
Reference in a new issue