KEYCLOAK-7560 Refactor Token Sign and Verify by Token Signature SPI
This commit is contained in:
parent
bd4098191b
commit
5b6036525c
47 changed files with 1979 additions and 83 deletions
|
@ -24,12 +24,15 @@ import org.keycloak.jose.jws.AlgorithmType;
|
|||
import org.keycloak.jose.jws.JWSHeader;
|
||||
import org.keycloak.jose.jws.JWSInput;
|
||||
import org.keycloak.jose.jws.JWSInputException;
|
||||
import org.keycloak.jose.jws.JWSSignatureProvider;
|
||||
import org.keycloak.jose.jws.crypto.HMACProvider;
|
||||
import org.keycloak.jose.jws.crypto.RSAProvider;
|
||||
import org.keycloak.representations.JsonWebToken;
|
||||
import org.keycloak.util.TokenUtil;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
import java.security.Key;
|
||||
import java.security.PublicKey;
|
||||
import java.util.*;
|
||||
import java.util.logging.Level;
|
||||
|
@ -144,6 +147,18 @@ public class TokenVerifier<T extends JsonWebToken> {
|
|||
private JWSInput jws;
|
||||
private T token;
|
||||
|
||||
// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI
|
||||
private Key verifyKey = null;
|
||||
private JWSSignatureProvider signatureProvider = null;
|
||||
public TokenVerifier<T> verifyKey(Key verifyKey) {
|
||||
this.verifyKey = verifyKey;
|
||||
return this;
|
||||
}
|
||||
public TokenVerifier<T> signatureProvider(JWSSignatureProvider signatureProvider) {
|
||||
this.signatureProvider = signatureProvider;
|
||||
return this;
|
||||
}
|
||||
|
||||
protected TokenVerifier(String tokenString, Class<T> clazz) {
|
||||
this.tokenString = tokenString;
|
||||
this.clazz = clazz;
|
||||
|
@ -337,6 +352,12 @@ public class TokenVerifier<T extends JsonWebToken> {
|
|||
}
|
||||
|
||||
public void verifySignature() throws VerificationException {
|
||||
// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI
|
||||
if (this.signatureProvider != null && this.verify() != null) {
|
||||
verifySignatureByProvider();
|
||||
return;
|
||||
}
|
||||
|
||||
AlgorithmType algorithmType = getHeader().getAlgorithm().getType();
|
||||
|
||||
if (null == algorithmType) {
|
||||
|
@ -361,6 +382,13 @@ public class TokenVerifier<T extends JsonWebToken> {
|
|||
}
|
||||
}
|
||||
|
||||
// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI
|
||||
private void verifySignatureByProvider() throws VerificationException {
|
||||
if (!signatureProvider.verify(jws, verifyKey)) {
|
||||
throw new TokenSignatureInvalidException(token, "Invalid token signature");
|
||||
}
|
||||
}
|
||||
|
||||
public TokenVerifier<T> verify() throws VerificationException {
|
||||
if (getToken() == null) {
|
||||
parse();
|
||||
|
|
|
@ -25,6 +25,7 @@ import org.keycloak.util.JsonSerialization;
|
|||
import javax.crypto.SecretKey;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.security.Key;
|
||||
import java.security.PrivateKey;
|
||||
|
||||
/**
|
||||
|
@ -36,7 +37,7 @@ public class JWSBuilder {
|
|||
String kid;
|
||||
String contentType;
|
||||
byte[] contentBytes;
|
||||
|
||||
|
||||
public JWSBuilder type(String type) {
|
||||
this.type = type;
|
||||
return this;
|
||||
|
@ -66,10 +67,34 @@ public class JWSBuilder {
|
|||
return new EncodingBuilder();
|
||||
}
|
||||
|
||||
protected String encodeAll(StringBuffer encoding, byte[] signature) {
|
||||
encoding.append('.');
|
||||
if (signature != null) {
|
||||
encoding.append(Base64Url.encode(signature));
|
||||
}
|
||||
return encoding.toString();
|
||||
}
|
||||
|
||||
protected String encodeHeader(Algorithm alg) {
|
||||
protected void encode(Algorithm alg, byte[] data, StringBuffer encoding) {
|
||||
// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI
|
||||
encode(alg.name(), data, encoding);
|
||||
}
|
||||
|
||||
protected byte[] marshalContent() {
|
||||
return contentBytes;
|
||||
}
|
||||
|
||||
// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI
|
||||
protected void encode(String sigAlgName, byte[] data, StringBuffer encoding) {
|
||||
encoding.append(encodeHeader(sigAlgName));
|
||||
encoding.append('.');
|
||||
encoding.append(Base64Url.encode(data));
|
||||
}
|
||||
|
||||
// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI
|
||||
protected String encodeHeader(String sigAlgName) {
|
||||
StringBuilder builder = new StringBuilder("{");
|
||||
builder.append("\"alg\":\"").append(alg.toString()).append("\"");
|
||||
builder.append("\"alg\":\"").append(sigAlgName).append("\"");
|
||||
|
||||
if (type != null) builder.append(",\"typ\" : \"").append(type).append("\"");
|
||||
if (kid != null) builder.append(",\"kid\" : \"").append(kid).append("\"");
|
||||
|
@ -82,24 +107,6 @@ public class JWSBuilder {
|
|||
}
|
||||
}
|
||||
|
||||
protected String encodeAll(StringBuffer encoding, byte[] signature) {
|
||||
encoding.append('.');
|
||||
if (signature != null) {
|
||||
encoding.append(Base64Url.encode(signature));
|
||||
}
|
||||
return encoding.toString();
|
||||
}
|
||||
|
||||
protected void encode(Algorithm alg, byte[] data, StringBuffer encoding) {
|
||||
encoding.append(encodeHeader(alg));
|
||||
encoding.append('.');
|
||||
encoding.append(Base64Url.encode(data));
|
||||
}
|
||||
|
||||
protected byte[] marshalContent() {
|
||||
return contentBytes;
|
||||
}
|
||||
|
||||
public class EncodingBuilder {
|
||||
public String none() {
|
||||
StringBuffer buffer = new StringBuffer();
|
||||
|
@ -108,6 +115,20 @@ public class JWSBuilder {
|
|||
return encodeAll(buffer, null);
|
||||
}
|
||||
|
||||
// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI
|
||||
public String sign(JWSSignatureProvider signatureProvider, String sigAlgName, Key key) {
|
||||
StringBuffer buffer = new StringBuffer();
|
||||
byte[] data = marshalContent();
|
||||
encode(sigAlgName, data, buffer);
|
||||
byte[] signature = null;
|
||||
try {
|
||||
signature = signatureProvider.sign(buffer.toString().getBytes("UTF-8"), sigAlgName, key);
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return encodeAll(buffer, signature);
|
||||
}
|
||||
|
||||
public String sign(Algorithm algorithm, PrivateKey privateKey) {
|
||||
StringBuffer buffer = new StringBuffer();
|
||||
byte[] data = marshalContent();
|
||||
|
@ -133,7 +154,6 @@ public class JWSBuilder {
|
|||
return sign(Algorithm.RS512, privateKey);
|
||||
}
|
||||
|
||||
|
||||
public String hmac256(byte[] sharedSecret) {
|
||||
StringBuffer buffer = new StringBuffer();
|
||||
byte[] data = marshalContent();
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
package org.keycloak.jose.jws;
|
||||
|
||||
import java.security.Key;
|
||||
|
||||
public interface JWSSignatureProvider {
|
||||
// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI
|
||||
byte[] sign(byte[] data, String sigAlgName, Key key);
|
||||
boolean verify(JWSInput input, Key key);
|
||||
}
|
|
@ -25,6 +25,7 @@ import org.keycloak.jose.jws.JWSInput;
|
|||
import javax.crypto.Mac;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
|
|
|
@ -27,18 +27,16 @@ import java.util.Arrays;
|
|||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class HashProvider {
|
||||
|
||||
// See "at_hash" and "c_hash" in OIDC specification
|
||||
public static String oidcHash(Algorithm jwtAlgorithm, String input) {
|
||||
byte[] digest = digest(jwtAlgorithm, input);
|
||||
// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI
|
||||
public static String oidcHash(String jwtAlgorithmName, String input) {
|
||||
byte[] digest = digest(jwtAlgorithmName, input);
|
||||
|
||||
int hashLength = digest.length / 2;
|
||||
byte[] hashInput = Arrays.copyOf(digest, hashLength);
|
||||
|
||||
return Base64Url.encode(hashInput);
|
||||
}
|
||||
|
||||
private static byte[] digest(Algorithm algorithm, String input) {
|
||||
private static byte[] digest(String algorithm, String input) {
|
||||
String digestAlg = getJavaDigestAlgorithm(algorithm);
|
||||
|
||||
try {
|
||||
|
@ -49,18 +47,22 @@ public class HashProvider {
|
|||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static String getJavaDigestAlgorithm(Algorithm alg) {
|
||||
private static String getJavaDigestAlgorithm(String alg) {
|
||||
switch (alg) {
|
||||
case RS256:
|
||||
case "RS256":
|
||||
return "SHA-256";
|
||||
case RS384:
|
||||
case "RS384":
|
||||
return "SHA-384";
|
||||
case RS512:
|
||||
case "RS512":
|
||||
return "SHA-512";
|
||||
default:
|
||||
throw new IllegalArgumentException("Not an RSA Algorithm");
|
||||
}
|
||||
}
|
||||
|
||||
// See "at_hash" and "c_hash" in OIDC specification
|
||||
public static String oidcHash(Algorithm jwtAlgorithm, String input) {
|
||||
return oidcHash(jwtAlgorithm.name(), input);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
package org.keycloak.jose.jws;
|
||||
|
||||
import java.security.Key;
|
||||
|
||||
import org.keycloak.provider.Provider;
|
||||
|
||||
// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI
|
||||
|
||||
public interface TokenSignatureProvider extends Provider {
|
||||
byte[] sign(byte[] data, String sigAlgName, Key key);
|
||||
boolean verify(JWSInput jws, Key verifyKey);
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package org.keycloak.jose.jws;
|
||||
|
||||
import org.keycloak.component.ComponentFactory;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
||||
// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI
|
||||
|
||||
public interface TokenSignatureProviderFactory<T extends TokenSignatureProvider> extends ComponentFactory<T, TokenSignatureProvider> {
|
||||
T create(KeycloakSession session, ComponentModel model);
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package org.keycloak.jose.jws;
|
||||
|
||||
import org.keycloak.provider.Provider;
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
import org.keycloak.provider.Spi;
|
||||
|
||||
// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI
|
||||
|
||||
public class TokenSignatureSpi implements Spi {
|
||||
@Override
|
||||
public boolean isInternal() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "tokenSignature";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends Provider> getProviderClass() {
|
||||
return TokenSignatureProvider.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends ProviderFactory> getProviderFactoryClass() {
|
||||
return TokenSignatureProviderFactory.class;
|
||||
}
|
||||
}
|
|
@ -18,12 +18,8 @@
|
|||
package org.keycloak.keys;
|
||||
|
||||
import org.keycloak.crypto.KeyWrapper;
|
||||
import org.keycloak.jose.jws.AlgorithmType;
|
||||
import org.keycloak.provider.Provider;
|
||||
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
package org.keycloak.keys;
|
||||
|
||||
import java.security.Key;
|
||||
|
||||
// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI
|
||||
|
||||
public interface SignatureKeyProvider {
|
||||
Key getSignKey();
|
||||
Key getVerifyKey(String kid);
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
package org.keycloak.models.utils;
|
||||
|
||||
import org.keycloak.common.util.MultivaluedHashMap;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.jose.jws.TokenSignatureProvider;
|
||||
import org.keycloak.models.RealmModel;
|
||||
|
||||
// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI
|
||||
|
||||
public class DefaultTokenSignatureProviders {
|
||||
private static final String COMPONENT_SIGNATURE_ALGORITHM_KEY = "org.keycloak.jose.jws.TokenSignatureProvider.algorithm";
|
||||
private static final String RSASSA_PROVIDER_ID = "rsassa-signature";
|
||||
private static final String HMAC_PROVIDER_ID = "hmac-signature";
|
||||
|
||||
public static void createProviders(RealmModel realm) {
|
||||
createAndAddProvider(realm, RSASSA_PROVIDER_ID, "RS256");
|
||||
createAndAddProvider(realm, RSASSA_PROVIDER_ID, "RS384");
|
||||
createAndAddProvider(realm, RSASSA_PROVIDER_ID, "RS512");
|
||||
createAndAddProvider(realm, HMAC_PROVIDER_ID, "HS256");
|
||||
createAndAddProvider(realm, HMAC_PROVIDER_ID, "HS384");
|
||||
createAndAddProvider(realm, HMAC_PROVIDER_ID, "HS512");
|
||||
}
|
||||
|
||||
private static void createAndAddProvider(RealmModel realm, String providerId, String sigAlgName) {
|
||||
ComponentModel generated = new ComponentModel();
|
||||
generated.setName(providerId);
|
||||
generated.setParentId(realm.getId());
|
||||
generated.setProviderId(providerId);
|
||||
generated.setProviderType(TokenSignatureProvider.class.getName());
|
||||
MultivaluedHashMap<String, String> config = new MultivaluedHashMap<>();
|
||||
config.putSingle(COMPONENT_SIGNATURE_ALGORITHM_KEY, sigAlgName);
|
||||
generated.setConfig(config);
|
||||
realm.addComponentModel(generated);
|
||||
}
|
||||
}
|
|
@ -53,6 +53,7 @@ import org.keycloak.common.util.MultivaluedHashMap;
|
|||
import org.keycloak.common.util.UriUtils;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.credential.CredentialModel;
|
||||
import org.keycloak.jose.jws.TokenSignatureProvider;
|
||||
import org.keycloak.keys.KeyProvider;
|
||||
import org.keycloak.migration.MigrationProvider;
|
||||
import org.keycloak.migration.migrators.MigrationUtils;
|
||||
|
@ -420,6 +421,12 @@ public class RepresentationToModel {
|
|||
DefaultKeyProviders.createProviders(newRealm);
|
||||
}
|
||||
}
|
||||
|
||||
// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI
|
||||
if (newRealm.getComponents(newRealm.getId(), TokenSignatureProvider.class.getName()).isEmpty()) {
|
||||
DefaultTokenSignatureProviders.createProviders(newRealm);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static void importUserFederationProvidersAndMappers(KeycloakSession session, RealmRepresentation rep, RealmModel newRealm) {
|
||||
|
|
|
@ -70,4 +70,7 @@ org.keycloak.credential.hash.PasswordHashSpi
|
|||
org.keycloak.credential.CredentialSpi
|
||||
org.keycloak.keys.PublicKeyStorageSpi
|
||||
org.keycloak.keys.KeySpi
|
||||
org.keycloak.storage.client.ClientStorageProviderSpi
|
||||
org.keycloak.storage.client.ClientStorageProviderSpi
|
||||
# KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI
|
||||
org.keycloak.jose.jws.TokenSignatureSpi
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,165 @@
|
|||
package org.keycloak.testsuite.util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PublicKey;
|
||||
import java.security.Signature;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.admin.client.Keycloak;
|
||||
import org.keycloak.admin.client.resource.ClientResource;
|
||||
import org.keycloak.common.util.Base64;
|
||||
import org.keycloak.common.util.MultivaluedHashMap;
|
||||
import org.keycloak.jose.jws.EcdsaTokenSignatureProviderFactory;
|
||||
import org.keycloak.jose.jws.JWSInput;
|
||||
import org.keycloak.jose.jws.TokenSignatureProvider;
|
||||
import org.keycloak.keys.GeneratedEcdsaKeyProviderFactory;
|
||||
import org.keycloak.keys.KeyProvider;
|
||||
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.ComponentRepresentation;
|
||||
import org.keycloak.representations.idm.KeysMetadataRepresentation;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.testsuite.admin.ApiUtil;
|
||||
import org.keycloak.testsuite.arquillian.TestContext;
|
||||
|
||||
// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI
|
||||
|
||||
public class TokenSignatureUtil {
|
||||
private static Logger log = Logger.getLogger(TokenSignatureUtil.class);
|
||||
|
||||
private static final String COMPONENT_SIGNATURE_ALGORITHM_KEY = "token.signed.response.alg";
|
||||
|
||||
private static final String ECDSA_ELLIPTIC_CURVE_KEY = "ecdsaEllipticCurveKey";
|
||||
private static final String TEST_REALM_NAME = "test";
|
||||
|
||||
public static void changeRealmTokenSignatureProvider(Keycloak adminClient, String toSigAlgName) {
|
||||
RealmRepresentation rep = adminClient.realm(TEST_REALM_NAME).toRepresentation();
|
||||
Map<String, String> attributes = rep.getAttributes();
|
||||
log.tracef("change realm test signature algorithm from %s to %s", attributes.get(COMPONENT_SIGNATURE_ALGORITHM_KEY), toSigAlgName);
|
||||
attributes.put(COMPONENT_SIGNATURE_ALGORITHM_KEY, toSigAlgName);
|
||||
rep.setAttributes(attributes);
|
||||
adminClient.realm(TEST_REALM_NAME).update(rep);
|
||||
}
|
||||
|
||||
public static void changeClientTokenSignatureProvider(ClientResource clientResource, Keycloak adminClient, String toSigAlgName) {
|
||||
ClientRepresentation clientRep = clientResource.toRepresentation();
|
||||
log.tracef("change client %s signature algorithm from %s to %s", clientRep.getClientId(), OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).getIdTokenSignedResponseAlg(), toSigAlgName);
|
||||
OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setIdTokenSignedResponseAlg(toSigAlgName);
|
||||
clientResource.update(clientRep);
|
||||
}
|
||||
|
||||
public static boolean verifySignature(String sigAlgName, String token, Keycloak adminClient) throws Exception {
|
||||
PublicKey publicKey = getRealmPublicKey(TEST_REALM_NAME, sigAlgName, adminClient);
|
||||
JWSInput jws = new JWSInput(token);
|
||||
Signature verifier = getSignature(sigAlgName);
|
||||
verifier.initVerify(publicKey);
|
||||
verifier.update(jws.getEncodedSignatureInput().getBytes("UTF-8"));
|
||||
return verifier.verify(jws.getSignature());
|
||||
}
|
||||
|
||||
public static void registerTokenSignatureProvider(String sigAlgName, Keycloak adminClient, TestContext testContext) {
|
||||
long priority = System.currentTimeMillis();
|
||||
|
||||
ComponentRepresentation rep = createTokenSignatureRep("valid", EcdsaTokenSignatureProviderFactory.ID);
|
||||
rep.setConfig(new MultivaluedHashMap<>());
|
||||
rep.getConfig().putSingle("priority", Long.toString(priority));
|
||||
rep.getConfig().putSingle("org.keycloak.jose.jws.TokenSignatureProvider.algorithm", sigAlgName);
|
||||
|
||||
Response response = adminClient.realm(TEST_REALM_NAME).components().add(rep);
|
||||
String id = ApiUtil.getCreatedId(response);
|
||||
testContext.getOrCreateCleanup(TEST_REALM_NAME).addComponentId(id);
|
||||
response.close();
|
||||
}
|
||||
|
||||
public static void registerKeyProvider(String ecNistRep, Keycloak adminClient, TestContext testContext) {
|
||||
long priority = System.currentTimeMillis();
|
||||
|
||||
ComponentRepresentation rep = createKeyRep("valid", GeneratedEcdsaKeyProviderFactory.ID);
|
||||
rep.setConfig(new MultivaluedHashMap<>());
|
||||
rep.getConfig().putSingle("priority", Long.toString(priority));
|
||||
rep.getConfig().putSingle(ECDSA_ELLIPTIC_CURVE_KEY, ecNistRep);
|
||||
|
||||
Response response = adminClient.realm(TEST_REALM_NAME).components().add(rep);
|
||||
String id = ApiUtil.getCreatedId(response);
|
||||
testContext.getOrCreateCleanup(TEST_REALM_NAME).addComponentId(id);
|
||||
response.close();
|
||||
}
|
||||
|
||||
private static ComponentRepresentation createTokenSignatureRep(String name, String providerId) {
|
||||
ComponentRepresentation rep = new ComponentRepresentation();
|
||||
rep.setName(name);
|
||||
rep.setParentId(TEST_REALM_NAME);
|
||||
rep.setProviderId(providerId);
|
||||
rep.setProviderType(TokenSignatureProvider.class.getName());
|
||||
rep.setConfig(new MultivaluedHashMap<>());
|
||||
return rep;
|
||||
}
|
||||
|
||||
private static ComponentRepresentation createKeyRep(String name, String providerId) {
|
||||
ComponentRepresentation rep = new ComponentRepresentation();
|
||||
rep.setName(name);
|
||||
rep.setParentId(TEST_REALM_NAME);
|
||||
rep.setProviderId(providerId);
|
||||
rep.setProviderType(KeyProvider.class.getName());
|
||||
rep.setConfig(new MultivaluedHashMap<>());
|
||||
return rep;
|
||||
}
|
||||
|
||||
private static PublicKey getRealmPublicKey(String realm, String sigAlgName, Keycloak adminClient) {
|
||||
KeysMetadataRepresentation keyMetadata = adminClient.realms().realm(realm).keys().getKeyMetadata();
|
||||
String activeKid = keyMetadata.getActive().get(sigAlgName);
|
||||
PublicKey publicKey = null;
|
||||
for (KeysMetadataRepresentation.KeyMetadataRepresentation rep : keyMetadata.getKeys()) {
|
||||
if (rep.getKid().equals(activeKid)) {
|
||||
X509EncodedKeySpec publicKeySpec = null;
|
||||
try {
|
||||
publicKeySpec = new X509EncodedKeySpec(Base64.decode(rep.getPublicKey()));
|
||||
} catch (IOException e1) {
|
||||
e1.printStackTrace();
|
||||
}
|
||||
KeyFactory kf = null;
|
||||
try {
|
||||
kf = KeyFactory.getInstance("EC");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
try {
|
||||
publicKey = kf.generatePublic(publicKeySpec);
|
||||
} catch (InvalidKeySpecException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
private static 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");
|
||||
}
|
||||
}
|
||||
|
||||
private static Signature getSignature(String sigAlgName) {
|
||||
try {
|
||||
// use Bouncy Castle for signature verification intentionally
|
||||
Signature signature = Signature.getInstance(getJavaAlgorithm(sigAlgName), "BC");
|
||||
return signature;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,162 @@
|
|||
package org.keycloak.testsuite.keys;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.ws.rs.WebApplicationException;
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
import org.jboss.arquillian.graphene.page.Page;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.common.util.MultivaluedHashMap;
|
||||
import org.keycloak.crypto.KeyType;
|
||||
import org.keycloak.keys.GeneratedEcdsaKeyProviderFactory;
|
||||
import org.keycloak.keys.KeyProvider;
|
||||
import org.keycloak.representations.idm.ComponentRepresentation;
|
||||
import org.keycloak.representations.idm.ErrorRepresentation;
|
||||
import org.keycloak.representations.idm.KeysMetadataRepresentation;
|
||||
import org.keycloak.representations.idm.KeysMetadataRepresentation.KeyMetadataRepresentation;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.testsuite.AbstractKeycloakTest;
|
||||
import org.keycloak.testsuite.AssertEvents;
|
||||
import org.keycloak.testsuite.admin.ApiUtil;
|
||||
import org.keycloak.testsuite.pages.AppPage;
|
||||
import org.keycloak.testsuite.pages.LoginPage;
|
||||
|
||||
// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI
|
||||
|
||||
public class GeneratedEcdsaKeyProviderTest extends AbstractKeycloakTest {
|
||||
private static final String DEFAULT_EC = "P-256";
|
||||
private static final String ECDSA_ELLIPTIC_CURVE_KEY = "ecdsaEllipticCurveKey";
|
||||
private static final String TEST_REALM_NAME = "test";
|
||||
|
||||
@Rule
|
||||
public AssertEvents events = new AssertEvents(this);
|
||||
|
||||
@Page
|
||||
protected AppPage appPage;
|
||||
|
||||
@Page
|
||||
protected LoginPage loginPage;
|
||||
|
||||
@Override
|
||||
public void addTestRealms(List<RealmRepresentation> testRealms) {
|
||||
RealmRepresentation realm = loadJson(getClass().getResourceAsStream("/testrealm.json"), RealmRepresentation.class);
|
||||
testRealms.add(realm);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultEc() throws Exception {
|
||||
supportedEc(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void supportedEcP521() throws Exception {
|
||||
supportedEc("P-521");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void supportedEcP384() throws Exception {
|
||||
supportedEc("P-384");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void supportedEcP256() throws Exception {
|
||||
supportedEc("P-256");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unsupportedEcK163() throws Exception {
|
||||
// NIST.FIPS.186-4 Koblitz Curve over Binary Field
|
||||
unsupportedEc("K-163");
|
||||
}
|
||||
|
||||
private void supportedEc(String ecInNistRep) {
|
||||
long priority = System.currentTimeMillis();
|
||||
|
||||
ComponentRepresentation rep = createRep("valid", GeneratedEcdsaKeyProviderFactory.ID);
|
||||
rep.setConfig(new MultivaluedHashMap<>());
|
||||
rep.getConfig().putSingle("priority", Long.toString(priority));
|
||||
if (ecInNistRep != null) {
|
||||
rep.getConfig().putSingle(ECDSA_ELLIPTIC_CURVE_KEY, ecInNistRep);
|
||||
} else {
|
||||
ecInNistRep = DEFAULT_EC;
|
||||
}
|
||||
|
||||
Response response = adminClient.realm(TEST_REALM_NAME).components().add(rep);
|
||||
String id = ApiUtil.getCreatedId(response);
|
||||
getCleanup().addComponentId(id);
|
||||
response.close();
|
||||
|
||||
ComponentRepresentation createdRep = adminClient.realm(TEST_REALM_NAME).components().component(id).toRepresentation();
|
||||
|
||||
// stands for the number of properties in the key provider config
|
||||
assertEquals(2, createdRep.getConfig().size());
|
||||
assertEquals(Long.toString(priority), createdRep.getConfig().getFirst("priority"));
|
||||
assertEquals(ecInNistRep, createdRep.getConfig().getFirst(ECDSA_ELLIPTIC_CURVE_KEY));
|
||||
|
||||
KeysMetadataRepresentation keys = adminClient.realm(TEST_REALM_NAME).keys().getKeyMetadata();
|
||||
|
||||
KeysMetadataRepresentation.KeyMetadataRepresentation key = null;
|
||||
|
||||
for (KeyMetadataRepresentation k : keys.getKeys()) {
|
||||
if (KeyType.EC.equals(k.getType()) && id.equals(k.getProviderId())) {
|
||||
key = k;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assertNotNull(key);
|
||||
|
||||
assertEquals(id, key.getProviderId());
|
||||
assertEquals(KeyType.EC, key.getType());
|
||||
assertEquals(priority, key.getProviderPriority());
|
||||
}
|
||||
|
||||
private void unsupportedEc(String ecInNistRep) {
|
||||
long priority = System.currentTimeMillis();
|
||||
|
||||
ComponentRepresentation rep = createRep("valid", GeneratedEcdsaKeyProviderFactory.ID);
|
||||
rep.setConfig(new MultivaluedHashMap<>());
|
||||
rep.getConfig().putSingle("priority", Long.toString(priority));
|
||||
rep.getConfig().putSingle(ECDSA_ELLIPTIC_CURVE_KEY, ecInNistRep);
|
||||
boolean isEcAccepted = true;
|
||||
|
||||
Response response = null;
|
||||
try {
|
||||
response = adminClient.realm(TEST_REALM_NAME).components().add(rep);
|
||||
String id = ApiUtil.getCreatedId(response);
|
||||
getCleanup().addComponentId(id);
|
||||
response.close();
|
||||
} catch (WebApplicationException e) {
|
||||
isEcAccepted = false;
|
||||
} finally {
|
||||
response.close();
|
||||
}
|
||||
assertEquals(isEcAccepted, false);
|
||||
}
|
||||
|
||||
protected void assertErrror(Response response, String error) {
|
||||
if (!response.hasEntity()) {
|
||||
fail("No error message set");
|
||||
}
|
||||
|
||||
ErrorRepresentation errorRepresentation = response.readEntity(ErrorRepresentation.class);
|
||||
assertEquals(error, errorRepresentation.getErrorMessage());
|
||||
response.close();
|
||||
}
|
||||
|
||||
protected ComponentRepresentation createRep(String name, String providerId) {
|
||||
ComponentRepresentation rep = new ComponentRepresentation();
|
||||
rep.setName(name);
|
||||
rep.setParentId(TEST_REALM_NAME);
|
||||
rep.setProviderId(providerId);
|
||||
rep.setProviderType(KeyProvider.class.getName());
|
||||
rep.setConfig(new MultivaluedHashMap<>());
|
||||
return rep;
|
||||
}
|
||||
}
|
|
@ -34,6 +34,7 @@ import org.keycloak.admin.client.resource.ClientScopeResource;
|
|||
import org.keycloak.admin.client.resource.RealmResource;
|
||||
import org.keycloak.admin.client.resource.UserResource;
|
||||
import org.keycloak.common.enums.SslRequired;
|
||||
import org.keycloak.crypto.Algorithm;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.jose.jws.JWSHeader;
|
||||
|
@ -65,6 +66,7 @@ import org.keycloak.testsuite.util.ClientManager;
|
|||
import org.keycloak.testsuite.util.OAuthClient;
|
||||
import org.keycloak.testsuite.util.RealmManager;
|
||||
import org.keycloak.testsuite.util.RoleBuilder;
|
||||
import org.keycloak.testsuite.util.TokenSignatureUtil;
|
||||
import org.keycloak.testsuite.util.UserBuilder;
|
||||
import org.keycloak.testsuite.util.UserInfoClientUtil;
|
||||
import org.keycloak.testsuite.util.UserManager;
|
||||
|
@ -1026,4 +1028,143 @@ public class AccessTokenTest extends AbstractKeycloakTest {
|
|||
.header(HttpHeaders.AUTHORIZATION, header)
|
||||
.post(Entity.form(form));
|
||||
}
|
||||
|
||||
// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI
|
||||
|
||||
@Test
|
||||
public void accessTokenRequest_RealmRS256_ClientRS384_EffectiveRS384() throws Exception {
|
||||
try {
|
||||
TokenSignatureUtil.changeRealmTokenSignatureProvider(adminClient, Algorithm.RS256);
|
||||
TokenSignatureUtil.changeClientTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"), adminClient, Algorithm.RS384);
|
||||
tokenRequest(Algorithm.RS384);
|
||||
} finally {
|
||||
TokenSignatureUtil.changeClientTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"), adminClient, Algorithm.RS256);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void accessTokenRequest_RealmRS512_ClientRS512_EffectiveRS512() throws Exception {
|
||||
try {
|
||||
TokenSignatureUtil.changeRealmTokenSignatureProvider(adminClient, Algorithm.RS512);
|
||||
TokenSignatureUtil.changeClientTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"), adminClient, Algorithm.RS512);
|
||||
tokenRequest(Algorithm.RS512);
|
||||
} finally {
|
||||
TokenSignatureUtil.changeRealmTokenSignatureProvider(adminClient, Algorithm.RS256);
|
||||
TokenSignatureUtil.changeClientTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"), adminClient, Algorithm.RS256);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void accessTokenRequest_RealmRS256_ClientES256_EffectiveES256() throws Exception {
|
||||
try {
|
||||
TokenSignatureUtil.changeRealmTokenSignatureProvider(adminClient, Algorithm.RS256);
|
||||
TokenSignatureUtil.changeClientTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"), adminClient, Algorithm.ES256);
|
||||
TokenSignatureUtil.registerKeyProvider("P-256", adminClient, testContext);
|
||||
TokenSignatureUtil.registerTokenSignatureProvider(Algorithm.ES256, adminClient, testContext);
|
||||
tokenRequestSignatureVerifyOnly(Algorithm.ES256);
|
||||
} finally {
|
||||
TokenSignatureUtil.changeClientTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"), adminClient, Algorithm.RS256);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void accessTokenRequest_RealmES384_ClientES384_EffectiveES384() throws Exception {
|
||||
try {
|
||||
TokenSignatureUtil.changeRealmTokenSignatureProvider(adminClient, Algorithm.ES384);
|
||||
TokenSignatureUtil.changeClientTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"), adminClient, Algorithm.ES384);
|
||||
TokenSignatureUtil.registerKeyProvider("P-384", adminClient, testContext);
|
||||
TokenSignatureUtil.registerTokenSignatureProvider(Algorithm.ES384, adminClient, testContext);
|
||||
tokenRequestSignatureVerifyOnly(Algorithm.ES384);
|
||||
} finally {
|
||||
TokenSignatureUtil.changeRealmTokenSignatureProvider(adminClient, Algorithm.RS256);
|
||||
TokenSignatureUtil.changeClientTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"), adminClient, Algorithm.RS256);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void accessTokenRequest_RealmRS256_ClientES512_EffectiveES512() throws Exception {
|
||||
try {
|
||||
TokenSignatureUtil.changeRealmTokenSignatureProvider(adminClient, Algorithm.RS256);
|
||||
TokenSignatureUtil.changeClientTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"), adminClient, Algorithm.ES512);
|
||||
TokenSignatureUtil.registerKeyProvider("P-521", adminClient, testContext);
|
||||
TokenSignatureUtil.registerTokenSignatureProvider(Algorithm.ES512, adminClient, testContext);
|
||||
tokenRequestSignatureVerifyOnly(Algorithm.ES512);
|
||||
} finally {
|
||||
TokenSignatureUtil.changeClientTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"), adminClient, Algorithm.RS256);
|
||||
}
|
||||
}
|
||||
|
||||
private void tokenRequest(String sigAlgName) throws Exception {
|
||||
oauth.doLogin("test-user@localhost", "password");
|
||||
|
||||
EventRepresentation loginEvent = events.expectLogin().assertEvent();
|
||||
|
||||
String sessionId = loginEvent.getSessionId();
|
||||
String codeId = loginEvent.getDetails().get(Details.CODE_ID);
|
||||
|
||||
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||
OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password");
|
||||
|
||||
assertEquals(200, response.getStatusCode());
|
||||
|
||||
assertEquals("bearer", response.getTokenType());
|
||||
|
||||
JWSHeader header = new JWSInput(response.getAccessToken()).getHeader();
|
||||
assertEquals(sigAlgName, header.getAlgorithm().name());
|
||||
assertEquals("JWT", header.getType());
|
||||
assertNull(header.getContentType());
|
||||
|
||||
header = new JWSInput(response.getIdToken()).getHeader();
|
||||
assertEquals(sigAlgName, header.getAlgorithm().name());
|
||||
assertEquals("JWT", header.getType());
|
||||
assertNull(header.getContentType());
|
||||
|
||||
header = new JWSInput(response.getRefreshToken()).getHeader();
|
||||
assertEquals(sigAlgName, header.getAlgorithm().name());
|
||||
assertEquals("JWT", header.getType());
|
||||
assertNull(header.getContentType());
|
||||
|
||||
AccessToken token = oauth.verifyToken(response.getAccessToken());
|
||||
|
||||
assertEquals(findUserByUsername(adminClient.realm("test"), "test-user@localhost").getId(), token.getSubject());
|
||||
Assert.assertNotEquals("test-user@localhost", token.getSubject());
|
||||
|
||||
assertEquals(sessionId, token.getSessionState());
|
||||
|
||||
EventRepresentation event = events.expectCodeToToken(codeId, sessionId).assertEvent();
|
||||
assertEquals(token.getId(), event.getDetails().get(Details.TOKEN_ID));
|
||||
assertEquals(oauth.verifyRefreshToken(response.getRefreshToken()).getId(), event.getDetails().get(Details.REFRESH_TOKEN_ID));
|
||||
assertEquals(sessionId, token.getSessionState());
|
||||
}
|
||||
|
||||
private void tokenRequestSignatureVerifyOnly(String sigAlgName) throws Exception {
|
||||
oauth.doLogin("test-user@localhost", "password");
|
||||
|
||||
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||
OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password");
|
||||
|
||||
assertEquals(200, response.getStatusCode());
|
||||
|
||||
assertEquals("bearer", response.getTokenType());
|
||||
|
||||
JWSHeader header = new JWSInput(response.getAccessToken()).getHeader();
|
||||
assertEquals(sigAlgName, header.getAlgorithm().name());
|
||||
assertEquals("JWT", header.getType());
|
||||
assertNull(header.getContentType());
|
||||
|
||||
header = new JWSInput(response.getIdToken()).getHeader();
|
||||
assertEquals(sigAlgName, header.getAlgorithm().name());
|
||||
assertEquals("JWT", header.getType());
|
||||
assertNull(header.getContentType());
|
||||
|
||||
header = new JWSInput(response.getRefreshToken()).getHeader();
|
||||
assertEquals(sigAlgName, header.getAlgorithm().name());
|
||||
assertEquals("JWT", header.getType());
|
||||
assertNull(header.getContentType());
|
||||
|
||||
assertEquals(TokenSignatureUtil.verifySignature(sigAlgName, response.getAccessToken(), adminClient), true);
|
||||
assertEquals(TokenSignatureUtil.verifySignature(sigAlgName, response.getIdToken(), adminClient), true);
|
||||
assertEquals(TokenSignatureUtil.verifySignature(sigAlgName, response.getRefreshToken(), adminClient), true);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -23,9 +23,13 @@ import org.junit.Test;
|
|||
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.jose.jws.Algorithm;
|
||||
import org.keycloak.jose.jws.JWSHeader;
|
||||
import org.keycloak.jose.jws.JWSInput;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.testsuite.AbstractKeycloakTest;
|
||||
import org.keycloak.testsuite.AssertEvents;
|
||||
import org.keycloak.testsuite.admin.ApiUtil;
|
||||
import org.keycloak.testsuite.pages.AppPage;
|
||||
import org.keycloak.testsuite.util.*;
|
||||
|
||||
|
@ -176,4 +180,52 @@ public class LogoutTest extends AbstractKeycloakTest {
|
|||
}
|
||||
}
|
||||
|
||||
// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI
|
||||
private void backchannelLogoutRequest(String sigAlgName) throws Exception {
|
||||
oauth.doLogin("test-user@localhost", "password");
|
||||
|
||||
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||
|
||||
oauth.clientSessionState("client-session");
|
||||
OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
|
||||
String idTokenString = tokenResponse.getIdToken();
|
||||
|
||||
JWSHeader header = new JWSInput(tokenResponse.getAccessToken()).getHeader();
|
||||
assertEquals(sigAlgName, header.getAlgorithm().name());
|
||||
assertEquals("JWT", header.getType());
|
||||
assertNull(header.getContentType());
|
||||
|
||||
header = new JWSInput(tokenResponse.getIdToken()).getHeader();
|
||||
assertEquals(sigAlgName, header.getAlgorithm().name());
|
||||
assertEquals("JWT", header.getType());
|
||||
assertNull(header.getContentType());
|
||||
|
||||
header = new JWSInput(tokenResponse.getRefreshToken()).getHeader();
|
||||
assertEquals(sigAlgName, header.getAlgorithm().name());
|
||||
assertEquals("JWT", header.getType());
|
||||
assertNull(header.getContentType());
|
||||
|
||||
String logoutUrl = oauth.getLogoutUrl()
|
||||
.idTokenHint(idTokenString)
|
||||
.postLogoutRedirectUri(AppPage.baseUrl)
|
||||
.build();
|
||||
|
||||
try (CloseableHttpClient c = HttpClientBuilder.create().disableRedirectHandling().build();
|
||||
CloseableHttpResponse response = c.execute(new HttpGet(logoutUrl))) {
|
||||
assertThat(response, Matchers.statusCodeIsHC(Status.FOUND));
|
||||
assertThat(response.getFirstHeader(HttpHeaders.LOCATION).getValue(), is(AppPage.baseUrl));
|
||||
}
|
||||
}
|
||||
@Test
|
||||
public void backchannelLogoutRequest_RealmRS384_ClientRS512_EffectiveRS512() throws Exception {
|
||||
try {
|
||||
TokenSignatureUtil.changeRealmTokenSignatureProvider(adminClient, "RS384");
|
||||
TokenSignatureUtil.changeClientTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"), adminClient, "RS512");
|
||||
backchannelLogoutRequest("RS512");
|
||||
} finally {
|
||||
TokenSignatureUtil.changeRealmTokenSignatureProvider(adminClient, "RS256");
|
||||
TokenSignatureUtil.changeClientTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"), adminClient, "RS256");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -33,6 +33,9 @@ import org.keycloak.common.constants.ServiceAccountConstants;
|
|||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.jose.jws.Algorithm;
|
||||
import org.keycloak.jose.jws.JWSHeader;
|
||||
import org.keycloak.jose.jws.JWSInput;
|
||||
import org.keycloak.models.AdminRoles;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
|
@ -60,6 +63,7 @@ import org.keycloak.testsuite.util.OAuthClient;
|
|||
import org.keycloak.testsuite.util.RealmBuilder;
|
||||
import org.keycloak.testsuite.util.RealmManager;
|
||||
import org.keycloak.testsuite.util.RoleBuilder;
|
||||
import org.keycloak.testsuite.util.TokenSignatureUtil;
|
||||
import org.keycloak.testsuite.util.UserBuilder;
|
||||
import org.keycloak.util.TokenUtil;
|
||||
|
||||
|
@ -72,6 +76,7 @@ import static org.junit.Assert.assertEquals;
|
|||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
|
||||
import static org.keycloak.testsuite.admin.ApiUtil.findRealmRoleByName;
|
||||
import static org.keycloak.testsuite.admin.ApiUtil.findUserByUsername;
|
||||
|
@ -747,4 +752,86 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
|||
changeOfflineSessionSettings(false, prev[0], prev[1]);
|
||||
}
|
||||
}
|
||||
|
||||
// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI
|
||||
private void offlineTokenRequest(String sigAlgName) throws Exception {
|
||||
oauth.scope(OAuth2Constants.OFFLINE_ACCESS);
|
||||
oauth.clientId("offline-client");
|
||||
OAuthClient.AccessTokenResponse tokenResponse = oauth.doClientCredentialsGrantAccessTokenRequest("secret1");
|
||||
|
||||
JWSHeader header = null;
|
||||
String idToken = tokenResponse.getIdToken();
|
||||
String accessToken = tokenResponse.getAccessToken();
|
||||
String refreshToken = tokenResponse.getRefreshToken();
|
||||
if (idToken != null) {
|
||||
header = new JWSInput(idToken).getHeader();
|
||||
assertEquals(sigAlgName, header.getAlgorithm().name());
|
||||
assertEquals("JWT", header.getType());
|
||||
assertNull(header.getContentType());
|
||||
}
|
||||
if (accessToken != null) {
|
||||
header = new JWSInput(accessToken).getHeader();
|
||||
assertEquals(sigAlgName, header.getAlgorithm().name());
|
||||
assertEquals("JWT", header.getType());
|
||||
assertNull(header.getContentType());
|
||||
}
|
||||
if (refreshToken != null) {
|
||||
header = new JWSInput(refreshToken).getHeader();
|
||||
assertEquals(sigAlgName, header.getAlgorithm().name());
|
||||
assertEquals("JWT", header.getType());
|
||||
assertNull(header.getContentType());
|
||||
}
|
||||
|
||||
AccessToken token = oauth.verifyToken(tokenResponse.getAccessToken());
|
||||
String offlineTokenString = tokenResponse.getRefreshToken();
|
||||
RefreshToken offlineToken = oauth.verifyRefreshToken(offlineTokenString);
|
||||
|
||||
events.expectClientLogin()
|
||||
.client("offline-client")
|
||||
.user(serviceAccountUserId)
|
||||
.session(token.getSessionState())
|
||||
.detail(Details.TOKEN_ID, token.getId())
|
||||
.detail(Details.REFRESH_TOKEN_ID, offlineToken.getId())
|
||||
.detail(Details.REFRESH_TOKEN_TYPE, TokenUtil.TOKEN_TYPE_OFFLINE)
|
||||
.detail(Details.USERNAME, ServiceAccountConstants.SERVICE_ACCOUNT_USER_PREFIX + "offline-client")
|
||||
.assertEvent();
|
||||
|
||||
Assert.assertEquals(TokenUtil.TOKEN_TYPE_OFFLINE, offlineToken.getType());
|
||||
Assert.assertEquals(0, offlineToken.getExpiration());
|
||||
|
||||
testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), serviceAccountUserId);
|
||||
|
||||
// Now retrieve another offline token and verify that previous offline token is still valid
|
||||
tokenResponse = oauth.doClientCredentialsGrantAccessTokenRequest("secret1");
|
||||
|
||||
AccessToken token2 = oauth.verifyToken(tokenResponse.getAccessToken());
|
||||
String offlineTokenString2 = tokenResponse.getRefreshToken();
|
||||
RefreshToken offlineToken2 = oauth.verifyRefreshToken(offlineTokenString2);
|
||||
|
||||
events.expectClientLogin()
|
||||
.client("offline-client")
|
||||
.user(serviceAccountUserId)
|
||||
.session(token2.getSessionState())
|
||||
.detail(Details.TOKEN_ID, token2.getId())
|
||||
.detail(Details.REFRESH_TOKEN_ID, offlineToken2.getId())
|
||||
.detail(Details.REFRESH_TOKEN_TYPE, TokenUtil.TOKEN_TYPE_OFFLINE)
|
||||
.detail(Details.USERNAME, ServiceAccountConstants.SERVICE_ACCOUNT_USER_PREFIX + "offline-client")
|
||||
.assertEvent();
|
||||
|
||||
// Refresh with both offline tokens is fine
|
||||
testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), serviceAccountUserId);
|
||||
testRefreshWithOfflineToken(token2, offlineToken2, offlineTokenString2, token2.getSessionState(), serviceAccountUserId);
|
||||
|
||||
}
|
||||
@Test
|
||||
public void offlineTokenRequest_RealmRS512_ClientRS384_EffectiveRS384() throws Exception {
|
||||
try {
|
||||
TokenSignatureUtil.changeRealmTokenSignatureProvider(adminClient, "RS512");
|
||||
TokenSignatureUtil.changeClientTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "offline-client"), adminClient, "RS384");
|
||||
offlineTokenRequest("RS384");
|
||||
} finally {
|
||||
TokenSignatureUtil.changeRealmTokenSignatureProvider(adminClient, "RS256");
|
||||
TokenSignatureUtil.changeClientTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "offline-client"), adminClient, "RS256");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,8 +23,11 @@ import org.junit.Test;
|
|||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.admin.client.resource.RealmResource;
|
||||
import org.keycloak.common.enums.SslRequired;
|
||||
import org.keycloak.crypto.Algorithm;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.jose.jws.JWSHeader;
|
||||
import org.keycloak.jose.jws.JWSInput;
|
||||
import org.keycloak.models.utils.SessionTimeoutHelper;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
|
||||
import org.keycloak.representations.AccessToken;
|
||||
|
@ -33,10 +36,12 @@ import org.keycloak.representations.idm.EventRepresentation;
|
|||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.testsuite.AbstractKeycloakTest;
|
||||
import org.keycloak.testsuite.AssertEvents;
|
||||
import org.keycloak.testsuite.admin.ApiUtil;
|
||||
import org.keycloak.testsuite.util.ClientManager;
|
||||
import org.keycloak.testsuite.util.OAuthClient;
|
||||
import org.keycloak.testsuite.util.RealmBuilder;
|
||||
import org.keycloak.testsuite.util.RealmManager;
|
||||
import org.keycloak.testsuite.util.TokenSignatureUtil;
|
||||
import org.keycloak.testsuite.util.UserManager;
|
||||
import org.keycloak.util.BasicAuthHelper;
|
||||
|
||||
|
@ -48,6 +53,7 @@ import javax.ws.rs.core.Form;
|
|||
import javax.ws.rs.core.HttpHeaders;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriBuilder;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
|
||||
|
@ -751,5 +757,189 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
|
|||
.post(Entity.form(form));
|
||||
}
|
||||
|
||||
// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI
|
||||
|
||||
private void refreshToken(String sigAlgName) throws Exception {
|
||||
oauth.doLogin("test-user@localhost", "password");
|
||||
|
||||
EventRepresentation loginEvent = events.expectLogin().assertEvent();
|
||||
|
||||
String sessionId = loginEvent.getSessionId();
|
||||
String codeId = loginEvent.getDetails().get(Details.CODE_ID);
|
||||
|
||||
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||
|
||||
OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
|
||||
|
||||
JWSHeader header = new JWSInput(tokenResponse.getAccessToken()).getHeader();
|
||||
assertEquals(sigAlgName, header.getAlgorithm().name());
|
||||
assertEquals("JWT", header.getType());
|
||||
assertNull(header.getContentType());
|
||||
|
||||
header = new JWSInput(tokenResponse.getIdToken()).getHeader();
|
||||
assertEquals(sigAlgName, header.getAlgorithm().name());
|
||||
assertEquals("JWT", header.getType());
|
||||
assertNull(header.getContentType());
|
||||
|
||||
header = new JWSInput(tokenResponse.getRefreshToken()).getHeader();
|
||||
assertEquals(sigAlgName, header.getAlgorithm().name());
|
||||
assertEquals("JWT", header.getType());
|
||||
assertNull(header.getContentType());
|
||||
|
||||
AccessToken token = oauth.verifyToken(tokenResponse.getAccessToken());
|
||||
String refreshTokenString = tokenResponse.getRefreshToken();
|
||||
RefreshToken refreshToken = oauth.verifyRefreshToken(refreshTokenString);
|
||||
|
||||
EventRepresentation tokenEvent = events.expectCodeToToken(codeId, sessionId).assertEvent();
|
||||
|
||||
Assert.assertNotNull(refreshTokenString);
|
||||
|
||||
assertEquals("bearer", tokenResponse.getTokenType());
|
||||
|
||||
assertEquals(sessionId, refreshToken.getSessionState());
|
||||
|
||||
setTimeOffset(2);
|
||||
|
||||
OAuthClient.AccessTokenResponse response = oauth.doRefreshTokenRequest(refreshTokenString, "password");
|
||||
AccessToken refreshedToken = oauth.verifyToken(response.getAccessToken());
|
||||
RefreshToken refreshedRefreshToken = oauth.verifyRefreshToken(response.getRefreshToken());
|
||||
|
||||
assertEquals(200, response.getStatusCode());
|
||||
|
||||
assertEquals(sessionId, refreshedToken.getSessionState());
|
||||
assertEquals(sessionId, refreshedRefreshToken.getSessionState());
|
||||
|
||||
Assert.assertNotEquals(token.getId(), refreshedToken.getId());
|
||||
Assert.assertNotEquals(refreshToken.getId(), refreshedRefreshToken.getId());
|
||||
|
||||
assertEquals("bearer", response.getTokenType());
|
||||
|
||||
assertEquals(findUserByUsername(adminClient.realm("test"), "test-user@localhost").getId(), refreshedToken.getSubject());
|
||||
Assert.assertNotEquals("test-user@localhost", refreshedToken.getSubject());
|
||||
|
||||
EventRepresentation refreshEvent = events.expectRefresh(tokenEvent.getDetails().get(Details.REFRESH_TOKEN_ID), sessionId).assertEvent();
|
||||
Assert.assertNotEquals(tokenEvent.getDetails().get(Details.TOKEN_ID), refreshEvent.getDetails().get(Details.TOKEN_ID));
|
||||
Assert.assertNotEquals(tokenEvent.getDetails().get(Details.REFRESH_TOKEN_ID), refreshEvent.getDetails().get(Details.UPDATED_REFRESH_TOKEN_ID));
|
||||
|
||||
setTimeOffset(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void tokenRefreshRequest_RealmRS384_ClientRS384_EffectiveRS384() throws Exception {
|
||||
try {
|
||||
TokenSignatureUtil.changeRealmTokenSignatureProvider(adminClient, Algorithm.RS384);
|
||||
TokenSignatureUtil.changeClientTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"), adminClient, Algorithm.RS384);
|
||||
refreshToken(Algorithm.RS384);
|
||||
} finally {
|
||||
TokenSignatureUtil.changeRealmTokenSignatureProvider(adminClient, Algorithm.RS256);
|
||||
TokenSignatureUtil.changeClientTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"), adminClient, Algorithm.RS256);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void tokenRefreshRequest_RealmRS256_ClientRS512_EffectiveRS512() throws Exception {
|
||||
try {
|
||||
TokenSignatureUtil.changeRealmTokenSignatureProvider(adminClient, Algorithm.RS256);
|
||||
TokenSignatureUtil.changeClientTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"), adminClient, Algorithm.RS512);
|
||||
refreshToken(Algorithm.RS512);
|
||||
} finally {
|
||||
TokenSignatureUtil.changeClientTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"), adminClient, Algorithm.RS256);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void tokenRefreshRequest_RealmRS256_ClientES256_EffectiveES256() throws Exception {
|
||||
try {
|
||||
TokenSignatureUtil.changeRealmTokenSignatureProvider(adminClient, Algorithm.RS256);
|
||||
TokenSignatureUtil.changeClientTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"), adminClient, Algorithm.ES256);
|
||||
TokenSignatureUtil.registerKeyProvider("P-256", adminClient, testContext);
|
||||
TokenSignatureUtil.registerTokenSignatureProvider(Algorithm.ES256, adminClient, testContext);
|
||||
refreshTokenSignatureVerifyOnly(Algorithm.ES256);
|
||||
} finally {
|
||||
TokenSignatureUtil.changeClientTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"), adminClient, Algorithm.RS256);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void tokenRefreshRequest_RealmES384_ClientES384_EffectiveES384() throws Exception {
|
||||
try {
|
||||
TokenSignatureUtil.changeRealmTokenSignatureProvider(adminClient, Algorithm.ES384);
|
||||
TokenSignatureUtil.changeClientTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"), adminClient, Algorithm.ES384);
|
||||
TokenSignatureUtil.registerKeyProvider("P-384", adminClient, testContext);
|
||||
TokenSignatureUtil.registerTokenSignatureProvider(Algorithm.ES384, adminClient, testContext);
|
||||
refreshTokenSignatureVerifyOnly(Algorithm.ES384);
|
||||
} finally {
|
||||
TokenSignatureUtil.changeRealmTokenSignatureProvider(adminClient, Algorithm.RS256);
|
||||
TokenSignatureUtil.changeClientTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"), adminClient, Algorithm.RS256);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void tokenRefreshRequest_RealmRS256_ClientES512_EffectiveES512() throws Exception {
|
||||
try {
|
||||
TokenSignatureUtil.changeRealmTokenSignatureProvider(adminClient, Algorithm.RS256);
|
||||
TokenSignatureUtil.changeClientTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"), adminClient, Algorithm.ES512);
|
||||
TokenSignatureUtil.registerKeyProvider("P-521", adminClient, testContext);
|
||||
TokenSignatureUtil.registerTokenSignatureProvider(Algorithm.ES512, adminClient, testContext);
|
||||
refreshTokenSignatureVerifyOnly(Algorithm.ES512);
|
||||
} finally {
|
||||
TokenSignatureUtil.changeClientTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"), adminClient, Algorithm.RS256);
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshTokenSignatureVerifyOnly(String sigAlgName) throws Exception {
|
||||
oauth.doLogin("test-user@localhost", "password");
|
||||
|
||||
EventRepresentation loginEvent = events.expectLogin().assertEvent();
|
||||
|
||||
String sessionId = loginEvent.getSessionId();
|
||||
String codeId = loginEvent.getDetails().get(Details.CODE_ID);
|
||||
|
||||
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||
|
||||
OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
|
||||
|
||||
JWSHeader header = new JWSInput(tokenResponse.getAccessToken()).getHeader();
|
||||
assertEquals(sigAlgName, header.getAlgorithm().name());
|
||||
assertEquals("JWT", header.getType());
|
||||
assertNull(header.getContentType());
|
||||
|
||||
header = new JWSInput(tokenResponse.getIdToken()).getHeader();
|
||||
assertEquals(sigAlgName, header.getAlgorithm().name());
|
||||
assertEquals("JWT", header.getType());
|
||||
assertNull(header.getContentType());
|
||||
|
||||
header = new JWSInput(tokenResponse.getRefreshToken()).getHeader();
|
||||
assertEquals(sigAlgName, header.getAlgorithm().name());
|
||||
assertEquals("JWT", header.getType());
|
||||
assertNull(header.getContentType());
|
||||
|
||||
String refreshTokenString = tokenResponse.getRefreshToken();
|
||||
|
||||
EventRepresentation tokenEvent = events.expectCodeToToken(codeId, sessionId).assertEvent();
|
||||
|
||||
Assert.assertNotNull(refreshTokenString);
|
||||
|
||||
assertEquals("bearer", tokenResponse.getTokenType());
|
||||
|
||||
setTimeOffset(2);
|
||||
|
||||
OAuthClient.AccessTokenResponse response = oauth.doRefreshTokenRequest(refreshTokenString, "password");
|
||||
|
||||
assertEquals(200, response.getStatusCode());
|
||||
|
||||
assertEquals("bearer", response.getTokenType());
|
||||
|
||||
// verify JWS for refreshed access token and refresh token
|
||||
assertEquals(TokenSignatureUtil.verifySignature(sigAlgName, response.getAccessToken(), adminClient), true);
|
||||
assertEquals(TokenSignatureUtil.verifySignature(sigAlgName, response.getIdToken(), adminClient), true);
|
||||
assertEquals(TokenSignatureUtil.verifySignature(sigAlgName, response.getRefreshToken(), adminClient), true);
|
||||
|
||||
EventRepresentation refreshEvent = events.expectRefresh(tokenEvent.getDetails().get(Details.REFRESH_TOKEN_ID), sessionId).assertEvent();
|
||||
Assert.assertNotEquals(tokenEvent.getDetails().get(Details.TOKEN_ID), refreshEvent.getDetails().get(Details.TOKEN_ID));
|
||||
Assert.assertNotEquals(tokenEvent.getDetails().get(Details.REFRESH_TOKEN_ID), refreshEvent.getDetails().get(Details.UPDATED_REFRESH_TOKEN_ID));
|
||||
|
||||
setTimeOffset(0);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -24,6 +24,8 @@ import org.keycloak.OAuthErrorException;
|
|||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.jose.jws.Algorithm;
|
||||
import org.keycloak.jose.jws.JWSHeader;
|
||||
import org.keycloak.jose.jws.JWSInput;
|
||||
import org.keycloak.representations.IDToken;
|
||||
import org.keycloak.representations.idm.EventRepresentation;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
|
@ -31,10 +33,12 @@ import org.keycloak.testsuite.Assert;
|
|||
import org.keycloak.testsuite.AssertEvents;
|
||||
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
|
||||
import org.keycloak.testsuite.admin.AbstractAdminTest;
|
||||
import org.keycloak.testsuite.admin.ApiUtil;
|
||||
import org.keycloak.testsuite.pages.AppPage;
|
||||
import org.keycloak.testsuite.pages.LoginPage;
|
||||
import org.keycloak.testsuite.util.ClientManager;
|
||||
import org.keycloak.testsuite.util.OAuthClient;
|
||||
import org.keycloak.testsuite.util.TokenSignatureUtil;
|
||||
|
||||
import javax.ws.rs.core.UriBuilder;
|
||||
import java.io.IOException;
|
||||
|
@ -42,6 +46,8 @@ import java.util.List;
|
|||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
|
||||
/**
|
||||
* Abstract test for various values of response_type
|
||||
|
@ -214,4 +220,54 @@ public abstract class AbstractOIDCResponseTypeTest extends AbstractTestRealmKeyc
|
|||
protected ClientManager.ClientManagerBuilder clientManagerBuilder() {
|
||||
return ClientManager.realm(adminClient.realm("test")).clientId("test-app");
|
||||
}
|
||||
|
||||
// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI
|
||||
private void oidcFlow(String sigAlgName) throws Exception {
|
||||
EventRepresentation loginEvent = loginUser("abcdef123456");
|
||||
|
||||
OAuthClient.AuthorizationEndpointResponse authzResponse = new OAuthClient.AuthorizationEndpointResponse(oauth, isFragment());
|
||||
Assert.assertNotNull(authzResponse.getSessionState());
|
||||
|
||||
JWSHeader header = null;
|
||||
String idToken = authzResponse.getIdToken();
|
||||
String accessToken = authzResponse.getAccessToken();
|
||||
if (idToken != null) {
|
||||
header = new JWSInput(idToken).getHeader();
|
||||
assertEquals(sigAlgName, header.getAlgorithm().name());
|
||||
assertEquals("JWT", header.getType());
|
||||
assertNull(header.getContentType());
|
||||
}
|
||||
if (accessToken != null) {
|
||||
header = new JWSInput(accessToken).getHeader();
|
||||
assertEquals(sigAlgName, header.getAlgorithm().name());
|
||||
assertEquals("JWT", header.getType());
|
||||
assertNull(header.getContentType());
|
||||
}
|
||||
|
||||
List<IDToken> idTokens = testAuthzResponseAndRetrieveIDTokens(authzResponse, loginEvent);
|
||||
|
||||
for (IDToken idt : idTokens) {
|
||||
Assert.assertEquals("abcdef123456", idt.getNonce());
|
||||
Assert.assertEquals(authzResponse.getSessionState(), idt.getSessionState());
|
||||
}
|
||||
}
|
||||
@Test
|
||||
public void oidcFlow_RealmRS256_ClientRS384_EffectiveRS384() throws Exception {
|
||||
try {
|
||||
setSignatureAlgorithm("RS384");
|
||||
TokenSignatureUtil.changeRealmTokenSignatureProvider(adminClient, "RS256");
|
||||
TokenSignatureUtil.changeClientTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"), adminClient, "RS384");
|
||||
oidcFlow("RS384");
|
||||
} finally {
|
||||
setSignatureAlgorithm("RS256");
|
||||
TokenSignatureUtil.changeClientTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"), adminClient, "RS256");
|
||||
}
|
||||
}
|
||||
private String sigAlgName = "RS256";
|
||||
private void setSignatureAlgorithm(String sigAlgName) {
|
||||
this.sigAlgName = sigAlgName;
|
||||
}
|
||||
protected String getSignatureAlgorithm() {
|
||||
return this.sigAlgName;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -63,13 +63,15 @@ public class OIDCHybridResponseTypeCodeIDTokenTest extends AbstractOIDCResponseT
|
|||
// Validate "c_hash"
|
||||
Assert.assertNull(idToken.getAccessTokenHash());
|
||||
Assert.assertNotNull(idToken.getCodeHash());
|
||||
Assert.assertEquals(idToken.getCodeHash(), HashProvider.oidcHash(jwsAlgorithm, authzResponse.getCode()));
|
||||
// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI
|
||||
Assert.assertEquals(idToken.getCodeHash(), HashProvider.oidcHash(getSignatureAlgorithm(), authzResponse.getCode()));
|
||||
|
||||
// Financial API - Part 2: Read and Write API Security Profile
|
||||
// http://openid.net/specs/openid-financial-api-part-2.html#authorization-server
|
||||
// Validate "s_hash"
|
||||
Assert.assertNotNull(idToken.getStateHash());
|
||||
Assert.assertEquals(idToken.getStateHash(), HashProvider.oidcHash(jwsAlgorithm, authzResponse.getState()));
|
||||
// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI
|
||||
Assert.assertEquals(idToken.getStateHash(), HashProvider.oidcHash(getSignatureAlgorithm(), authzResponse.getState()));
|
||||
|
||||
// IDToken exchanged for the code
|
||||
IDToken idToken2 = sendTokenRequestAndGetIDToken(loginEvent);
|
||||
|
|
|
@ -62,16 +62,19 @@ public class OIDCHybridResponseTypeCodeIDTokenTokenTest extends AbstractOIDCResp
|
|||
|
||||
// Validate "at_hash" and "c_hash"
|
||||
Assert.assertNotNull(idToken.getAccessTokenHash());
|
||||
Assert.assertEquals(idToken.getAccessTokenHash(), HashProvider.oidcHash(jwsAlgorithm, authzResponse.getAccessToken()));
|
||||
// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI
|
||||
Assert.assertEquals(idToken.getAccessTokenHash(), HashProvider.oidcHash(getSignatureAlgorithm(), authzResponse.getAccessToken()));
|
||||
Assert.assertNotNull(idToken.getCodeHash());
|
||||
Assert.assertEquals(idToken.getCodeHash(), HashProvider.oidcHash(jwsAlgorithm, authzResponse.getCode()));
|
||||
// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI
|
||||
Assert.assertEquals(idToken.getCodeHash(), HashProvider.oidcHash(getSignatureAlgorithm(), authzResponse.getCode()));
|
||||
|
||||
// Financial API - Part 2: Read and Write API Security Profile
|
||||
// http://openid.net/specs/openid-financial-api-part-2.html#authorization-server
|
||||
// Validate "s_hash"
|
||||
Assert.assertNotNull(idToken.getStateHash());
|
||||
Assert.assertEquals(idToken.getStateHash(), HashProvider.oidcHash(jwsAlgorithm, authzResponse.getState()));
|
||||
|
||||
// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI
|
||||
Assert.assertEquals(idToken.getStateHash(), HashProvider.oidcHash(getSignatureAlgorithm(), authzResponse.getState()));
|
||||
|
||||
// IDToken exchanged for the code
|
||||
IDToken idToken2 = sendTokenRequestAndGetIDToken(loginEvent);
|
||||
|
||||
|
|
|
@ -61,7 +61,8 @@ public class OIDCImplicitResponseTypeIDTokenTokenTest extends AbstractOIDCRespon
|
|||
|
||||
// Validate "at_hash"
|
||||
Assert.assertNotNull(idToken.getAccessTokenHash());
|
||||
Assert.assertEquals(idToken.getAccessTokenHash(), HashProvider.oidcHash(jwsAlgorithm, authzResponse.getAccessToken()));
|
||||
// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI
|
||||
Assert.assertEquals(idToken.getAccessTokenHash(), HashProvider.oidcHash(getSignatureAlgorithm(), authzResponse.getAccessToken()));
|
||||
Assert.assertNull(idToken.getCodeHash());
|
||||
|
||||
return Collections.singletonList(idToken);
|
||||
|
|
Loading…
Reference in a new issue