KEYCLOAK-7560 Refactor Token Sign and Verify by Token Signature SPI

This commit is contained in:
Takashi Norimatsu authored and Stian Thorgersen committed 2018-09-11 08:14:10 +02:00
1 parent bd4098191b
commit 5b6036525c
47 files changed
+1979 -83

No files matched your search

@@ -0,0 +1,36 @@
package org.keycloak.jose.jws;
import java.security.Key;
import java.security.Signature;
import org.jboss.logging.Logger;
import org.keycloak.component.ComponentModel;
import org.keycloak.crypto.JavaAlgorithm;
import org.keycloak.models.KeycloakSession;
import org.keycloak.jose.jws.JWSSignatureProvider;
// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI
public abstract class AbstractTokenSignatureProvider implements TokenSignatureProvider, JWSSignatureProvider {
protected static final Logger logger = Logger.getLogger(AbstractTokenSignatureProvider.class);
public AbstractTokenSignatureProvider(KeycloakSession session, ComponentModel model) {}
@Override
public void close() {}
@Override
public abstract byte[] sign(byte[] data, String sigAlgName, Key key);
@Override
public abstract boolean verify(JWSInput jws, Key verifyKey);
protected Signature getSignature(String sigAlgName) {
try {
return Signature.getInstance(JavaAlgorithm.getJavaAlgorithm(sigAlgName));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
@@ -0,0 +1,69 @@
package org.keycloak.jose.jws;
import java.security.Key;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.KeycloakSession;
// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI
public class EcdsaTokenSignatureProvider extends AbstractTokenSignatureProvider {
public EcdsaTokenSignatureProvider(KeycloakSession session, ComponentModel model) {
super(session, model);
}
@Override
public void close() {}
@Override
public byte[] sign(byte[] data, String sigAlgName, Key key) {
try {
PrivateKey privateKey = (PrivateKey)key;
Signature signature = getSignature(sigAlgName);
signature.initSign(privateKey);
signature.update(data);
return signature.sign();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public boolean verify(JWSInput jws, Key verifyKey) {
try {
PublicKey publicKey = (PublicKey)verifyKey;
Signature verifier = getSignature(jws.getHeader().getAlgorithm().name());
verifier.initVerify(publicKey);
verifier.update(jws.getEncodedSignatureInput().getBytes("UTF-8"));
return verifier.verify(jws.getSignature());
} catch (Exception e) {
return false;
}
}
@Override
protected Signature getSignature(String sigAlgName) {
try {
return Signature.getInstance(getJavaAlgorithm(sigAlgName));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private String getJavaAlgorithm(String sigAlgName) {
switch (sigAlgName) {
case "ES256":
return "SHA256withECDSA";
case "ES384":
return "SHA384withECDSA";
case "ES512":
return "SHA512withECDSA";
default:
throw new IllegalArgumentException("Not an ECDSA Algorithm");
}
}
}
@@ -0,0 +1,54 @@
package org.keycloak.jose.jws;
import java.util.List;
import org.keycloak.Config.Scope;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderConfigurationBuilder;
// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI
@SuppressWarnings("rawtypes")
public class EcdsaTokenSignatureProviderFactory implements TokenSignatureProviderFactory {
public static final String ID = "ecdsa-signature";
private static final String HELP_TEXT = "Generates token signature provider using EC key";
private static final List<ProviderConfigProperty> CONFIG_PROPERTIES = ProviderConfigurationBuilder.create().build();
@Override
public void init(Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
@Override
public String getId() {
return ID;
}
@Override
public String getHelpText() {
return HELP_TEXT;
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return CONFIG_PROPERTIES;
}
@Override
public TokenSignatureProvider create(KeycloakSession session, ComponentModel model) {
return new EcdsaTokenSignatureProvider(session, model);
}
}
@@ -0,0 +1,52 @@
package org.keycloak.jose.jws;
import java.security.Key;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import javax.crypto.Mac;
import org.keycloak.common.util.Base64Url;
import org.keycloak.component.ComponentModel;
import org.keycloak.crypto.JavaAlgorithm;
import org.keycloak.models.KeycloakSession;
// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI
public class HmacTokenSignatureProvider extends AbstractTokenSignatureProvider {
public HmacTokenSignatureProvider(KeycloakSession session, ComponentModel model) {
super(session, model);
}
private Mac getMAC(final String sigAlgName) {
try {
return Mac.getInstance(JavaAlgorithm.getJavaAlgorithm(sigAlgName));
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("Unsupported HMAC algorithm: " + e.getMessage(), e);
}
}
@Override
public byte[] sign(byte[] data, String sigAlgName, Key key) {
try {
Mac mac = getMAC(sigAlgName);
mac.init(key);
mac.update(data);
return mac.doFinal();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public boolean verify(JWSInput jws, Key verifyKey) {
try {
byte[] signature = sign(jws.getEncodedSignatureInput().getBytes("UTF-8"), jws.getHeader().getAlgorithm().name(), verifyKey);
return MessageDigest.isEqual(signature, Base64Url.decode(jws.getEncodedSignature()));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
@@ -0,0 +1,56 @@
package org.keycloak.jose.jws;
import java.util.List;
import org.keycloak.Config.Scope;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderConfigurationBuilder;
// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI
@SuppressWarnings("rawtypes")
public class HmacTokenSignatureProviderFactory implements TokenSignatureProviderFactory {
public static final String ID = "hmac-signature";
private static final String HELP_TEXT = "Generates token signature provider using HMAC key";
private static final List<ProviderConfigProperty> CONFIG_PROPERTIES = ProviderConfigurationBuilder.create().build();
@Override
public void init(Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
@Override
public String getId() {
return ID;
}
@Override
public String getHelpText() {
return HELP_TEXT;
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return CONFIG_PROPERTIES;
}
@Override
public TokenSignatureProvider create(KeycloakSession session, ComponentModel model) {
return new HmacTokenSignatureProvider(session, model);
}
}
@@ -0,0 +1,47 @@
package org.keycloak.jose.jws;
import java.security.Key;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.KeycloakSession;
// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI
public class RsassaTokenSignatureProvider extends AbstractTokenSignatureProvider {
public RsassaTokenSignatureProvider(KeycloakSession session, ComponentModel model) {
super(session, model);
}
@Override
public void close() {}
@Override
public byte[] sign(byte[] data, String sigAlgName, Key key) {
try {
PrivateKey privateKey = (PrivateKey)key;
Signature signature = getSignature(sigAlgName);
signature.initSign(privateKey);
signature.update(data);
return signature.sign();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public boolean verify(JWSInput jws, Key verifyKey) {
try {
PublicKey publicKey = (PublicKey)verifyKey;
Signature verifier = getSignature(jws.getHeader().getAlgorithm().name());
verifier.initVerify(publicKey);
verifier.update(jws.getEncodedSignatureInput().getBytes("UTF-8"));
return verifier.verify(jws.getSignature());
} catch (Exception e) {
return false;
}
}
}
@@ -0,0 +1,55 @@
package org.keycloak.jose.jws;
import java.util.List;
import org.keycloak.Config.Scope;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderConfigurationBuilder;
// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI
@SuppressWarnings("rawtypes")
public class RsassaTokenSignatureProviderFactory implements TokenSignatureProviderFactory {
public static final String ID = "rsassa-signature";
private static final String HELP_TEXT = "Generates token signature provider using RSA key";
private static final List<ProviderConfigProperty> CONFIG_PROPERTIES = ProviderConfigurationBuilder.create().build();
@Override
public void init(Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
@Override
public String getId() {
return ID;
}
@Override
public String getHelpText() {
return HELP_TEXT;
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return CONFIG_PROPERTIES;
}
@Override
public TokenSignatureProvider create(KeycloakSession session, ComponentModel model) {
return new RsassaTokenSignatureProvider(session, model);
}
}
@@ -0,0 +1,97 @@
package org.keycloak.jose.jws;
import java.security.Key;
import java.util.LinkedList;
import java.util.List;
import org.jboss.logging.Logger;
import org.keycloak.component.ComponentModel;
import org.keycloak.crypto.KeyUse;
import org.keycloak.crypto.KeyWrapper;
import org.keycloak.jose.jws.JWSSignatureProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.representations.JsonWebToken;
import org.keycloak.representations.RefreshToken;
import org.keycloak.util.TokenUtil;
// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI
public class TokenSignature {
private static final Logger logger = Logger.getLogger(TokenSignature.class);
KeycloakSession session;
RealmModel realm;
String sigAlgName;
public static TokenSignature getInstance(KeycloakSession session, RealmModel realm, String sigAlgName) {
return new TokenSignature(session, realm, sigAlgName);
}
public TokenSignature(KeycloakSession session, RealmModel realm, String sigAlgName) {
this.session = session;
this.realm = realm;
this.sigAlgName = sigAlgName;
}
public String sign(JsonWebToken jwt) {
TokenSignatureProvider tokenSignatureProvider = getTokenSignatureProvider(sigAlgName);
if (tokenSignatureProvider == null) return null;
KeyWrapper keyWrapper = session.keys().getActiveKey(realm, KeyUse.SIG, sigAlgName);
if (keyWrapper == null) return null;
String keyId = keyWrapper.getKid();
Key signKey = keyWrapper.getSignKey();
String encodedToken = new JWSBuilder().type("JWT").kid(keyId).jsonContent(jwt).sign((JWSSignatureProvider)tokenSignatureProvider, sigAlgName, signKey);
return encodedToken;
}
public boolean verify(JWSInput jws) throws JWSInputException {
TokenSignatureProvider tokenSignatureProvider = getTokenSignatureProvider(sigAlgName);
if (tokenSignatureProvider == null) return false;
KeyWrapper keyWrapper = null;
// Backwards compatibility. Old offline tokens didn't have KID in the header
if (jws.getHeader().getKeyId() == null && isOfflineToken(jws)) {
logger.debugf("KID is null in offline token. Using the realm active key to verify token signature.");
keyWrapper = session.keys().getActiveKey(realm, KeyUse.SIG, sigAlgName);
} else {
keyWrapper = session.keys().getKey(realm, jws.getHeader().getKeyId(), KeyUse.SIG, sigAlgName);
}
if (keyWrapper == null) return false;
return tokenSignatureProvider.verify(jws, keyWrapper.getVerifyKey());
}
private static final String COMPONENT_SIGNATURE_ALGORITHM_KEY = "org.keycloak.jose.jws.TokenSignatureProvider.algorithm";
@SuppressWarnings("rawtypes")
private TokenSignatureProvider getTokenSignatureProvider(String sigAlgName) {
List<ComponentModel> components = new LinkedList<>(realm.getComponents(realm.getId(), TokenSignatureProvider.class.getName()));
ComponentModel c = null;
for (ComponentModel component : components) {
if (sigAlgName.equals(component.get(COMPONENT_SIGNATURE_ALGORITHM_KEY))) {
c = component;
break;
}
}
if (c == null) {
if (logger.isTraceEnabled()) {
logger.tracev("Failed to find TokenSignatureProvider algorithm={0}.", sigAlgName);
}
return null;
}
ProviderFactory<TokenSignatureProvider> f = session.getKeycloakSessionFactory().getProviderFactory(TokenSignatureProvider.class, c.getProviderId());
TokenSignatureProviderFactory factory = (TokenSignatureProviderFactory) f;
TokenSignatureProvider provider = factory.create(session, c);
return provider;
}
private boolean isOfflineToken(JWSInput jws) throws JWSInputException {
RefreshToken token = TokenUtil.getRefreshToken(jws.getContent());
return token.getType().equals(TokenUtil.TOKEN_TYPE_OFFLINE);
}
}
@@ -0,0 +1,22 @@
package org.keycloak.jose.jws;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI
public class TokenSignatureUtil {
public static final String REALM_SIGNATURE_ALGORITHM_KEY = "token.signed.response.alg";
private static String DEFAULT_ALGORITHM_NAME = "RS256";
public static String getTokenSignatureAlgorithm(KeycloakSession session, RealmModel realm, ClientModel client) {
String realmSigAlgName = realm.getAttribute(REALM_SIGNATURE_ALGORITHM_KEY);
String clientSigAlgname = null;
if (client != null) clientSigAlgname = OIDCAdvancedConfigWrapper.fromClientModel(client).getIdTokenSignedResponseAlg();
String sigAlgName = clientSigAlgname;
if (sigAlgName == null) sigAlgName = (realmSigAlgName == null ? DEFAULT_ALGORITHM_NAME : realmSigAlgName);
return sigAlgName;
}
}
@@ -0,0 +1,60 @@
package org.keycloak.keys;
import java.security.KeyPair;
import java.util.Collections;
import java.util.List;
import org.keycloak.common.util.KeyUtils;
import org.keycloak.component.ComponentModel;
import org.keycloak.crypto.KeyStatus;
import org.keycloak.crypto.KeyType;
import org.keycloak.crypto.KeyUse;
import org.keycloak.crypto.KeyWrapper;
import org.keycloak.models.RealmModel;
// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI
public abstract class AbstractEcdsaKeyProvider implements KeyProvider {
private final KeyStatus status;
private final ComponentModel model;
private final KeyWrapper key;
public AbstractEcdsaKeyProvider(RealmModel realm, ComponentModel model) {
this.model = model;
this.status = KeyStatus.from(model.get(Attributes.ACTIVE_KEY, true), model.get(Attributes.ENABLED_KEY, true));
if (model.hasNote(KeyWrapper.class.getName())) {
key = model.getNote(KeyWrapper.class.getName());
} else {
key = loadKey(realm, model);
model.setNote(KeyWrapper.class.getName(), key);
}
}
protected abstract KeyWrapper loadKey(RealmModel realm, ComponentModel model);
@Override
public List<KeyWrapper> getKeys() {
return Collections.singletonList(key);
}
protected KeyWrapper createKeyWrapper(KeyPair keyPair, String ecInNistRep) {
KeyWrapper key = new KeyWrapper();
key.setProviderId(model.getId());
key.setProviderPriority(model.get("priority", 0l));
key.setKid(KeyUtils.createKeyId(keyPair.getPublic()));
key.setUse(KeyUse.SIG);
key.setType(KeyType.EC);
key.setAlgorithms(AbstractEcdsaKeyProviderFactory.convertECDomainParmNistRepToAlgorithm(ecInNistRep));
key.setStatus(status);
key.setSignKey(keyPair.getPrivate());
key.setVerifyKey(keyPair.getPublic());
return key;
}
}
@@ -0,0 +1,98 @@
package org.keycloak.keys;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.SecureRandom;
import java.security.spec.ECGenParameterSpec;
import org.keycloak.component.ComponentModel;
import org.keycloak.component.ComponentValidationException;
import org.keycloak.crypto.Algorithm;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.provider.ConfigurationValidationHelper;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderConfigurationBuilder;
import static org.keycloak.provider.ProviderConfigProperty.LIST_TYPE;
// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI
@SuppressWarnings("rawtypes")
public abstract class AbstractEcdsaKeyProviderFactory implements KeyProviderFactory {
protected static final String ECDSA_PRIVATE_KEY_KEY = "ecdsaPrivateKey";
protected static final String ECDSA_PUBLIC_KEY_KEY = "ecdsaPublicKey";
protected static final String ECDSA_ELLIPTIC_CURVE_KEY = "ecdsaEllipticCurveKey";
// only support NIST P-256 for ES256, P-384 for ES384, P-521 for ES512
protected static ProviderConfigProperty ECDSA_ELLIPTIC_CURVE_PROPERTY = new ProviderConfigProperty(ECDSA_ELLIPTIC_CURVE_KEY, "Elliptic Curve", "Elliptic Curve used in ECDSA", LIST_TYPE,
String.valueOf(GeneratedEcdsaKeyProviderFactory.DEFAULT_ECDSA_ELLIPTIC_CURVE),
"P-256", "P-384", "P-521");
public final static ProviderConfigurationBuilder configurationBuilder() {
return ProviderConfigurationBuilder.create()
.property(Attributes.PRIORITY_PROPERTY)
.property(Attributes.ENABLED_PROPERTY)
.property(Attributes.ACTIVE_PROPERTY);
}
@Override
public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model) throws ComponentValidationException {
ConfigurationValidationHelper.check(model)
.checkLong(Attributes.PRIORITY_PROPERTY, false)
.checkBoolean(Attributes.ENABLED_PROPERTY, false)
.checkBoolean(Attributes.ACTIVE_PROPERTY, false);
}
public static KeyPair generateEcdsaKeyPair(String keySpecName) {
try {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("EC");
SecureRandom randomGen = SecureRandom.getInstance("SHA1PRNG");
ECGenParameterSpec ecSpec = new ECGenParameterSpec(keySpecName);
keyGen.initialize(ecSpec, randomGen);
return keyGen.generateKeyPair();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static String convertECDomainParmNistRepToSecRep(String ecInNistRep) {
// convert Elliptic Curve Domain Parameter Name in NIST to SEC which is used to generate its EC key
String ecInSecRep = null;
switch(ecInNistRep) {
case "P-256" :
ecInSecRep = "secp256r1";
break;
case "P-384" :
ecInSecRep = "secp384r1";
break;
case "P-521" :
ecInSecRep = "secp521r1";
break;
default :
// return null
}
return ecInSecRep;
}
public static String convertECDomainParmNistRepToAlgorithm(String ecInNistRep) {
// convert Elliptic Curve Domain Parameter Name in NIST to Algorithm (JWA) representation
String ecInAlgorithmRep = null;
switch(ecInNistRep) {
case "P-256" :
ecInAlgorithmRep = Algorithm.ES256;
break;
case "P-384" :
ecInAlgorithmRep = Algorithm.ES384;
break;
case "P-521" :
ecInAlgorithmRep = Algorithm.ES512;
break;
default :
// return null
}
return ecInAlgorithmRep;
}
}
@@ -48,4 +48,5 @@ public class FailsafeAesKeyProvider extends FailsafeSecretKeyProvider {
protected Logger logger() {
return logger;
}
}
@@ -0,0 +1,66 @@
package org.keycloak.keys;
import java.security.KeyPair;
import java.util.Collections;
import java.util.List;
import org.jboss.logging.Logger;
import org.keycloak.common.util.KeyUtils;
import org.keycloak.common.util.Time;
import org.keycloak.crypto.Algorithm;
import org.keycloak.crypto.KeyStatus;
import org.keycloak.crypto.KeyType;
import org.keycloak.crypto.KeyUse;
import org.keycloak.crypto.KeyWrapper;
// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI
public class FailsafeEcdsaKeyProvider implements KeyProvider {
private static final Logger logger = Logger.getLogger(FailsafeEcdsaKeyProvider.class);
private static KeyWrapper KEY;
private static long EXPIRES;
private KeyWrapper key;
public FailsafeEcdsaKeyProvider() {
logger.errorv("No active keys found, using failsafe provider, please login to admin console to add keys. Clustering is not supported.");
synchronized (FailsafeEcdsaKeyProvider.class) {
if (EXPIRES < Time.currentTime()) {
KEY = createKeyWrapper();
EXPIRES = Time.currentTime() + 60 * 10;
if (EXPIRES > 0) {
logger.warnv("Keys expired, re-generated kid={0}", KEY.getKid());
}
}
key = KEY;
}
}
@Override
public List<KeyWrapper> getKeys() {
return Collections.singletonList(key);
}
private KeyWrapper createKeyWrapper() {
// secp256r1,NIST P-256,X9.62 prime256v1,1.2.840.10045.3.1.7
KeyPair keyPair = AbstractEcdsaKeyProviderFactory.generateEcdsaKeyPair("secp256r1");
KeyWrapper key = new KeyWrapper();
key.setKid(KeyUtils.createKeyId(keyPair.getPublic()));
key.setUse(KeyUse.SIG);
key.setType(KeyType.EC);
key.setAlgorithms(Algorithm.ES256);
key.setStatus(KeyStatus.ACTIVE);
key.setSignKey(keyPair.getPrivate());
key.setVerifyKey(keyPair.getPublic());
return key;
}
}
@@ -18,17 +18,9 @@
package org.keycloak.keys;
import org.jboss.logging.Logger;
import org.keycloak.common.util.KeyUtils;
import org.keycloak.common.util.Time;
import org.keycloak.crypto.Algorithm;
import org.keycloak.crypto.KeyType;
import org.keycloak.crypto.KeyUse;
import org.keycloak.crypto.KeyWrapper;
import org.keycloak.models.utils.KeycloakModelUtils;
import javax.crypto.SecretKey;
import java.util.Collections;
import java.util.List;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -77,5 +77,4 @@ public class FailsafeRsaKeyProvider implements KeyProvider {
return key;
}
}
@@ -0,0 +1,49 @@
package org.keycloak.keys;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import org.jboss.logging.Logger;
import org.keycloak.common.util.Base64;
import org.keycloak.component.ComponentModel;
import org.keycloak.crypto.KeyWrapper;
import org.keycloak.models.RealmModel;
// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI
public class GeneratedEcdsaKeyProvider extends AbstractEcdsaKeyProvider {
private static final Logger logger = Logger.getLogger(GeneratedEcdsaKeyProvider.class);
public GeneratedEcdsaKeyProvider(RealmModel realm, ComponentModel model) {
super(realm, model);
}
@Override
protected KeyWrapper loadKey(RealmModel realm, ComponentModel model) {
String privateEcdsaKeyBase64Encoded = model.getConfig().getFirst(GeneratedEcdsaKeyProviderFactory.ECDSA_PRIVATE_KEY_KEY);
String publicEcdsaKeyBase64Encoded = model.getConfig().getFirst(GeneratedEcdsaKeyProviderFactory.ECDSA_PUBLIC_KEY_KEY);
String ecInNistRep = model.getConfig().getFirst(GeneratedEcdsaKeyProviderFactory.ECDSA_ELLIPTIC_CURVE_KEY);
try {
PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(Base64.decode(privateEcdsaKeyBase64Encoded));
KeyFactory kf = KeyFactory.getInstance("EC");
PrivateKey decodedPrivateKey = kf.generatePrivate(privateKeySpec);
X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(Base64.decode(publicEcdsaKeyBase64Encoded));
PublicKey decodedPublicKey = kf.generatePublic(publicKeySpec);
KeyPair keyPair = new KeyPair(decodedPublicKey, decodedPrivateKey);
return createKeyWrapper(keyPair, ecInNistRep);
} catch (Exception e) {
logger.warnf("Exception at decodeEcdsaPublicKey. %s", e.toString());
return null;
}
}
}
@@ -0,0 +1,90 @@
package org.keycloak.keys;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.List;
import org.jboss.logging.Logger;
import org.keycloak.common.util.Base64;
import org.keycloak.component.ComponentModel;
import org.keycloak.component.ComponentValidationException;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.provider.ConfigurationValidationHelper;
import org.keycloak.provider.ProviderConfigProperty;
// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI
public class GeneratedEcdsaKeyProviderFactory extends AbstractEcdsaKeyProviderFactory {
private static final Logger logger = Logger.getLogger(GeneratedEcdsaKeyProviderFactory.class);
public static final String ID = "ecdsa-generated";
private static final String HELP_TEXT = "Generates ECDSA keys";
// secp256r1,NIST P-256,X9.62 prime256v1,1.2.840.10045.3.1.7
public static final String DEFAULT_ECDSA_ELLIPTIC_CURVE = "P-256";
private static final List<ProviderConfigProperty> CONFIG_PROPERTIES = AbstractEcdsaKeyProviderFactory.configurationBuilder()
.property(ECDSA_ELLIPTIC_CURVE_PROPERTY)
.build();
@Override
public KeyProvider create(KeycloakSession session, ComponentModel model) {
return new GeneratedEcdsaKeyProvider(session.getContext().getRealm(), model);
}
@Override
public String getHelpText() {
return HELP_TEXT;
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return CONFIG_PROPERTIES;
}
@Override
public String getId() {
return ID;
}
@Override
public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model) throws ComponentValidationException {
super.validateConfiguration(session, realm, model);
ConfigurationValidationHelper.check(model).checkList(ECDSA_ELLIPTIC_CURVE_PROPERTY, false);
String ecInNistRep = model.get(ECDSA_ELLIPTIC_CURVE_KEY);
if (ecInNistRep == null) ecInNistRep = DEFAULT_ECDSA_ELLIPTIC_CURVE;
if (!(model.contains(ECDSA_PRIVATE_KEY_KEY) && model.contains(ECDSA_PUBLIC_KEY_KEY))) {
generateKeys(realm, model, ecInNistRep);
logger.debugv("Generated keys for {0}", realm.getName());
} else {
String currentEc = model.get(ECDSA_ELLIPTIC_CURVE_KEY);
if (!ecInNistRep.equals(currentEc)) {
generateKeys(realm, model, ecInNistRep);
logger.debugv("Elliptic Curve changed, generating new keys for {0}", realm.getName());
}
}
}
private void generateKeys(RealmModel realm, ComponentModel model, String ecInNistRep) {
KeyPair keyPair;
try {
keyPair = generateEcdsaKeyPair(convertECDomainParmNistRepToSecRep(ecInNistRep));
model.put(ECDSA_PRIVATE_KEY_KEY, Base64.encodeBytes(keyPair.getPrivate().getEncoded()));
model.put(ECDSA_PUBLIC_KEY_KEY, Base64.encodeBytes(keyPair.getPublic().getEncoded()));
model.put(ECDSA_ELLIPTIC_CURVE_KEY, ecInNistRep);
} catch (Throwable t) {
throw new ComponentValidationException("Failed to generate ECDSA keys", t);
}
}
}
@@ -17,6 +17,8 @@
package org.keycloak.keys;
import java.security.Key;
import org.keycloak.component.ComponentModel;
import org.keycloak.crypto.Algorithm;
import org.keycloak.crypto.KeyType;
@@ -47,6 +47,9 @@ public class OIDCAdvancedConfigWrapper {
// https://tools.ietf.org/html/draft-ietf-oauth-mtls-08#section-6.5
private static final String USE_MTLS_HOK_TOKEN = "tls.client.certificate.bound.access.tokens";
// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI
private static final String ID_TOKEN_SIGNED_RESPONSE_ALG = "id.token.signed.response.alg";
private final ClientModel clientModel;
private final ClientRepresentation clientRep;
@@ -137,6 +140,14 @@ public class OIDCAdvancedConfigWrapper {
setAttribute(USE_MTLS_HOK_TOKEN, val);
}
// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI
public String getIdTokenSignedResponseAlg() {
return getAttribute(ID_TOKEN_SIGNED_RESPONSE_ALG);
}
public void setIdTokenSignedResponseAlg(String algName) {
setAttribute(ID_TOKEN_SIGNED_RESPONSE_ALG, algName);
}
private String getAttribute(String attrKey) {
if (clientModel != null) {
return clientModel.getAttribute(attrKey);
@@ -30,8 +30,9 @@ import org.keycloak.jose.jws.Algorithm;
import org.keycloak.jose.jws.JWSBuilder;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.JWSInputException;
import org.keycloak.jose.jws.TokenSignature;
import org.keycloak.jose.jws.TokenSignatureUtil;
import org.keycloak.jose.jws.crypto.HashProvider;
import org.keycloak.jose.jws.crypto.RSAProvider;
import org.keycloak.migration.migrators.MigrationUtils;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
@@ -74,7 +75,6 @@ import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import java.security.PublicKey;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
@@ -376,21 +376,11 @@ public class TokenManager {
public RefreshToken toRefreshToken(KeycloakSession session, RealmModel realm, String encodedRefreshToken) throws JWSInputException, OAuthErrorException {
JWSInput jws = new JWSInput(encodedRefreshToken);
PublicKey publicKey;
// Backwards compatibility. Old offline tokens didn't have KID in the header
if (jws.getHeader().getKeyId() == null && TokenUtil.isOfflineToken(encodedRefreshToken)) {
logger.debugf("KID is null in offline token. Using the realm active key to verify token signature.");
publicKey = session.keys().getActiveRsaKey(realm).getPublicKey();
} else {
publicKey = session.keys().getRsaPublicKey(realm, jws.getHeader().getKeyId());
}
if (!RSAProvider.verify(jws, publicKey)) {
// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI
TokenSignature ts = TokenSignature.getInstance(session, realm, jws.getHeader().getAlgorithm().name());
if (!ts.verify(jws)) {
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid refresh token");
}
return jws.readJsonContent(RefreshToken.class);
}
@@ -398,15 +388,15 @@ public class TokenManager {
try {
JWSInput jws = new JWSInput(encodedIDToken);
IDToken idToken;
if (!RSAProvider.verify(jws, session.keys().getRsaPublicKey(realm, jws.getHeader().getKeyId()))) {
// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI
TokenSignature ts = TokenSignature.getInstance(session, realm, jws.getHeader().getAlgorithm().name());
if (!ts.verify(jws)) {
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid IDToken");
}
idToken = jws.readJsonContent(IDToken.class);
if (idToken.isExpired()) {
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "IDToken expired");
}
if (idToken.getIssuedAt() < realm.getNotBefore()) {
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Stale IDToken");
}
@@ -420,11 +410,12 @@ public class TokenManager {
try {
JWSInput jws = new JWSInput(encodedIDToken);
IDToken idToken;
if (!RSAProvider.verify(jws, session.keys().getRsaPublicKey(realm, jws.getHeader().getKeyId()))) {
// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI
TokenSignature ts = TokenSignature.getInstance(session, realm, jws.getHeader().getAlgorithm().name());
if (!ts.verify(jws)) {
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid IDToken");
}
idToken = jws.readJsonContent(IDToken.class);
return idToken;
} catch (JWSInputException e) {
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid IDToken", e);
@@ -862,20 +853,20 @@ public class TokenManager {
}
public AccessTokenResponseBuilder generateCodeHash(String code) {
codeHash = HashProvider.oidcHash(jwsAlgorithm, code);
// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI
codeHash = HashProvider.oidcHash(TokenSignatureUtil.getTokenSignatureAlgorithm(session, realm, client), code);
return this;
}
// Financial API - Part 2: Read and Write API Security Profile
// http://openid.net/specs/openid-financial-api-part-2.html#authorization-server
public AccessTokenResponseBuilder generateStateHash(String state) {
stateHash = HashProvider.oidcHash(jwsAlgorithm, state);
// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI
stateHash = HashProvider.oidcHash(TokenSignatureUtil.getTokenSignatureAlgorithm(session, realm, client), state);
return this;
}
public AccessTokenResponse build() {
KeyManager.ActiveRsaKey activeRsaKey = session.keys().getActiveRsaKey(realm);
if (accessToken != null) {
event.detail(Details.TOKEN_ID, accessToken.getId());
}
@@ -890,8 +881,13 @@ public class TokenManager {
}
AccessTokenResponse res = new AccessTokenResponse();
// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI
TokenSignature ts = TokenSignature.getInstance(session, realm, TokenSignatureUtil.getTokenSignatureAlgorithm(session, realm, client));
if (accessToken != null) {
String encodedToken = new JWSBuilder().type(JWT).kid(activeRsaKey.getKid()).jsonContent(accessToken).sign(jwsAlgorithm, activeRsaKey.getPrivateKey());
// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI
String encodedToken = ts.sign(accessToken);
res.setToken(encodedToken);
res.setTokenType("bearer");
res.setSessionState(accessToken.getSessionState());
@@ -901,7 +897,8 @@ public class TokenManager {
}
if (generateAccessTokenHash) {
String atHash = HashProvider.oidcHash(jwsAlgorithm, res.getToken());
// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI
String atHash = HashProvider.oidcHash(TokenSignatureUtil.getTokenSignatureAlgorithm(session, realm, client), res.getToken());
idToken.setAccessTokenHash(atHash);
}
if (codeHash != null) {
@@ -913,11 +910,13 @@ public class TokenManager {
idToken.setStateHash(stateHash);
}
if (idToken != null) {
String encodedToken = new JWSBuilder().type(JWT).kid(activeRsaKey.getKid()).jsonContent(idToken).sign(jwsAlgorithm, activeRsaKey.getPrivateKey());
// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI
String encodedToken = ts.sign(idToken);
res.setIdToken(encodedToken);
}
if (refreshToken != null) {
String encodedToken = new JWSBuilder().type(JWT).kid(activeRsaKey.getKid()).jsonContent(refreshToken).sign(jwsAlgorithm, activeRsaKey.getPrivateKey());
// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI
String encodedToken = ts.sign(refreshToken);
res.setRefreshToken(encodedToken);
if (refreshToken.getExpiration() != 0) {
res.setRefreshExpiresIn(refreshToken.getExpiration() - Time.currentTime());
@@ -121,6 +121,11 @@ public class DescriptionConverter {
else configWrapper.setUseMtlsHoKToken(false);
}
// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI
if (clientOIDC.getIdTokenSignedResponseAlg() != null) {
configWrapper.setIdTokenSignedResponseAlg(clientOIDC.getIdTokenSignedResponseAlg());
}
return client;
}
@@ -201,6 +206,10 @@ public class DescriptionConverter {
} else {
response.setTlsClientCertificateBoundAccessTokens(Boolean.FALSE);
}
// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI
if (config.getIdTokenSignedResponseAlg() != null) {
response.setIdTokenSignedResponseAlg(config.getIdTokenSignedResponseAlg());
}
List<ProtocolMapperRepresentation> foundPairwiseMappers = PairwiseSubMapperUtils.getPairwiseSubMappers(client);
SubjectType subjectType = foundPairwiseMappers.isEmpty() ? SubjectType.PUBLIC : SubjectType.PAIRWISE;
@@ -27,6 +27,7 @@ import org.keycloak.models.RoleModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.DefaultKeyProviders;
import org.keycloak.models.utils.DefaultTokenSignatureProviders;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.services.ServicesLogger;
@@ -89,6 +90,9 @@ public class ApplianceBootstrap {
session.getContext().setRealm(realm);
DefaultKeyProviders.createProviders(realm);
// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI
DefaultTokenSignatureProviders.createProviders(realm);
return true;
}
@@ -0,0 +1,4 @@
# KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI
org.keycloak.jose.jws.RsassaTokenSignatureProviderFactory
org.keycloak.jose.jws.HmacTokenSignatureProviderFactory
org.keycloak.jose.jws.EcdsaTokenSignatureProviderFactory
@@ -19,4 +19,6 @@ org.keycloak.keys.GeneratedHmacKeyProviderFactory
org.keycloak.keys.GeneratedAesKeyProviderFactory
org.keycloak.keys.GeneratedRsaKeyProviderFactory
org.keycloak.keys.JavaKeystoreKeyProviderFactory
org.keycloak.keys.ImportedRsaKeyProviderFactory
org.keycloak.keys.ImportedRsaKeyProviderFactory
# KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI
org.keycloak.keys.GeneratedEcdsaKeyProviderFactory