[#11036] Identity Providers: Add support for elliptic curve signatures (ES256/ES384/ES512) using JWKS URL
This commit is contained in:
parent
7d96f3ad5a
commit
711440e513
15 changed files with 307 additions and 40 deletions
|
@ -57,9 +57,13 @@ public final class DerUtils {
|
|||
}
|
||||
|
||||
public static PublicKey decodePublicKey(byte[] der) throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchProviderException {
|
||||
return decodePublicKey(der, "RSA");
|
||||
}
|
||||
|
||||
public static PublicKey decodePublicKey(byte[] der, String type) throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchProviderException {
|
||||
X509EncodedKeySpec spec =
|
||||
new X509EncodedKeySpec(der);
|
||||
KeyFactory kf = KeyFactory.getInstance("RSA", BouncyIntegration.PROVIDER);
|
||||
KeyFactory kf = KeyFactory.getInstance(type, BouncyIntegration.PROVIDER);
|
||||
return kf.generatePublic(spec);
|
||||
}
|
||||
|
||||
|
|
|
@ -73,13 +73,23 @@ public final class PemUtils {
|
|||
* @throws Exception
|
||||
*/
|
||||
public static PublicKey decodePublicKey(String pem) {
|
||||
return decodePublicKey(pem, "RSA");
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a Public Key from a PEM string
|
||||
* @param pem The pem encoded pblic key
|
||||
* @param type The type of the key (RSA, EC,...)
|
||||
* @return The public key or null
|
||||
*/
|
||||
public static PublicKey decodePublicKey(String pem, String type) {
|
||||
if (pem == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
byte[] der = pemToDer(pem);
|
||||
return DerUtils.decodePublicKey(der);
|
||||
return DerUtils.decodePublicKey(der, type);
|
||||
} catch (Exception e) {
|
||||
throw new PemException(e);
|
||||
}
|
||||
|
|
|
@ -21,10 +21,28 @@ import org.keycloak.provider.Provider;
|
|||
|
||||
public interface SignatureProvider extends Provider {
|
||||
|
||||
static void checkKeyForSignature(KeyWrapper key, String algorithm, String type) throws SignatureException {
|
||||
if (!type.equals(key.getType()) || !algorithm.equals(key.getAlgorithmOrDefault())) {
|
||||
throw new SignatureException(String.format("Key with algorithm %s and type %s is incorrect for provider algorithm %s",
|
||||
key.getAlgorithm(), key.getType(), algorithm));
|
||||
}
|
||||
}
|
||||
|
||||
static void checkKeyForVerification(KeyWrapper key, String algorithm, String type) throws VerificationException {
|
||||
if (!type.equals(key.getType()) || !algorithm.equals(key.getAlgorithmOrDefault())) {
|
||||
throw new VerificationException(String.format("Key with algorithm %s and type %s is incorrect for provider algorithm %s",
|
||||
key.getAlgorithm(), key.getType(), algorithm));
|
||||
}
|
||||
}
|
||||
|
||||
SignatureSignerContext signer() throws SignatureException;
|
||||
|
||||
SignatureSignerContext signer(KeyWrapper key) throws SignatureException;
|
||||
|
||||
SignatureVerifierContext verifier(String kid) throws VerificationException;
|
||||
|
||||
SignatureVerifierContext verifier(KeyWrapper key) throws VerificationException;
|
||||
|
||||
boolean isAsymmetricAlgorithm();
|
||||
|
||||
@Override
|
||||
|
|
|
@ -30,13 +30,14 @@ import org.keycloak.broker.provider.util.SimpleHttp;
|
|||
import org.keycloak.common.util.Base64Url;
|
||||
import org.keycloak.common.util.SecretGenerator;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.crypto.KeyWrapper;
|
||||
import org.keycloak.crypto.SignatureProvider;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.jose.jws.JWSInput;
|
||||
import org.keycloak.jose.jws.JWSInputException;
|
||||
import org.keycloak.jose.jws.crypto.RSAProvider;
|
||||
import org.keycloak.keys.loader.PublicKeyStorageManager;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.FederatedIdentityModel;
|
||||
|
@ -44,7 +45,6 @@ import org.keycloak.models.KeycloakSession;
|
|||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.representations.AccessTokenResponse;
|
||||
import org.keycloak.representations.IDToken;
|
||||
|
@ -70,7 +70,7 @@ import javax.ws.rs.core.UriBuilder;
|
|||
import javax.ws.rs.core.UriInfo;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.PublicKey;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* @author Pedro Igor
|
||||
|
@ -530,9 +530,21 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
|||
if (!getConfig().isValidateSignature()) return true;
|
||||
|
||||
try {
|
||||
PublicKey publicKey = PublicKeyStorageManager.getIdentityProviderPublicKey(session, session.getContext().getRealm(), getConfig(), jws);
|
||||
KeyWrapper key = PublicKeyStorageManager.getIdentityProviderKeyWrapper(session, session.getContext().getRealm(), getConfig(), jws);
|
||||
if (key == null) {
|
||||
logger.debugf("Failed to verify token, key not found for algorithm %s", jws.getHeader().getRawAlgorithm());
|
||||
return false;
|
||||
}
|
||||
if (key.getAlgorithm() == null) {
|
||||
key.setAlgorithm(jws.getHeader().getRawAlgorithm());
|
||||
}
|
||||
SignatureProvider signatureProvider = session.getProvider(SignatureProvider.class, jws.getHeader().getRawAlgorithm());
|
||||
if (signatureProvider == null) {
|
||||
logger.debugf("Failed to verify token, signature provider not found for algorithm %s", jws.getHeader().getRawAlgorithm());
|
||||
return false;
|
||||
}
|
||||
|
||||
return publicKey != null && RSAProvider.verify(jws, publicKey);
|
||||
return signatureProvider.verifier(key).verify(jws.getEncodedSignatureInput().getBytes(StandardCharsets.UTF_8), jws.getSignature());
|
||||
} catch (Exception e) {
|
||||
logger.debug("Failed to verify token", e);
|
||||
return false;
|
||||
|
|
|
@ -34,11 +34,23 @@ public class AsymmetricSignatureProvider implements SignatureProvider {
|
|||
return new ServerAsymmetricSignatureSignerContext(session, algorithm);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SignatureSignerContext signer(KeyWrapper key) throws SignatureException {
|
||||
SignatureProvider.checkKeyForSignature(key, algorithm, KeyType.RSA);
|
||||
return new ServerAsymmetricSignatureSignerContext(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SignatureVerifierContext verifier(String kid) throws VerificationException {
|
||||
return new ServerAsymmetricSignatureVerifierContext(session, kid, algorithm);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SignatureVerifierContext verifier(KeyWrapper key) throws VerificationException {
|
||||
SignatureProvider.checkKeyForVerification(key, algorithm, KeyType.RSA);
|
||||
return new ServerAsymmetricSignatureVerifierContext(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAsymmetricAlgorithm() {
|
||||
return true;
|
||||
|
|
|
@ -28,11 +28,23 @@ public class ECDSASignatureProvider implements SignatureProvider {
|
|||
return new ServerECDSASignatureSignerContext(session, algorithm);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SignatureSignerContext signer(KeyWrapper key) throws SignatureException {
|
||||
SignatureProvider.checkKeyForSignature(key, algorithm, KeyType.EC);
|
||||
return new ServerECDSASignatureSignerContext(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SignatureVerifierContext verifier(String kid) throws VerificationException {
|
||||
return new ServerECDSASignatureVerifierContext(session, kid, algorithm);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SignatureVerifierContext verifier(KeyWrapper key) throws VerificationException {
|
||||
SignatureProvider.checkKeyForVerification(key, algorithm, KeyType.EC);
|
||||
return new ServerECDSASignatureVerifierContext(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAsymmetricAlgorithm() {
|
||||
return true;
|
||||
|
|
|
@ -34,11 +34,23 @@ public class MacSecretSignatureProvider implements SignatureProvider {
|
|||
return new ServerMacSignatureSignerContext(session, algorithm);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SignatureSignerContext signer(KeyWrapper key) throws SignatureException {
|
||||
SignatureProvider.checkKeyForSignature(key, algorithm, KeyType.OCT);
|
||||
return new ServerMacSignatureSignerContext(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SignatureVerifierContext verifier(String kid) throws VerificationException {
|
||||
return new ServerMacSignatureVerifierContext(session, kid, algorithm);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SignatureVerifierContext verifier(KeyWrapper key) throws VerificationException {
|
||||
SignatureProvider.checkKeyForVerification(key, algorithm, KeyType.OCT);
|
||||
return new ServerMacSignatureVerifierContext(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAsymmetricAlgorithm() {
|
||||
return false;
|
||||
|
|
|
@ -24,6 +24,10 @@ public class ServerAsymmetricSignatureSignerContext extends AsymmetricSignatureS
|
|||
super(getKey(session, algorithm));
|
||||
}
|
||||
|
||||
public ServerAsymmetricSignatureSignerContext(KeyWrapper key) throws SignatureException {
|
||||
super(key);
|
||||
}
|
||||
|
||||
static KeyWrapper getKey(KeycloakSession session, String algorithm) {
|
||||
KeyWrapper key = session.keys().getActiveKey(session.getContext().getRealm(), KeyUse.SIG, algorithm);
|
||||
if (key == null) {
|
||||
|
|
|
@ -25,6 +25,10 @@ public class ServerAsymmetricSignatureVerifierContext extends AsymmetricSignatur
|
|||
super(getKey(session, kid, algorithm));
|
||||
}
|
||||
|
||||
public ServerAsymmetricSignatureVerifierContext(KeyWrapper key) throws VerificationException {
|
||||
super(key);
|
||||
}
|
||||
|
||||
static KeyWrapper getKey(KeycloakSession session, String kid, String algorithm) throws VerificationException {
|
||||
KeyWrapper key = session.keys().getKey(session.getContext().getRealm(), kid, KeyUse.SIG, algorithm);
|
||||
if (key == null) {
|
||||
|
|
|
@ -24,6 +24,10 @@ public class ServerMacSignatureSignerContext extends MacSignatureSignerContext {
|
|||
super(getKey(session, algorithm));
|
||||
}
|
||||
|
||||
public ServerMacSignatureSignerContext(KeyWrapper key) throws SignatureException {
|
||||
super(key);
|
||||
}
|
||||
|
||||
private static KeyWrapper getKey(KeycloakSession session, String algorithm) {
|
||||
KeyWrapper key = session.keys().getActiveKey(session.getContext().getRealm(), KeyUse.SIG, algorithm);
|
||||
if (key == null) {
|
||||
|
|
|
@ -25,6 +25,10 @@ public class ServerMacSignatureVerifierContext extends MacSignatureVerifierConte
|
|||
super(getKey(session, kid, algorithm));
|
||||
}
|
||||
|
||||
public ServerMacSignatureVerifierContext(KeyWrapper key) throws VerificationException {
|
||||
super(key);
|
||||
}
|
||||
|
||||
private static KeyWrapper getKey(KeycloakSession session, String kid, String algorithm) throws VerificationException {
|
||||
KeyWrapper key = session.keys().getKey(session.getContext().getRealm(), kid, KeyUse.SIG, algorithm);
|
||||
if (key == null) {
|
||||
|
|
|
@ -16,8 +16,11 @@
|
|||
*/
|
||||
package org.keycloak.keys.loader;
|
||||
|
||||
import org.keycloak.common.util.Base64Url;
|
||||
import org.keycloak.common.util.KeyUtils;
|
||||
import org.keycloak.common.util.PemUtils;
|
||||
import org.keycloak.crypto.Algorithm;
|
||||
import org.keycloak.crypto.JavaAlgorithm;
|
||||
import org.keycloak.crypto.KeyType;
|
||||
import org.keycloak.crypto.KeyUse;
|
||||
import org.keycloak.crypto.KeyWrapper;
|
||||
|
@ -32,29 +35,41 @@ import java.util.Map;
|
|||
*/
|
||||
public class HardcodedPublicKeyLoader implements PublicKeyLoader {
|
||||
|
||||
private final String kid;
|
||||
private final String pem;
|
||||
private final KeyWrapper keyWrapper;
|
||||
|
||||
public HardcodedPublicKeyLoader(String kid, String pem) {
|
||||
this.kid = kid;
|
||||
this.pem = pem;
|
||||
this(kid, pem, Algorithm.RS256);
|
||||
}
|
||||
|
||||
public HardcodedPublicKeyLoader(String kid, String encodedKey, String algorithm) {
|
||||
if (encodedKey != null && !encodedKey.trim().isEmpty()) {
|
||||
keyWrapper = new KeyWrapper();
|
||||
keyWrapper.setKid(kid);
|
||||
keyWrapper.setUse(KeyUse.SIG);
|
||||
// depending the algorithm load the correct key from the encoded string
|
||||
if (JavaAlgorithm.isRSAJavaAlgorithm(algorithm)) {
|
||||
keyWrapper.setType(KeyType.RSA);
|
||||
keyWrapper.setPublicKey(PemUtils.decodePublicKey(encodedKey, KeyType.RSA));
|
||||
} else if (JavaAlgorithm.isECJavaAlgorithm(algorithm)) {
|
||||
keyWrapper.setType(KeyType.EC);
|
||||
keyWrapper.setPublicKey(PemUtils.decodePublicKey(encodedKey, KeyType.EC));
|
||||
} else if (JavaAlgorithm.isHMACJavaAlgorithm(algorithm)) {
|
||||
keyWrapper.setType(KeyType.OCT);
|
||||
keyWrapper.setSecretKey(KeyUtils.loadSecretKey(Base64Url.decode(encodedKey), algorithm));
|
||||
}
|
||||
} else {
|
||||
keyWrapper = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, KeyWrapper> loadKeys() throws Exception {
|
||||
return Collections.unmodifiableMap(Collections.singletonMap(kid, getSavedPublicKey()));
|
||||
return keyWrapper != null
|
||||
? Collections.unmodifiableMap(Collections.singletonMap(keyWrapper.getKid(), getSavedPublicKey()))
|
||||
: Collections.emptyMap();
|
||||
}
|
||||
|
||||
protected KeyWrapper getSavedPublicKey() {
|
||||
KeyWrapper keyWrapper = null;
|
||||
if (pem != null && ! pem.trim().equals("")) {
|
||||
keyWrapper = new KeyWrapper();
|
||||
keyWrapper.setKid(kid);
|
||||
keyWrapper.setType(KeyType.RSA);
|
||||
keyWrapper.setAlgorithm(Algorithm.RS256);
|
||||
keyWrapper.setUse(KeyUse.SIG);
|
||||
keyWrapper.setPublicKey(PemUtils.decodePublicKey(pem));
|
||||
}
|
||||
return keyWrapper;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -62,11 +62,12 @@ public class PublicKeyStorageManager {
|
|||
return keyStorage.getFirstPublicKey(modelKey, algAlgorithm, loader);
|
||||
}
|
||||
|
||||
public static PublicKey getIdentityProviderPublicKey(KeycloakSession session, RealmModel realm, OIDCIdentityProviderConfig idpConfig, JWSInput input) {
|
||||
public static KeyWrapper getIdentityProviderKeyWrapper(KeycloakSession session, RealmModel realm, OIDCIdentityProviderConfig idpConfig, JWSInput input) {
|
||||
boolean keyIdSetInConfiguration = idpConfig.getPublicKeySignatureVerifierKeyId() != null
|
||||
&& ! idpConfig.getPublicKeySignatureVerifierKeyId().trim().isEmpty();
|
||||
|
||||
String kid = input.getHeader().getKeyId();
|
||||
String alg = input.getHeader().getRawAlgorithm();
|
||||
|
||||
PublicKeyStorageProvider keyStorage = session.getProvider(PublicKeyStorageProvider.class);
|
||||
|
||||
|
@ -85,9 +86,14 @@ public class PublicKeyStorageManager {
|
|||
loader = new HardcodedPublicKeyLoader(
|
||||
keyIdSetInConfiguration
|
||||
? idpConfig.getPublicKeySignatureVerifierKeyId().trim()
|
||||
: kid, pem);
|
||||
: kid, pem, alg);
|
||||
}
|
||||
|
||||
return (PublicKey)keyStorage.getPublicKey(modelKey, kid, loader).getPublicKey();
|
||||
return keyStorage.getPublicKey(modelKey, kid, loader);
|
||||
}
|
||||
|
||||
public static PublicKey getIdentityProviderPublicKey(KeycloakSession session, RealmModel realm, OIDCIdentityProviderConfig idpConfig, JWSInput input) {
|
||||
KeyWrapper key = getIdentityProviderKeyWrapper(session, realm, idpConfig, input);
|
||||
return key != null? (PublicKey) key.getPublicKey() : null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,7 +23,6 @@ import org.keycloak.admin.client.resource.ClientScopeResource;
|
|||
import org.keycloak.admin.client.resource.RealmResource;
|
||||
import org.keycloak.admin.client.resource.RoleResource;
|
||||
import org.keycloak.admin.client.resource.UserResource;
|
||||
import org.keycloak.crypto.Algorithm;
|
||||
import org.keycloak.crypto.KeyStatus;
|
||||
import org.keycloak.crypto.KeyUse;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
|
@ -280,4 +279,13 @@ public class ApiUtil {
|
|||
return null;
|
||||
}
|
||||
|
||||
public static KeysMetadataRepresentation.KeyMetadataRepresentation findActiveSigningKey(RealmResource realm, String alg) {
|
||||
KeysMetadataRepresentation keyMetadata = realm.keys().getKeyMetadata();
|
||||
for (KeysMetadataRepresentation.KeyMetadataRepresentation rep : keyMetadata.getKeys()) {
|
||||
if (rep.getPublicKey() != null && KeyStatus.valueOf(rep.getStatus()).isActive() && KeyUse.SIG.equals(rep.getUse()) && alg.equals(rep.getAlgorithm())) {
|
||||
return rep;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,8 @@
|
|||
|
||||
package org.keycloak.testsuite.broker;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriBuilder;
|
||||
|
||||
|
@ -29,6 +31,8 @@ import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
|
|||
import org.keycloak.crypto.Algorithm;
|
||||
import org.keycloak.keys.KeyProvider;
|
||||
import org.keycloak.keys.PublicKeyStorageUtils;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
|
||||
import org.keycloak.representations.idm.ComponentRepresentation;
|
||||
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
||||
|
@ -37,6 +41,7 @@ import org.keycloak.representations.idm.UserRepresentation;
|
|||
import org.keycloak.testsuite.Assert;
|
||||
import org.keycloak.testsuite.admin.ApiUtil;
|
||||
import org.keycloak.testsuite.client.resources.TestingCacheResource;
|
||||
import org.keycloak.testsuite.updaters.ClientAttributeUpdater;
|
||||
import org.keycloak.testsuite.util.OAuthClient;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
@ -100,7 +105,7 @@ public class KcOIDCBrokerWithSignatureTest extends AbstractBaseBrokerTest {
|
|||
logoutFromRealm(getConsumerRoot(), bc.consumerRealmName());
|
||||
|
||||
// Rotate public keys on the parent broker
|
||||
rotateKeys();
|
||||
rotateKeys(Algorithm.RS256, "rsa-generated");
|
||||
|
||||
// User not able to login now as new keys can't be yet downloaded (10s timeout)
|
||||
logInAsUserInIDP();
|
||||
|
@ -137,7 +142,7 @@ public class KcOIDCBrokerWithSignatureTest extends AbstractBaseBrokerTest {
|
|||
cfg.setValidateSignature(true);
|
||||
cfg.setUseJwksUrl(false);
|
||||
|
||||
KeysMetadataRepresentation.KeyMetadataRepresentation key = ApiUtil.findActiveSigningKey(providerRealm());
|
||||
KeysMetadataRepresentation.KeyMetadataRepresentation key = ApiUtil.findActiveSigningKey(providerRealm(), Algorithm.RS256);
|
||||
cfg.setPublicKeySignatureVerifier(key.getPublicKey());
|
||||
updateIdentityProvider(idpRep);
|
||||
|
||||
|
@ -148,7 +153,7 @@ public class KcOIDCBrokerWithSignatureTest extends AbstractBaseBrokerTest {
|
|||
logoutFromRealm(getConsumerRoot(), bc.consumerRealmName());
|
||||
|
||||
// Rotate public keys on the parent broker
|
||||
rotateKeys();
|
||||
rotateKeys(Algorithm.RS256, "rsa-generated");
|
||||
|
||||
// User not able to login now as new keys can't be yet downloaded (10s timeout)
|
||||
logInAsUserInIDP();
|
||||
|
@ -163,6 +168,89 @@ public class KcOIDCBrokerWithSignatureTest extends AbstractBaseBrokerTest {
|
|||
assertErrorPage("Unexpected error when authenticating with identity provider");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSignatureVerificationHardcodedPublicKeyES256() throws Exception {
|
||||
IdentityProviderRepresentation idpRep = getIdentityProvider();
|
||||
OIDCIdentityProviderConfigRep cfg = new OIDCIdentityProviderConfigRep(idpRep);
|
||||
cfg.setValidateSignature(true);
|
||||
cfg.setUseJwksUrl(false);
|
||||
|
||||
rotateKeys(Algorithm.ES256, "ecdsa-generated");
|
||||
|
||||
KeysMetadataRepresentation.KeyMetadataRepresentation key = ApiUtil.findActiveSigningKey(providerRealm(), Algorithm.ES256);
|
||||
cfg.setPublicKeySignatureVerifier(key.getPublicKey());
|
||||
updateIdentityProvider(idpRep);
|
||||
|
||||
try (Closeable clientUpdater = ClientAttributeUpdater.forClient(adminClient, bc.providerRealmName(), bc.getIDPClientIdInProviderRealm())
|
||||
.setAttribute(OIDCConfigAttributes.ACCESS_TOKEN_SIGNED_RESPONSE_ALG, Algorithm.ES256)
|
||||
.setAttribute(OIDCConfigAttributes.AUTHORIZATION_SIGNED_RESPONSE_ALG, Algorithm.ES256)
|
||||
.setAttribute(OIDCConfigAttributes.ID_TOKEN_SIGNED_RESPONSE_ALG, Algorithm.ES256)
|
||||
.update()) {
|
||||
|
||||
logInAsUserInIDPForFirstTime();
|
||||
assertLoggedInAccountManagement();
|
||||
logoutFromRealm(getConsumerRoot(), bc.consumerRealmName());
|
||||
|
||||
logInAsUserInIDP();
|
||||
logoutFromRealm(getConsumerRoot(), bc.consumerRealmName());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSignatureVerificationHardcodedPublicKeyPS512() throws Exception {
|
||||
IdentityProviderRepresentation idpRep = getIdentityProvider();
|
||||
OIDCIdentityProviderConfigRep cfg = new OIDCIdentityProviderConfigRep(idpRep);
|
||||
cfg.setValidateSignature(true);
|
||||
cfg.setUseJwksUrl(false);
|
||||
|
||||
rotateKeys(Algorithm.PS512, "rsa-generated");
|
||||
|
||||
KeysMetadataRepresentation.KeyMetadataRepresentation key = ApiUtil.findActiveSigningKey(providerRealm(), Algorithm.PS512);
|
||||
cfg.setPublicKeySignatureVerifier(key.getPublicKey());
|
||||
updateIdentityProvider(idpRep);
|
||||
|
||||
try (Closeable clientUpdater = ClientAttributeUpdater.forClient(adminClient, bc.providerRealmName(), bc.getIDPClientIdInProviderRealm())
|
||||
.setAttribute(OIDCConfigAttributes.ACCESS_TOKEN_SIGNED_RESPONSE_ALG, Algorithm.PS512)
|
||||
.setAttribute(OIDCConfigAttributes.AUTHORIZATION_SIGNED_RESPONSE_ALG, Algorithm.PS512)
|
||||
.setAttribute(OIDCConfigAttributes.ID_TOKEN_SIGNED_RESPONSE_ALG, Algorithm.PS512)
|
||||
.update()) {
|
||||
|
||||
logInAsUserInIDPForFirstTime();
|
||||
assertLoggedInAccountManagement();
|
||||
logoutFromRealm(getConsumerRoot(), bc.consumerRealmName());
|
||||
|
||||
logInAsUserInIDP();
|
||||
logoutFromRealm(getConsumerRoot(), bc.consumerRealmName());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSignatureVerificationHardcodedPublicKeyHS512() throws Exception {
|
||||
IdentityProviderRepresentation idpRep = getIdentityProvider();
|
||||
OIDCIdentityProviderConfigRep cfg = new OIDCIdentityProviderConfigRep(idpRep);
|
||||
cfg.setValidateSignature(true);
|
||||
cfg.setUseJwksUrl(false);
|
||||
|
||||
String base64secret = Base64Url.encode("01234567890123456789012345678912".getBytes(StandardCharsets.UTF_8));
|
||||
createHSKey(Algorithm.HS512, "32", base64secret);
|
||||
cfg.setPublicKeySignatureVerifier(base64secret);
|
||||
updateIdentityProvider(idpRep);
|
||||
|
||||
try (Closeable clientUpdater = ClientAttributeUpdater.forClient(adminClient, bc.providerRealmName(), bc.getIDPClientIdInProviderRealm())
|
||||
.setAttribute(OIDCConfigAttributes.ACCESS_TOKEN_SIGNED_RESPONSE_ALG, Algorithm.HS512)
|
||||
.setAttribute(OIDCConfigAttributes.AUTHORIZATION_SIGNED_RESPONSE_ALG, Algorithm.HS512)
|
||||
.setAttribute(OIDCConfigAttributes.ID_TOKEN_SIGNED_RESPONSE_ALG, Algorithm.HS512)
|
||||
.update()) {
|
||||
|
||||
logInAsUserInIDPForFirstTime();
|
||||
assertLoggedInAccountManagement();
|
||||
logoutFromRealm(getConsumerRoot(), bc.consumerRealmName());
|
||||
|
||||
logInAsUserInIDP();
|
||||
logoutFromRealm(getConsumerRoot(), bc.consumerRealmName());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSignatureVerificationHardcodedPublicKeyWithKeyIdSetExplicitly() throws Exception {
|
||||
// Configure OIDC identity provider with JWKS URL
|
||||
|
@ -171,7 +259,7 @@ public class KcOIDCBrokerWithSignatureTest extends AbstractBaseBrokerTest {
|
|||
cfg.setValidateSignature(true);
|
||||
cfg.setUseJwksUrl(false);
|
||||
|
||||
KeysMetadataRepresentation.KeyMetadataRepresentation key = ApiUtil.findActiveSigningKey(providerRealm());
|
||||
KeysMetadataRepresentation.KeyMetadataRepresentation key = ApiUtil.findActiveSigningKey(providerRealm(), Algorithm.RS256);
|
||||
String pemData = key.getPublicKey();
|
||||
cfg.setPublicKeySignatureVerifier(pemData);
|
||||
String expectedKeyId = KeyUtils.createKeyId(PemUtils.decodePublicKey(pemData));
|
||||
|
@ -269,28 +357,86 @@ public class KcOIDCBrokerWithSignatureTest extends AbstractBaseBrokerTest {
|
|||
assertErrorPage("Unexpected error when authenticating with identity provider");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSignatureVerificationJwksES256() throws Exception {
|
||||
updateIdentityProviderWithJwksUrl();
|
||||
rotateKeys(Algorithm.ES256, "ecdsa-generated");
|
||||
try (Closeable clientUpdater = ClientAttributeUpdater.forClient(adminClient, bc.providerRealmName(), bc.getIDPClientIdInProviderRealm())
|
||||
.setAttribute(OIDCConfigAttributes.ACCESS_TOKEN_SIGNED_RESPONSE_ALG, Algorithm.ES256)
|
||||
.setAttribute(OIDCConfigAttributes.AUTHORIZATION_SIGNED_RESPONSE_ALG, Algorithm.ES256)
|
||||
.setAttribute(OIDCConfigAttributes.ID_TOKEN_SIGNED_RESPONSE_ALG, Algorithm.ES256)
|
||||
.update()) {
|
||||
|
||||
// Check that user is able to login with ES256
|
||||
logInAsUserInIDPForFirstTime();
|
||||
assertLoggedInAccountManagement();
|
||||
logoutFromRealm(getConsumerRoot(), bc.consumerRealmName());
|
||||
|
||||
private void rotateKeys() {
|
||||
String activeKid = providerRealm().keys().getKeyMetadata().getActive().get(Algorithm.RS256);
|
||||
logInAsUserInIDP();
|
||||
logoutFromRealm(getConsumerRoot(), bc.consumerRealmName());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSignatureVerificationJwksPS512() throws Exception {
|
||||
updateIdentityProviderWithJwksUrl();
|
||||
try (Closeable clientUpdater = ClientAttributeUpdater.forClient(adminClient, bc.providerRealmName(), bc.getIDPClientIdInProviderRealm())
|
||||
.setAttribute(OIDCConfigAttributes.ACCESS_TOKEN_SIGNED_RESPONSE_ALG, Algorithm.PS512)
|
||||
.setAttribute(OIDCConfigAttributes.AUTHORIZATION_SIGNED_RESPONSE_ALG, Algorithm.PS512)
|
||||
.setAttribute(OIDCConfigAttributes.ID_TOKEN_SIGNED_RESPONSE_ALG, Algorithm.PS512)
|
||||
.update()) {
|
||||
|
||||
// Check that user is able to login with PS512
|
||||
logInAsUserInIDPForFirstTime();
|
||||
assertLoggedInAccountManagement();
|
||||
logoutFromRealm(getConsumerRoot(), bc.consumerRealmName());
|
||||
|
||||
logInAsUserInIDP();
|
||||
logoutFromRealm(getConsumerRoot(), bc.consumerRealmName());
|
||||
}
|
||||
}
|
||||
|
||||
private void rotateKeys(String algorithm, String providerId) {
|
||||
String activeKid = providerRealm().keys().getKeyMetadata().getActive().get(algorithm);
|
||||
|
||||
// Rotate public keys on the parent broker
|
||||
String realmId = providerRealm().toRepresentation().getId();
|
||||
ComponentRepresentation keys = new ComponentRepresentation();
|
||||
keys.setName("generated");
|
||||
keys.setProviderType(KeyProvider.class.getName());
|
||||
keys.setProviderId("rsa-generated");
|
||||
keys.setProviderId(providerId);
|
||||
keys.setParentId(realmId);
|
||||
keys.setConfig(new MultivaluedHashMap<>());
|
||||
keys.getConfig().putSingle("priority", Long.toString(System.currentTimeMillis()));
|
||||
Response response = providerRealm().components().add(keys);
|
||||
keys.getConfig().putSingle("algorithm", algorithm);
|
||||
try (Response response = providerRealm().components().add(keys)) {
|
||||
assertEquals(201, response.getStatus());
|
||||
response.close();
|
||||
}
|
||||
|
||||
String updatedActiveKid = providerRealm().keys().getKeyMetadata().getActive().get(Algorithm.RS256);
|
||||
String updatedActiveKid = providerRealm().keys().getKeyMetadata().getActive().get(algorithm);
|
||||
assertNotEquals(activeKid, updatedActiveKid);
|
||||
}
|
||||
|
||||
private void createHSKey(String algorithm, String size, String secret) {
|
||||
String realmId = providerRealm().toRepresentation().getId();
|
||||
ComponentRepresentation keys = new ComponentRepresentation();
|
||||
keys.setName("generated");
|
||||
keys.setProviderType(KeyProvider.class.getName());
|
||||
keys.setProviderId("hmac-generated");
|
||||
keys.setParentId(realmId);
|
||||
keys.setConfig(new MultivaluedHashMap<>());
|
||||
keys.getConfig().putSingle("priority", Long.toString(System.currentTimeMillis()));
|
||||
keys.getConfig().putSingle("algorithm", algorithm);
|
||||
keys.getConfig().putSingle("secretSize", size);
|
||||
keys.getConfig().putSingle("kid", KeycloakModelUtils.generateId());
|
||||
keys.getConfig().putSingle("secret", secret);
|
||||
try (Response response = providerRealm().components().add(keys)) {
|
||||
assertEquals(201, response.getStatus());
|
||||
}
|
||||
|
||||
String updatedActiveKid = providerRealm().keys().getKeyMetadata().getActive().get(algorithm);
|
||||
Assert.assertNotNull(updatedActiveKid);
|
||||
}
|
||||
|
||||
private RealmResource providerRealm() {
|
||||
return adminClient.realm(bc.providerRealmName());
|
||||
|
@ -307,8 +453,4 @@ public class KcOIDCBrokerWithSignatureTest extends AbstractBaseBrokerTest {
|
|||
private RealmResource consumerRealm() {
|
||||
return adminClient.realm(bc.consumerRealmName());
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue