KEYCLOAK-4092 key provider for HMAC signatures

This commit is contained in:
Stian Thorgersen 2016-12-15 11:46:15 +01:00
parent a4cbf130b4
commit f29bb7d501
65 changed files with 1578 additions and 329 deletions

View file

@ -17,6 +17,8 @@
package org.keycloak.common.util;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.security.Key;
import java.security.KeyFactory;
import java.security.KeyPair;
@ -38,6 +40,10 @@ public class KeyUtils {
private KeyUtils() {
}
public static SecretKey loadSecretKey(String secret) {
return new SecretKeySpec(secret.getBytes(), "HmacSHA256");
}
public static KeyPair generateRsaKeyPair(int keysize) {
try {
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");

View file

@ -19,11 +19,7 @@ package org.keycloak;
import org.keycloak.common.VerificationException;
import org.keycloak.jose.jws.JWSHeader;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.JWSInputException;
import org.keycloak.jose.jws.crypto.RSAProvider;
import org.keycloak.representations.AccessToken;
import org.keycloak.util.TokenUtil;
import java.security.PublicKey;
@ -33,18 +29,10 @@ import java.security.PublicKey;
*/
public class RSATokenVerifier {
private final String tokenString;
private PublicKey publicKey;
private String realmUrl;
private boolean checkTokenType = true;
private boolean checkActive = true;
private boolean checkRealmUrl = true;
private JWSInput jws;
private AccessToken token;
private TokenVerifier tokenVerifier;
private RSATokenVerifier(String tokenString) {
this.tokenString = tokenString;
this.tokenVerifier = TokenVerifier.create(tokenString);
}
public static RSATokenVerifier create(String tokenString) {
@ -60,94 +48,45 @@ public class RSATokenVerifier {
}
public RSATokenVerifier publicKey(PublicKey publicKey) {
this.publicKey = publicKey;
tokenVerifier.publicKey(publicKey);
return this;
}
public RSATokenVerifier realmUrl(String realmUrl) {
this.realmUrl = realmUrl;
tokenVerifier.realmUrl(realmUrl);
return this;
}
public RSATokenVerifier checkTokenType(boolean checkTokenType) {
this.checkTokenType = checkTokenType;
tokenVerifier.checkTokenType(checkTokenType);
return this;
}
public RSATokenVerifier checkActive(boolean checkActive) {
this.checkActive = checkActive;
tokenVerifier.checkActive(checkActive);
return this;
}
public RSATokenVerifier checkRealmUrl(boolean checkRealmUrl) {
this.checkRealmUrl = checkRealmUrl;
tokenVerifier.checkRealmUrl(checkRealmUrl);
return this;
}
public RSATokenVerifier parse() throws VerificationException {
if (jws == null) {
if (tokenString == null) {
throw new VerificationException("Token not set");
}
try {
jws = new JWSInput(tokenString);
} catch (JWSInputException e) {
throw new VerificationException("Failed to parse JWT", e);
}
try {
token = jws.readJsonContent(AccessToken.class);
} catch (JWSInputException e) {
throw new VerificationException("Failed to read access token from JWT", e);
}
}
tokenVerifier.parse();
return this;
}
public AccessToken getToken() throws VerificationException {
parse();
return token;
return tokenVerifier.getToken();
}
public JWSHeader getHeader() throws VerificationException {
parse();
return jws.getHeader();
return tokenVerifier.getHeader();
}
public RSATokenVerifier verify() throws VerificationException {
parse();
if (publicKey == null) {
throw new VerificationException("Public key not set");
}
if (checkRealmUrl && realmUrl == null) {
throw new VerificationException("Realm URL not set");
}
if (!RSAProvider.verify(jws, publicKey)) {
throw new VerificationException("Invalid token signature");
}
String user = token.getSubject();
if (user == null) {
throw new VerificationException("Subject missing in token");
}
if (checkRealmUrl && !realmUrl.equals(token.getIssuer())) {
throw new VerificationException("Invalid token issuer. Expected '" + realmUrl + "', but was '" + token.getIssuer() + "'");
}
if (checkTokenType && !TokenUtil.TOKEN_TYPE_BEARER.equalsIgnoreCase(token.getType())) {
throw new VerificationException("Token type is incorrect. Expected '" + TokenUtil.TOKEN_TYPE_BEARER + "' but was '" + token.getType() + "'");
}
if (checkActive && !token.isActive()) {
throw new VerificationException("Token is not active");
}
tokenVerifier.verify();
return this;
}

View file

@ -0,0 +1,170 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak;
import org.keycloak.common.VerificationException;
import org.keycloak.jose.jws.Algorithm;
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.crypto.HMACProvider;
import org.keycloak.jose.jws.crypto.RSAProvider;
import org.keycloak.representations.AccessToken;
import org.keycloak.util.TokenUtil;
import javax.crypto.SecretKey;
import java.security.PublicKey;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class TokenVerifier {
private final String tokenString;
private PublicKey publicKey;
private SecretKey secretKey;
private String realmUrl;
private boolean checkTokenType = true;
private boolean checkActive = true;
private boolean checkRealmUrl = true;
private JWSInput jws;
private AccessToken token;
protected TokenVerifier(String tokenString) {
this.tokenString = tokenString;
}
public static TokenVerifier create(String tokenString) {
return new TokenVerifier(tokenString);
}
public TokenVerifier publicKey(PublicKey publicKey) {
this.publicKey = publicKey;
return this;
}
public TokenVerifier secretKey(SecretKey secretKey) {
this.secretKey = secretKey;
return this;
}
public TokenVerifier realmUrl(String realmUrl) {
this.realmUrl = realmUrl;
return this;
}
public TokenVerifier checkTokenType(boolean checkTokenType) {
this.checkTokenType = checkTokenType;
return this;
}
public TokenVerifier checkActive(boolean checkActive) {
this.checkActive = checkActive;
return this;
}
public TokenVerifier checkRealmUrl(boolean checkRealmUrl) {
this.checkRealmUrl = checkRealmUrl;
return this;
}
public TokenVerifier parse() throws VerificationException {
if (jws == null) {
if (tokenString == null) {
throw new VerificationException("Token not set");
}
try {
jws = new JWSInput(tokenString);
} catch (JWSInputException e) {
throw new VerificationException("Failed to parse JWT", e);
}
try {
token = jws.readJsonContent(AccessToken.class);
} catch (JWSInputException e) {
throw new VerificationException("Failed to read access token from JWT", e);
}
}
return this;
}
public AccessToken getToken() throws VerificationException {
parse();
return token;
}
public JWSHeader getHeader() throws VerificationException {
parse();
return jws.getHeader();
}
public TokenVerifier verify() throws VerificationException {
parse();
if (checkRealmUrl && realmUrl == null) {
throw new VerificationException("Realm URL not set");
}
AlgorithmType algorithmType = getHeader().getAlgorithm().getType();
if (AlgorithmType.RSA.equals(algorithmType)) {
if (publicKey == null) {
throw new VerificationException("Public key not set");
}
if (!RSAProvider.verify(jws, publicKey)) {
throw new VerificationException("Invalid token signature");
}
} else if (AlgorithmType.HMAC.equals(algorithmType)) {
if (secretKey == null) {
throw new VerificationException("Secret key not set");
}
if (!HMACProvider.verify(jws, secretKey)) {
throw new VerificationException("Invalid token signature");
}
} else {
throw new VerificationException("Unknown or unsupported token algorith");
}
String user = token.getSubject();
if (user == null) {
throw new VerificationException("Subject missing in token");
}
if (checkRealmUrl && !realmUrl.equals(token.getIssuer())) {
throw new VerificationException("Invalid token issuer. Expected '" + realmUrl + "', but was '" + token.getIssuer() + "'");
}
if (checkTokenType && !TokenUtil.TOKEN_TYPE_BEARER.equalsIgnoreCase(token.getType())) {
throw new VerificationException("Token type is incorrect. Expected '" + TokenUtil.TOKEN_TYPE_BEARER + "' but was '" + token.getType() + "'");
}
if (checkActive && !token.isActive()) {
throw new VerificationException("Token is not active");
}
return this;
}
}

View file

@ -26,23 +26,30 @@ import org.keycloak.jose.jws.crypto.SignatureProvider;
*/
public enum Algorithm {
none(null),
HS256(null),
HS384(null),
HS512(null),
RS256(new RSAProvider()),
RS384(new RSAProvider()),
RS512(new RSAProvider()),
ES256(null),
ES384(null),
ES512(null)
none(null, null),
HS256(AlgorithmType.HMAC, null),
HS384(AlgorithmType.HMAC, null),
HS512(AlgorithmType.HMAC, null),
RS256(AlgorithmType.RSA, new RSAProvider()),
RS384(AlgorithmType.RSA, new RSAProvider()),
RS512(AlgorithmType.RSA, new RSAProvider()),
ES256(AlgorithmType.ECDSA, null),
ES384(AlgorithmType.ECDSA, null),
ES512(AlgorithmType.ECDSA, null)
;
private AlgorithmType type;
private SignatureProvider provider;
Algorithm(SignatureProvider provider) {
Algorithm(AlgorithmType type, SignatureProvider provider) {
this.type = type;
this.provider = provider;
}
public AlgorithmType getType() {
return type;
}
public SignatureProvider getProvider() {
return provider;
}

View file

@ -0,0 +1,30 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.jose.jws;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public enum AlgorithmType {
RSA,
HMAC,
ECDSA
}

View file

@ -0,0 +1,52 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.keys;
import org.keycloak.jose.jws.AlgorithmType;
import javax.crypto.SecretKey;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.X509Certificate;
import java.util.List;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public interface HmacKeyProvider extends KeyProvider<HmacKeyMetadata> {
default AlgorithmType getType() {
return AlgorithmType.HMAC;
}
/**
* Return the active secret key, or <code>null</code> if no active key is available.
*
* @return
*/
SecretKey getSecretKey();
/**
* Return the secret key for the specified kid, or <code>null</code> if the kid is unknown.
*
* @param kid
* @return
*/
SecretKey getSecretKey(String kid);
}

View file

@ -0,0 +1,35 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.keys;
import org.keycloak.jose.jws.AlgorithmType;
import java.util.Collections;
import java.util.Map;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public interface HmacKeyProviderFactory extends KeyProviderFactory {
@Override
default Map<String, Object> getTypeMetadata() {
return Collections.singletonMap("algorithmType", AlgorithmType.HMAC);
}
}

View file

@ -17,6 +17,7 @@
package org.keycloak.keys;
import org.keycloak.jose.jws.AlgorithmType;
import org.keycloak.provider.Provider;
import java.security.PrivateKey;
@ -27,7 +28,14 @@ import java.util.List;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public interface KeyProvider extends Provider {
public interface KeyProvider<T extends KeyMetadata> extends Provider {
/**
* Returns the algorithm type the keys can be used for
*
* @return
*/
AlgorithmType getType();
/**
* Return the KID for the active keypair, or <code>null</code> if no active key is available.
@ -36,33 +44,10 @@ public interface KeyProvider extends Provider {
*/
String getKid();
/**
* Return the private key for the active keypair, or <code>null</code> if no active key is available.
*
* @return
*/
PrivateKey getPrivateKey();
/**
* Return the public key for the specified kid, or <code>null</code> if the kid is unknown.
*
* @param kid
* @return
*/
PublicKey getPublicKey(String kid);
/**
* Return the certificate for the specified kid, or <code>null</code> if the kid is unknown.
*
* @param kid
* @return
*/
X509Certificate getCertificate(String kid);
/**
* Return metadata about all keypairs held by the provider
* @return
*/
List<KeyMetadata> getKeyMetadata();
List<T> getKeyMetadata();
}

View file

@ -0,0 +1,60 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.keys;
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;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public interface RsaKeyProvider extends KeyProvider<RsaKeyMetadata> {
default AlgorithmType getType() {
return AlgorithmType.RSA;
}
/**
* Return the private key for the active keypair, or <code>null</code> if no active key is available.
*
* @return
*/
PrivateKey getPrivateKey();
/**
* Return the public key for the specified kid, or <code>null</code> if the kid is unknown.
*
* @param kid
* @return
*/
PublicKey getPublicKey(String kid);
/**
* Return the certificate for the specified kid, or <code>null</code> if the kid is unknown.
*
* @param kid
* @return
*/
X509Certificate getCertificate(String kid);
}

View file

@ -0,0 +1,35 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.keys;
import org.keycloak.jose.jws.AlgorithmType;
import java.util.Collections;
import java.util.Map;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public interface RsaKeyProviderFactory extends KeyProviderFactory {
@Override
default Map<String, Object> getTypeMetadata() {
return Collections.singletonMap("algorithmType", AlgorithmType.RSA);
}
}

View file

@ -31,6 +31,7 @@ import org.keycloak.migration.migrators.MigrateTo2_0_0;
import org.keycloak.migration.migrators.MigrateTo2_1_0;
import org.keycloak.migration.migrators.MigrateTo2_2_0;
import org.keycloak.migration.migrators.MigrateTo2_3_0;
import org.keycloak.migration.migrators.MigrateTo2_5_0;
import org.keycloak.migration.migrators.Migration;
import org.keycloak.models.KeycloakSession;
@ -55,6 +56,7 @@ public class MigrationModelManager {
new MigrateTo2_1_0(),
new MigrateTo2_2_0(),
new MigrateTo2_3_0(),
new MigrateTo2_5_0(),
};
public static void migrate(KeycloakSession session) {

View file

@ -0,0 +1,46 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.migration.migrators;
import org.keycloak.migration.ModelVersion;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientTemplateModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.DefaultKeyProviders;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class MigrateTo2_5_0 implements Migration {
public static final ModelVersion VERSION = new ModelVersion("2.5.0");
@Override
public void migrate(KeycloakSession session) {
session.realms().getRealms().stream().forEach(
r -> DefaultKeyProviders.createSecretProvider(r)
);
}
@Override
public ModelVersion getVersion() {
return VERSION;
}
}

View file

@ -39,6 +39,22 @@ public class DefaultKeyProviders {
generated.setConfig(config);
realm.addComponentModel(generated);
createSecretProvider(realm);
}
public static void createSecretProvider(RealmModel realm) {
ComponentModel generated = new ComponentModel();
generated.setName("hmac-generated");
generated.setParentId(realm.getId());
generated.setProviderId("hmac-generated");
generated.setProviderType(KeyProvider.class.getName());
MultivaluedHashMap<String, String> config = new MultivaluedHashMap<>();
config.putSingle("priority", "100");
generated.setConfig(config);
realm.addComponentModel(generated);
}
public static void createProviders(RealmModel realm, String privateKeyPem, String certificatePem) {
@ -57,6 +73,8 @@ public class DefaultKeyProviders {
rsa.setConfig(config);
realm.addComponentModel(rsa);
createSecretProvider(realm);
}
}

View file

@ -41,6 +41,28 @@ public class ConfigurationValidationHelper {
return checkInt(property.getName(), property.getLabel(), required);
}
public ConfigurationValidationHelper checkList(ProviderConfigProperty property, boolean required) throws ComponentValidationException {
checkSingle(property.getName(), property.getLabel(), required);
String value = model.getConfig().getFirst(property.getName());
if (value != null && !property.options.contains(value)) {
StringBuilder options = new StringBuilder();
int i = 1;
for (String o : property.options) {
if (i == property.options.size()) {
options.append(" or ");
} else if (i > 1) {
options.append(", ");
}
options.append(o);
i++;
}
throw new ComponentValidationException("''{0}'' should be {1}", property.label, options.toString());
}
return this;
}
public ConfigurationValidationHelper checkInt(String key, String label, boolean required) throws ComponentValidationException {
checkSingle(key, label, required);

View file

@ -0,0 +1,25 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.keys;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class HmacKeyMetadata extends KeyMetadata {
}

View file

@ -17,22 +17,15 @@
package org.keycloak.keys;
import java.security.PublicKey;
import java.security.cert.Certificate;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class KeyMetadata {
public abstract class KeyMetadata {
public enum Status {
ACTIVE, PASSIVE, DISABLED
}
public enum Type {
RSA
}
private String providerId;
private long providerPriority;
@ -40,11 +33,6 @@ public class KeyMetadata {
private Status status;
private Type type;
private PublicKey publicKey;
private Certificate certificate;
public String getProviderId() {
return providerId;
}
@ -77,28 +65,4 @@ public class KeyMetadata {
this.status = status;
}
public Type getType() {
return type;
}
public void setType(Type type) {
this.type = type;
}
public PublicKey getPublicKey() {
return publicKey;
}
public void setPublicKey(PublicKey publicKey) {
this.publicKey = publicKey;
}
public Certificate getCertificate() {
return certificate;
}
public void setCertificate(Certificate certificate) {
this.certificate = certificate;
}
}

View file

@ -0,0 +1,47 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.keys;
import java.security.PublicKey;
import java.security.cert.Certificate;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class RsaKeyMetadata extends KeyMetadata {
private PublicKey publicKey;
private Certificate certificate;
public PublicKey getPublicKey() {
return publicKey;
}
public void setPublicKey(PublicKey publicKey) {
this.publicKey = publicKey;
}
public Certificate getCertificate() {
return certificate;
}
public void setCertificate(Certificate certificate) {
this.certificate = certificate;
}
}

View file

@ -17,8 +17,10 @@
package org.keycloak.models;
import org.keycloak.keys.KeyMetadata;
import org.keycloak.keys.HmacKeyMetadata;
import org.keycloak.keys.RsaKeyMetadata;
import javax.crypto.SecretKey;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.Certificate;
@ -30,21 +32,27 @@ import java.util.List;
*/
public interface KeyManager {
ActiveKey getActiveKey(RealmModel realm);
ActiveRsaKey getActiveRsaKey(RealmModel realm);
PublicKey getPublicKey(RealmModel realm, String kid);
PublicKey getRsaPublicKey(RealmModel realm, String kid);
Certificate getCertificate(RealmModel realm, String kid);
Certificate getRsaCertificate(RealmModel realm, String kid);
List<KeyMetadata> getKeys(RealmModel realm, boolean includeDisabled);
List<RsaKeyMetadata> getRsaKeys(RealmModel realm, boolean includeDisabled);
class ActiveKey {
ActiveHmacKey getActiveHmacKey(RealmModel realm);
SecretKey getHmacSecretKey(RealmModel realm, String kid);
List<HmacKeyMetadata> getHmacKeys(RealmModel realm, boolean includeDisabled);
class ActiveRsaKey {
private final String kid;
private final PrivateKey privateKey;
private final PublicKey publicKey;
private final X509Certificate certificate;
public ActiveKey(String kid, PrivateKey privateKey, PublicKey publicKey, X509Certificate certificate) {
public ActiveRsaKey(String kid, PrivateKey privateKey, PublicKey publicKey, X509Certificate certificate) {
this.kid = kid;
this.privateKey = privateKey;
this.publicKey = publicKey;
@ -68,4 +76,22 @@ public interface KeyManager {
}
}
class ActiveHmacKey {
private final String kid;
private final SecretKey secretKey;
public ActiveHmacKey(String kid, SecretKey secretKey) {
this.kid = kid;
this.secretKey = secretKey;
}
public String getKid() {
return kid;
}
public SecretKey getSecretKey() {
return secretKey;
}
}
}

View file

@ -22,6 +22,6 @@ package org.keycloak.provider;
*/
public interface Provider {
public void close();
void close();
}

View file

@ -47,7 +47,7 @@ public class UmaWellKnownProvider implements WellKnownProvider {
return Configuration.fromDefault(RealmsResource.realmBaseUrl(uriInfo).build(realm.getName()).toString(), realm.getName(),
URI.create(RealmsResource.protocolUrl(uriInfo).path(OIDCLoginProtocolService.class, "auth").build(realm.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL).toString()),
URI.create(RealmsResource.protocolUrl(uriInfo).path(OIDCLoginProtocolService.class, "token").build(realm.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL).toString()),
PemUtils.encodeKey(session.keys().getActiveKey(realm).getPublicKey()));
PemUtils.encodeKey(session.keys().getActiveRsaKey(realm).getPublicKey()));
}
@Override

View file

@ -131,7 +131,7 @@ public class AbstractPermissionService {
}
private String createPermissionTicket(List<ResourceRepresentation> resources) {
KeyManager.ActiveKey keys = this.authorization.getKeycloakSession().keys().getActiveKey(this.authorization.getRealm());
KeyManager.ActiveRsaKey keys = this.authorization.getKeycloakSession().keys().getActiveRsaKey(this.authorization.getRealm());
return new JWSBuilder().kid(keys.getKid()).jsonContent(new PermissionTicket(resources, this.resourceServer.getId(), this.identity.getAccessToken()))
.rsa256(keys.getPrivateKey());
}

View file

@ -57,7 +57,7 @@ public class Tokens {
public static boolean verifySignature(KeycloakSession keycloakSession, RealmModel realm, String token) {
try {
JWSInput jws = new JWSInput(token);
PublicKey publicKey = keycloakSession.keys().getPublicKey(realm, jws.getHeader().getKeyId());
PublicKey publicKey = keycloakSession.keys().getRsaPublicKey(realm, jws.getHeader().getKeyId());
return RSAProvider.verify(jws, publicKey);
} catch (Exception e) {
throw new ErrorResponseException("invalid_signature", "Unexpected error while validating signature.", Status.INTERNAL_SERVER_ERROR);

View file

@ -301,7 +301,7 @@ public class SAMLEndpoint {
.relayState(relayState);
boolean postBinding = config.isPostBindingResponse();
if (config.isWantAuthnRequestsSigned()) {
KeyManager.ActiveKey keys = session.keys().getActiveKey(realm);
KeyManager.ActiveRsaKey keys = session.keys().getActiveRsaKey(realm);
String keyName = config.getXmlSigKeyInfoKeyNameTransformer().getKeyName(keys.getKid(), keys.getCertificate());
binding.signWith(keyName, keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate())
.signatureAlgorithm(provider.getSignatureAlgorithm())
@ -332,7 +332,7 @@ public class SAMLEndpoint {
protected Response handleLoginResponse(String samlResponse, SAMLDocumentHolder holder, ResponseType responseType, String relayState, String clientId) {
try {
KeyManager.ActiveKey keys = session.keys().getActiveKey(realm);
KeyManager.ActiveRsaKey keys = session.keys().getActiveRsaKey(realm);
AssertionType assertion = AssertionUtil.getAssertion(responseType, keys.getPrivateKey());
SubjectType subject = assertion.getSubject();
SubjectType.STSubType subType = subject.getSubType();

View file

@ -30,6 +30,7 @@ import org.keycloak.dom.saml.v2.assertion.NameIDType;
import org.keycloak.dom.saml.v2.assertion.SubjectType;
import org.keycloak.dom.saml.v2.protocol.ResponseType;
import org.keycloak.events.EventBuilder;
import org.keycloak.keys.RsaKeyMetadata;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.KeyManager;
@ -103,7 +104,7 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
boolean postBinding = getConfig().isPostBindingAuthnRequest();
if (getConfig().isWantAuthnRequestsSigned()) {
KeyManager.ActiveKey keys = session.keys().getActiveKey(realm);
KeyManager.ActiveRsaKey keys = session.keys().getActiveRsaKey(realm);
KeyPair keypair = new KeyPair(keys.getPublicKey(), keys.getPrivateKey());
@ -205,7 +206,7 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder()
.relayState(userSession.getId());
if (getConfig().isWantAuthnRequestsSigned()) {
KeyManager.ActiveKey keys = session.keys().getActiveKey(realm);
KeyManager.ActiveRsaKey keys = session.keys().getActiveRsaKey(realm);
String keyName = getConfig().getXmlSigKeyInfoKeyNameTransformer().getKeyName(keys.getKid(), keys.getCertificate());
binding.signWith(keyName, keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate())
.signatureAlgorithm(getSignatureAlgorithm())
@ -236,18 +237,18 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
String nameIDPolicyFormat = getConfig().getNameIDPolicyFormat();
StringBuilder keysString = new StringBuilder();
Set<KeyMetadata> keys = new TreeSet<>((o1, o2) -> o1.getStatus() == o2.getStatus() // Status can be only PASSIVE OR ACTIVE, push PASSIVE to end of list
Set<RsaKeyMetadata> keys = new TreeSet<>((o1, o2) -> o1.getStatus() == o2.getStatus() // Status can be only PASSIVE OR ACTIVE, push PASSIVE to end of list
? (int) (o2.getProviderPriority() - o1.getProviderPriority())
: (o1.getStatus() == KeyMetadata.Status.PASSIVE ? 1 : -1));
keys.addAll(session.keys().getKeys(realm, false));
for (KeyMetadata key : keys) {
keys.addAll(session.keys().getRsaKeys(realm, false));
for (RsaKeyMetadata key : keys) {
addKeyInfo(keysString, key, KeyTypes.SIGNING.value());
}
String descriptor = SPMetadataDescriptor.getSPDescriptor(authnBinding, endpoint, endpoint, wantAuthnRequestsSigned, entityId, nameIDPolicyFormat, keysString.toString());
return Response.ok(descriptor, MediaType.APPLICATION_XML_TYPE).build();
}
private static void addKeyInfo(StringBuilder target, KeyMetadata key, String purpose) {
private static void addKeyInfo(StringBuilder target, RsaKeyMetadata key, String purpose) {
if (key == null) {
return;
}

View file

@ -0,0 +1,47 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.keys;
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.ProviderConfigurationBuilder;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public abstract class AbstractHmacKeyProviderFactory implements HmacKeyProviderFactory {
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);
}
}

View file

@ -18,6 +18,7 @@
package org.keycloak.keys;
import org.keycloak.component.ComponentModel;
import org.keycloak.jose.jws.AlgorithmType;
import org.keycloak.models.RealmModel;
import java.security.KeyPair;
@ -30,7 +31,7 @@ import java.util.List;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public abstract class AbstractRsaKeyProvider implements KeyProvider {
public abstract class AbstractRsaKeyProvider implements RsaKeyProvider {
private final boolean enabled;
@ -77,11 +78,11 @@ public abstract class AbstractRsaKeyProvider implements KeyProvider {
}
@Override
public final List<KeyMetadata> getKeyMetadata() {
public final List<RsaKeyMetadata> getKeyMetadata() {
String kid = keys.getKid();
PublicKey publicKey = keys.getKeyPair().getPublic();
if (kid != null && publicKey != null) {
KeyMetadata k = new KeyMetadata();
RsaKeyMetadata k = new RsaKeyMetadata();
k.setProviderId(model.getId());
k.setProviderPriority(model.get(Attributes.PRIORITY_KEY, 0l));
k.setKid(kid);
@ -92,7 +93,6 @@ public abstract class AbstractRsaKeyProvider implements KeyProvider {
} else {
k.setStatus(KeyMetadata.Status.DISABLED);
}
k.setType(KeyMetadata.Type.RSA);
k.setPublicKey(publicKey);
k.setCertificate(keys.getCertificate());
return Collections.singletonList(k);

View file

@ -27,7 +27,7 @@ import org.keycloak.provider.ProviderConfigurationBuilder;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public abstract class AbstractRsaKeyProviderFactory implements KeyProviderFactory {
public abstract class AbstractRsaKeyProviderFactory implements RsaKeyProviderFactory {
public final static ProviderConfigurationBuilder configurationBuilder() {
return ProviderConfigurationBuilder.create()

View file

@ -44,6 +44,13 @@ public interface Attributes {
ProviderConfigProperty CERTIFICATE_PROPERTY = new ProviderConfigProperty(CERTIFICATE_KEY, "X509 Certificate", "X509 Certificate encoded in PEM format", FILE_TYPE, null);
String KEY_SIZE_KEY = "keySize";
ProviderConfigProperty KEY_SIZE_PROPERTY = new ProviderConfigProperty(KEY_SIZE_KEY, "Keysize", "Size for the generated keys (1024, 2048 or 4096)", LIST_TYPE, "2048", "1024", "2048", "4096");
ProviderConfigProperty KEY_SIZE_PROPERTY = new ProviderConfigProperty(KEY_SIZE_KEY, "Key size", "Size for the generated keys", LIST_TYPE, "2048", "1024", "2048", "4096");
String KID_KEY = "kid";
String SECRET_KEY = "secret";
String SECRET_SIZE_KEY = "secretSize";
ProviderConfigProperty SECRET_SIZE_PROPERTY = new ProviderConfigProperty(SECRET_SIZE_KEY, "Secret size", "Size in bytes for the generated secret", LIST_TYPE, "32", "32", "64", "128", "256", "512");
}

View file

@ -19,11 +19,13 @@ package org.keycloak.keys;
import org.jboss.logging.Logger;
import org.keycloak.component.ComponentModel;
import org.keycloak.jose.jws.AlgorithmType;
import org.keycloak.models.KeyManager;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.provider.ProviderFactory;
import javax.crypto.SecretKey;
import java.security.PublicKey;
import java.security.cert.Certificate;
import java.util.Comparator;
@ -47,33 +49,56 @@ public class DefaultKeyManager implements KeyManager {
}
@Override
public ActiveKey getActiveKey(RealmModel realm) {
public ActiveRsaKey getActiveRsaKey(RealmModel realm) {
for (KeyProvider p : getProviders(realm)) {
if (p.getKid() != null && p.getPrivateKey() != null) {
if (logger.isTraceEnabled()) {
logger.tracev("Active key realm={0} kid={1}", realm.getName(), p.getKid());
if (p.getType().equals(AlgorithmType.RSA)) {
RsaKeyProvider r = (RsaKeyProvider) p;
if (r.getKid() != null && r.getPrivateKey() != null) {
if (logger.isTraceEnabled()) {
logger.tracev("Active key realm={0} kid={1}", realm.getName(), p.getKid());
}
String kid = p.getKid();
return new ActiveRsaKey(kid, r.getPrivateKey(), r.getPublicKey(kid), r.getCertificate(kid));
}
}
}
throw new RuntimeException("Failed to get RSA keys");
}
@Override
public ActiveHmacKey getActiveHmacKey(RealmModel realm) {
for (KeyProvider p : getProviders(realm)) {
if (p.getType().equals(AlgorithmType.HMAC)) {
HmacKeyProvider h = (HmacKeyProvider) p;
if (h.getKid() != null && h.getSecretKey() != null) {
if (logger.isTraceEnabled()) {
logger.tracev("Active secret realm={0} kid={1}", realm.getName(), p.getKid());
}
String kid = p.getKid();
return new ActiveHmacKey(kid, h.getSecretKey());
}
String kid = p.getKid();
return new ActiveKey(kid, p.getPrivateKey(), p.getPublicKey(kid), p.getCertificate(kid));
}
}
throw new RuntimeException("Failed to get keys");
}
@Override
public PublicKey getPublicKey(RealmModel realm, String kid) {
public PublicKey getRsaPublicKey(RealmModel realm, String kid) {
if (kid == null) {
logger.warnv("KID is null, can't find public key", realm.getName(), kid);
return null;
}
for (KeyProvider p : getProviders(realm)) {
PublicKey publicKey = p.getPublicKey(kid);
if (publicKey != null) {
if (logger.isTraceEnabled()) {
logger.tracev("Found public key realm={0} kid={1}", realm.getName(), kid);
if (p.getType().equals(AlgorithmType.RSA)) {
RsaKeyProvider r = (RsaKeyProvider) p;
PublicKey publicKey = r.getPublicKey(kid);
if (publicKey != null) {
if (logger.isTraceEnabled()) {
logger.tracev("Found public key realm={0} kid={1}", realm.getName(), kid);
}
return publicKey;
}
return publicKey;
}
}
if (logger.isTraceEnabled()) {
@ -83,19 +108,22 @@ public class DefaultKeyManager implements KeyManager {
}
@Override
public Certificate getCertificate(RealmModel realm, String kid) {
public Certificate getRsaCertificate(RealmModel realm, String kid) {
if (kid == null) {
logger.warnv("KID is null, can't find public key", realm.getName(), kid);
return null;
}
for (KeyProvider p : getProviders(realm)) {
Certificate certificate = p.getCertificate(kid);
if (certificate != null) {
if (logger.isTraceEnabled()) {
logger.tracev("Found certificate realm={0} kid={1}", realm.getName(), kid);
if (p.getType().equals(AlgorithmType.RSA)) {
RsaKeyProvider r = (RsaKeyProvider) p;
Certificate certificate = r.getCertificate(kid);
if (certificate != null) {
if (logger.isTraceEnabled()) {
logger.tracev("Found certificate realm={0} kid={1}", realm.getName(), kid);
}
return certificate;
}
return certificate;
}
}
if (logger.isTraceEnabled()) {
@ -105,20 +133,63 @@ public class DefaultKeyManager implements KeyManager {
}
@Override
public List<KeyMetadata> getKeys(RealmModel realm, boolean includeDisabled) {
List<KeyMetadata> keys = new LinkedList<>();
public SecretKey getHmacSecretKey(RealmModel realm, String kid) {
if (kid == null) {
logger.warnv("KID is null, can't find public key", realm.getName(), kid);
return null;
}
for (KeyProvider p : getProviders(realm)) {
if (includeDisabled) {
keys.addAll(p.getKeyMetadata());
} else {
p.getKeyMetadata().stream().filter(k -> k.getStatus() != KeyMetadata.Status.DISABLED).forEach(k -> keys.add(k));
if (p.getType().equals(AlgorithmType.HMAC)) {
HmacKeyProvider h = (HmacKeyProvider) p;
SecretKey s = h.getSecretKey(kid);
if (s != null) {
if (logger.isTraceEnabled()) {
logger.tracev("Found secret key realm={0} kid={1}", realm.getName(), kid);
}
return s;
}
}
}
if (logger.isTraceEnabled()) {
logger.tracev("Failed to find secret key realm={0} kid={1}", realm.getName(), kid);
}
return null;
}
@Override
public List<RsaKeyMetadata> getRsaKeys(RealmModel realm, boolean includeDisabled) {
List<RsaKeyMetadata> keys = new LinkedList<>();
for (KeyProvider p : getProviders(realm)) {
if (p instanceof RsaKeyProvider) {
if (includeDisabled) {
keys.addAll(p.getKeyMetadata());
} else {
List<RsaKeyMetadata> metadata = p.getKeyMetadata();
metadata.stream().filter(k -> k.getStatus() != KeyMetadata.Status.DISABLED).forEach(k -> keys.add(k));
}
}
}
return keys;
}
@Override
public List<HmacKeyMetadata> getHmacKeys(RealmModel realm, boolean includeDisabled) {
List<HmacKeyMetadata> keys = new LinkedList<>();
for (KeyProvider p : getProviders(realm)) {
if (p instanceof HmacKeyProvider) {
if (includeDisabled) {
keys.addAll(p.getKeyMetadata());
} else {
List<HmacKeyMetadata> metadata = p.getKeyMetadata();
metadata.stream().filter(k -> k.getStatus() != KeyMetadata.Status.DISABLED).forEach(k -> keys.add(k));
}
}
}
return keys;
}
private List<KeyProvider> getProviders(RealmModel realm) {
boolean active = false;
List<KeyProvider> providers = providersMap.get(realm.getId());
if (providers == null) {
providers = new LinkedList<>();
@ -126,6 +197,9 @@ public class DefaultKeyManager implements KeyManager {
List<ComponentModel> components = new LinkedList<>(realm.getComponents(realm.getId(), KeyProvider.class.getName()));
components.sort(new ProviderComparator());
boolean activeRsa = false;
boolean activeHmac = false;
for (ComponentModel c : components) {
try {
ProviderFactory<KeyProvider> f = session.getKeycloakSessionFactory().getProviderFactory(KeyProvider.class, c.getProviderId());
@ -133,18 +207,30 @@ public class DefaultKeyManager implements KeyManager {
KeyProvider provider = factory.create(session, c);
session.enlistForClose(provider);
providers.add(provider);
if (!active && provider.getKid() != null && provider.getPrivateKey() != null) {
active = true;
if (provider.getType().equals(AlgorithmType.RSA)) {
RsaKeyProvider r = (RsaKeyProvider) provider;
if (r.getKid() != null && r.getPrivateKey() != null) {
activeRsa = true;
}
} else if (provider.getType().equals(AlgorithmType.HMAC)) {
HmacKeyProvider r = (HmacKeyProvider) provider;
if (r.getKid() != null && r.getSecretKey() != null) {
activeHmac = true;
}
}
} catch (Throwable t) {
logger.errorv(t, "Failed to load provider {0}", c.getId());
}
}
if (!active) {
if (!activeRsa) {
providers.add(new FailsafeRsaKeyProvider());
}
if (!activeHmac) {
providers.add(new FailsafeHmacKeyProvider());
}
providersMap.put(realm.getId(), providers);
}
return providers;

View file

@ -0,0 +1,89 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.keys;
import org.jboss.logging.Logger;
import org.keycloak.common.util.KeyUtils;
import org.keycloak.common.util.Time;
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>
*/
public class FailsafeHmacKeyProvider implements HmacKeyProvider {
private static final Logger logger = Logger.getLogger(FailsafeHmacKeyProvider.class);
private static String KID;
private static SecretKey KEY;
private static long EXPIRES;
private SecretKey key;
private String kid;
public FailsafeHmacKeyProvider() {
logger.errorv("No active keys found, using failsafe provider, please login to admin console to add keys. Clustering is not supported.");
synchronized (FailsafeHmacKeyProvider.class) {
if (EXPIRES < Time.currentTime()) {
KEY = KeyUtils.loadSecretKey(KeycloakModelUtils.generateSecret(32));
KID = KeycloakModelUtils.generateId();
EXPIRES = Time.currentTime() + 60 * 10;
if (EXPIRES > 0) {
logger.warnv("Keys expired, re-generated kid={0}", KID);
}
}
kid = KID;
key = KEY;
}
}
@Override
public String getKid() {
return kid;
}
@Override
public SecretKey getSecretKey() {
return key;
}
@Override
public SecretKey getSecretKey(String kid) {
return kid.equals(this.kid) ? key : null;
}
@Override
public List<HmacKeyMetadata> getKeyMetadata() {
return Collections.emptyList();
}
@Override
public void close() {
}
}

View file

@ -31,7 +31,7 @@ import java.util.List;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class FailsafeRsaKeyProvider implements KeyProvider {
public class FailsafeRsaKeyProvider implements RsaKeyProvider {
private static final Logger logger = Logger.getLogger(FailsafeRsaKeyProvider.class);
@ -85,7 +85,7 @@ public class FailsafeRsaKeyProvider implements KeyProvider {
}
@Override
public List<KeyMetadata> getKeyMetadata() {
public List<RsaKeyMetadata> getKeyMetadata() {
return Collections.emptyList();
}

View file

@ -0,0 +1,102 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.keys;
import org.keycloak.common.util.KeyUtils;
import org.keycloak.component.ComponentModel;
import org.keycloak.jose.jws.AlgorithmType;
import javax.crypto.SecretKey;
import java.util.Collections;
import java.util.List;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class GeneratedHmacKeyProvider implements HmacKeyProvider {
private final boolean enabled;
private final boolean active;
private final ComponentModel model;
private final String kid;
private final SecretKey secretKey;
public GeneratedHmacKeyProvider(ComponentModel model) {
this.enabled = model.get(Attributes.ENABLED_KEY, true);
this.active = model.get(Attributes.ACTIVE_KEY, true);
this.kid = model.get(Attributes.KID_KEY);
this.model = model;
if (model.hasNote(SecretKey.class.getName())) {
secretKey = model.getNote(SecretKey.class.getName());
} else {
secretKey = KeyUtils.loadSecretKey(model.get(Attributes.SECRET_KEY));
model.setNote(SecretKey.class.getName(), secretKey);
}
}
@Override
public SecretKey getSecretKey() {
return isActive() ? secretKey : null;
}
@Override
public SecretKey getSecretKey(String kid) {
return isEnabled() && kid.equals(this.kid) ? secretKey : null;
}
@Override
public String getKid() {
return isActive() ? kid : null;
}
@Override
public List<HmacKeyMetadata> getKeyMetadata() {
if (kid != null && secretKey != null) {
HmacKeyMetadata k = new HmacKeyMetadata();
k.setProviderId(model.getId());
k.setProviderPriority(model.get(Attributes.PRIORITY_KEY, 0l));
k.setKid(kid);
if (isActive()) {
k.setStatus(KeyMetadata.Status.ACTIVE);
} else if (isEnabled()) {
k.setStatus(KeyMetadata.Status.PASSIVE);
} else {
k.setStatus(KeyMetadata.Status.DISABLED);
}
return Collections.singletonList(k);
} else {
return Collections.emptyList();
}
}
@Override
public void close() {
}
private boolean isEnabled() {
return secretKey != null && enabled;
}
private boolean isActive() {
return isEnabled() && active;
}
}

View file

@ -0,0 +1,111 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.keys;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.common.util.Base64Url;
import org.keycloak.component.ComponentModel;
import org.keycloak.component.ComponentValidationException;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.provider.ConfigurationValidationHelper;
import org.keycloak.provider.ProviderConfigProperty;
import java.util.List;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class GeneratedHmacKeyProviderFactory extends AbstractHmacKeyProviderFactory {
private static final Logger logger = Logger.getLogger(GeneratedHmacKeyProviderFactory.class);
public static final String ID = "hmac-generated";
private static final String HELP_TEXT = "Generates HMAC secret key";
private static final List<ProviderConfigProperty> CONFIG_PROPERTIES = AbstractHmacKeyProviderFactory.configurationBuilder()
.property(Attributes.SECRET_SIZE_PROPERTY)
.build();
@Override
public KeyProvider create(KeycloakSession session, ComponentModel model) {
return new GeneratedHmacKeyProvider(model);
}
@Override
public String getHelpText() {
return HELP_TEXT;
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return CONFIG_PROPERTIES;
}
@Override
public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model) throws ComponentValidationException {
ConfigurationValidationHelper.check(model).checkList(Attributes.SECRET_SIZE_PROPERTY, false);
int size = model.get(Attributes.SECRET_SIZE_KEY, 32);
if (!(model.contains(Attributes.SECRET_KEY))) {
generateSecret(model, size);
logger.debugv("Generated secret for {0}", realm.getName());
} else {
int currentSize = Base64Url.decode(model.get(Attributes.SECRET_KEY)).length;
if (currentSize != size) {
generateSecret(model, size);
logger.debugv("Secret size changed, generating new secret for {0}", realm.getName());
}
}
}
private void generateSecret(ComponentModel model, int size) {
try {
String secret = KeycloakModelUtils.generateSecret(size);
model.put(Attributes.SECRET_KEY, secret);
String kid = KeycloakModelUtils.generateId();
model.put(Attributes.KID_KEY, kid);
} catch (Throwable t) {
throw new ComponentValidationException("Failed to generate secret", t);
}
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
@Override
public String getId() {
return ID;
}
}

View file

@ -53,26 +53,16 @@ public class GeneratedRsaKeyProviderFactory extends AbstractRsaKeyProviderFactor
@Override
public KeyProvider create(KeycloakSession session, ComponentModel model) {
return new RsaKeyProvider(session.getContext().getRealm(), model);
return new ImportedRsaKeyProvider(session.getContext().getRealm(), model);
}
@Override
public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model) throws ComponentValidationException {
super.validateConfiguration(session, realm, model);
ConfigurationValidationHelper.check(model)
.checkInt(Attributes.KEY_SIZE_PROPERTY, false);
ConfigurationValidationHelper.check(model).checkList(Attributes.KEY_SIZE_PROPERTY, false);
int size;
if (!model.contains(Attributes.KEY_SIZE_KEY)) {
size = 2048;
model.put(Attributes.KEY_SIZE_KEY, size);
} else {
size = model.get(Attributes.KEY_SIZE_KEY, 2048);
if (size != 1024 && size != 2048 && size != 4096) {
throw new ComponentValidationException("Keysize should be 1024, 2048 or 4096");
}
}
int size = model.get(Attributes.KEY_SIZE_KEY, 2048);
if (!(model.contains(Attributes.PRIVATE_KEY_KEY) && model.contains(Attributes.CERTIFICATE_KEY))) {
generateKeys(realm, model, size);

View file

@ -31,9 +31,9 @@ import java.security.cert.X509Certificate;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class RsaKeyProvider extends AbstractRsaKeyProvider {
public class ImportedRsaKeyProvider extends AbstractRsaKeyProvider {
public RsaKeyProvider(RealmModel realm, ComponentModel model) {
public ImportedRsaKeyProvider(RealmModel realm, ComponentModel model) {
super(realm, model);
}

View file

@ -33,13 +33,12 @@ import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.List;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class RsaKeyProviderFactory extends AbstractRsaKeyProviderFactory {
public class ImportedRsaKeyProviderFactory extends AbstractRsaKeyProviderFactory {
public static final String ID = "rsa";
@ -52,7 +51,7 @@ public class RsaKeyProviderFactory extends AbstractRsaKeyProviderFactory {
@Override
public KeyProvider create(KeycloakSession session, ComponentModel model) {
return new RsaKeyProvider(session.getContext().getRealm(), model);
return new ImportedRsaKeyProvider(session.getContext().getRealm(), model);
}
@Override

View file

@ -22,6 +22,7 @@ import org.jboss.logging.Logger;
import org.keycloak.common.ClientConnection;
import org.keycloak.jose.jws.JWSBuilder;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.crypto.HMACProvider;
import org.keycloak.jose.jws.crypto.RSAProvider;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionModel;
@ -31,6 +32,7 @@ import org.keycloak.models.RealmModel;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.util.CookieHelper;
import javax.crypto.SecretKey;
import javax.ws.rs.core.Cookie;
import javax.ws.rs.core.UriInfo;
import java.security.PublicKey;
@ -114,12 +116,11 @@ public class RestartLoginCookie {
}
public String encode(KeycloakSession session, RealmModel realm) {
KeyManager.ActiveKey keys = session.keys().getActiveKey(realm);
KeyManager.ActiveHmacKey activeKey = session.keys().getActiveHmacKey(realm);
JWSBuilder builder = new JWSBuilder();
return builder.kid(keys.getKid()).jsonContent(this)
.rsa256(keys.getPrivateKey());
return builder.kid(activeKey.getKid()).jsonContent(this)
.hmac256(activeKey.getSecretKey());
}
public RestartLoginCookie() {
@ -157,8 +158,8 @@ public class RestartLoginCookie {
}
String encodedCookie = cook.getValue();
JWSInput input = new JWSInput(encodedCookie);
PublicKey publicKey = session.keys().getPublicKey(realm, input.getHeader().getKeyId());
if (!RSAProvider.verify(input, publicKey)) {
SecretKey secretKey = session.keys().getHmacSecretKey(realm, input.getHeader().getKeyId());
if (!HMACProvider.verify(input, secretKey)) {
logger.debug("Failed to verify encoded RestartLoginCookie");
return null;
}

View file

@ -56,7 +56,7 @@ public class AccessTokenIntrospectionProvider implements TokenIntrospectionProvi
RSATokenVerifier verifier = RSATokenVerifier.create(token)
.realmUrl(Urls.realmIssuer(session.getContext().getUri().getBaseUri(), realm.getName()));
PublicKey publicKey = session.keys().getPublicKey(realm, verifier.getHeader().getKeyId());
PublicKey publicKey = session.keys().getRsaPublicKey(realm, verifier.getHeader().getKeyId());
if (publicKey == null) {
valid = false;
} else {
@ -96,7 +96,7 @@ public class AccessTokenIntrospectionProvider implements TokenIntrospectionProvi
RSATokenVerifier verifier = RSATokenVerifier.create(token)
.realmUrl(Urls.realmIssuer(session.getContext().getUri().getBaseUri(), realm.getName()));
PublicKey publicKey = session.keys().getPublicKey(realm, verifier.getHeader().getKeyId());
PublicKey publicKey = session.keys().getRsaPublicKey(realm, verifier.getHeader().getKeyId());
verifier.publicKey(publicKey);
return verifier.verify().getToken();

View file

@ -26,6 +26,7 @@ import org.keycloak.jose.jwk.JSONWebKeySet;
import org.keycloak.jose.jwk.JWK;
import org.keycloak.jose.jwk.JWKBuilder;
import org.keycloak.keys.KeyMetadata;
import org.keycloak.keys.RsaKeyMetadata;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.protocol.oidc.endpoints.AuthorizationEndpoint;
@ -187,11 +188,11 @@ public class OIDCLoginProtocolService {
@Produces(MediaType.APPLICATION_JSON)
@NoCache
public Response certs() {
List<KeyMetadata> publicKeys = session.keys().getKeys(realm, false);
List<RsaKeyMetadata> publicKeys = session.keys().getRsaKeys(realm, false);
JWK[] keys = new JWK[publicKeys.size()];
int i = 0;
for (KeyMetadata k : publicKeys) {
for (RsaKeyMetadata k : publicKeys) {
keys[i++] = JWKBuilder.create().kid(k.getKid()).rs256(k.getPublicKey());
}

View file

@ -287,7 +287,7 @@ public class TokenManager {
public RefreshToken toRefreshToken(KeycloakSession session, RealmModel realm, String encodedRefreshToken) throws JWSInputException, OAuthErrorException {
JWSInput jws = new JWSInput(encodedRefreshToken);
if (!RSAProvider.verify(jws, session.keys().getPublicKey(realm, jws.getHeader().getKeyId()))) {
if (!RSAProvider.verify(jws, session.keys().getRsaPublicKey(realm, jws.getHeader().getKeyId()))) {
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid refresh token");
}
@ -298,7 +298,7 @@ public class TokenManager {
try {
JWSInput jws = new JWSInput(encodedIDToken);
IDToken idToken;
if (!RSAProvider.verify(jws, session.keys().getPublicKey(realm, jws.getHeader().getKeyId()))) {
if (!RSAProvider.verify(jws, session.keys().getRsaPublicKey(realm, jws.getHeader().getKeyId()))) {
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid IDToken");
}
idToken = jws.readJsonContent(IDToken.class);
@ -626,8 +626,8 @@ public class TokenManager {
}
public String encodeToken(KeycloakSession session, RealmModel realm, Object token) {
KeyManager.ActiveKey activeKey = session.keys().getActiveKey(realm);
return new JWSBuilder().type(JWT).kid(activeKey.getKid()).jsonContent(token).sign(jwsAlgorithm, activeKey.getPrivateKey());
KeyManager.ActiveRsaKey activeRsaKey = session.keys().getActiveRsaKey(realm);
return new JWSBuilder().type(JWT).kid(activeRsaKey.getKid()).jsonContent(token).sign(jwsAlgorithm, activeRsaKey.getPrivateKey());
}
public AccessTokenResponseBuilder responseBuilder(RealmModel realm, ClientModel client, EventBuilder event, KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession) {
@ -734,7 +734,7 @@ public class TokenManager {
public AccessTokenResponse build() {
KeyManager.ActiveKey activeKey = session.keys().getActiveKey(realm);
KeyManager.ActiveRsaKey activeRsaKey = session.keys().getActiveRsaKey(realm);
if (accessToken != null) {
event.detail(Details.TOKEN_ID, accessToken.getId());
@ -751,7 +751,7 @@ public class TokenManager {
AccessTokenResponse res = new AccessTokenResponse();
if (accessToken != null) {
String encodedToken = new JWSBuilder().type(JWT).kid(activeKey.getKid()).jsonContent(accessToken).sign(jwsAlgorithm, activeKey.getPrivateKey());
String encodedToken = new JWSBuilder().type(JWT).kid(activeRsaKey.getKid()).jsonContent(accessToken).sign(jwsAlgorithm, activeRsaKey.getPrivateKey());
res.setToken(encodedToken);
res.setTokenType("bearer");
res.setSessionState(accessToken.getSessionState());
@ -769,11 +769,11 @@ public class TokenManager {
}
if (idToken != null) {
String encodedToken = new JWSBuilder().type(JWT).kid(activeKey.getKid()).jsonContent(idToken).sign(jwsAlgorithm, activeKey.getPrivateKey());
String encodedToken = new JWSBuilder().type(JWT).kid(activeRsaKey.getKid()).jsonContent(idToken).sign(jwsAlgorithm, activeRsaKey.getPrivateKey());
res.setIdToken(encodedToken);
}
if (refreshToken != null) {
String encodedToken = new JWSBuilder().type(JWT).kid(activeKey.getKid()).jsonContent(refreshToken).sign(jwsAlgorithm, activeKey.getPrivateKey());
String encodedToken = new JWSBuilder().type(JWT).kid(activeRsaKey.getKid()).jsonContent(refreshToken).sign(jwsAlgorithm, activeRsaKey.getPrivateKey());
res.setRefreshToken(encodedToken);
if (refreshToken.getExpiration() != 0) {
res.setRefreshExpiresIn(refreshToken.getExpiration() - Time.currentTime());

View file

@ -132,7 +132,7 @@ public class UserInfoEndpoint {
RSATokenVerifier verifier = RSATokenVerifier.create(tokenString)
.realmUrl(Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()));
String kid = verifier.getHeader().getKeyId();
verifier.publicKey(session.keys().getPublicKey(realm, kid));
verifier.publicKey(session.keys().getRsaPublicKey(realm, kid));
token = verifier.verify().getToken();
} catch (VerificationException e) {
event.error(Errors.INVALID_TOKEN);
@ -194,7 +194,7 @@ public class UserInfoEndpoint {
claims.put("aud", audience);
Algorithm signatureAlg = cfg.getUserInfoSignedResponseAlg();
PrivateKey privateKey = session.keys().getActiveKey(realm).getPrivateKey();
PrivateKey privateKey = session.keys().getActiveRsaKey(realm).getPrivateKey();
String signedUserInfo = new JWSBuilder()
.jsonContent(claims)

View file

@ -382,7 +382,7 @@ public class SamlProtocol implements LoginProtocol {
Document samlDocument = null;
KeyManager keyManager = session.keys();
KeyManager.ActiveKey keys = keyManager.getActiveKey(realm);
KeyManager.ActiveRsaKey keys = keyManager.getActiveRsaKey(realm);
boolean postBinding = isPostBinding(clientSession);
String keyName = samlClient.getXmlSigKeyInfoKeyNameTransformer().getKeyName(keys.getKid(), keys.getCertificate());
@ -518,7 +518,7 @@ public class SamlProtocol implements LoginProtocol {
String bindingUri = getLogoutServiceUrl(uriInfo, client, SAML_REDIRECT_BINDING);
SAML2LogoutRequestBuilder logoutBuilder = createLogoutRequest(bindingUri, clientSession, client);
if (samlClient.requiresRealmSignature() && samlClient.addExtensionsElementWithKeyInfo()) {
KeyManager.ActiveKey keys = session.keys().getActiveKey(realm);
KeyManager.ActiveRsaKey keys = session.keys().getActiveRsaKey(realm);
String keyName = samlClient.getXmlSigKeyInfoKeyNameTransformer().getKeyName(keys.getKid(), keys.getCertificate());
logoutBuilder.addExtension(new KeycloakKeySamlExtensionGenerator(keyName));
}
@ -561,7 +561,7 @@ public class SamlProtocol implements LoginProtocol {
if (canonicalization != null) {
binding.canonicalizationMethod(canonicalization);
}
KeyManager.ActiveKey keys = session.keys().getActiveKey(realm);
KeyManager.ActiveRsaKey keys = session.keys().getActiveRsaKey(realm);
XmlKeyInfoKeyNameTransformer transformer = XmlKeyInfoKeyNameTransformer.from(
userSession.getNote(SAML_SERVER_SIGNATURE_KEYINFO_KEY_NAME_TRANSFORMER),
SamlClient.DEFAULT_XML_KEY_INFO_KEY_NAME_TRANSFORMER);
@ -668,7 +668,7 @@ public class SamlProtocol implements LoginProtocol {
private JaxrsSAML2BindingBuilder createBindingBuilder(SamlClient samlClient) {
JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder();
if (samlClient.requiresRealmSignature()) {
KeyManager.ActiveKey keys = session.keys().getActiveKey(realm);
KeyManager.ActiveRsaKey keys = session.keys().getActiveRsaKey(realm);
String keyName = samlClient.getXmlSigKeyInfoKeyNameTransformer().getKeyName(keys.getKid(), keys.getCertificate());
binding.signatureAlgorithm(samlClient.getSignatureAlgorithm()).signWith(keyName, keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate()).signDocument();
}

View file

@ -36,6 +36,7 @@ import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.keys.RsaKeyMetadata;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeyManager;
@ -416,7 +417,7 @@ public class SamlService extends AuthorizationEndpointBase {
boolean postBinding = SamlProtocol.SAML_POST_BINDING.equals(logoutBinding);
if (samlClient.requiresRealmSignature()) {
SignatureAlgorithm algorithm = samlClient.getSignatureAlgorithm();
KeyManager.ActiveKey keys = session.keys().getActiveKey(realm);
KeyManager.ActiveRsaKey keys = session.keys().getActiveRsaKey(realm);
binding.signatureAlgorithm(algorithm).signWith(keys.getKid(), keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate()).signDocument();
if (! postBinding && samlClient.addExtensionsElementWithKeyInfo()) { // Only include extension if REDIRECT binding and signing whole SAML protocol message
builder.addExtension(new KeycloakKeySamlExtensionGenerator(keys.getKid()));
@ -567,18 +568,18 @@ public class SamlService extends AuthorizationEndpointBase {
props.put("idp.sso.HTTP-Redirect", RealmsResource.protocolUrl(uriInfo).build(realm.getName(), SamlProtocol.LOGIN_PROTOCOL).toString());
props.put("idp.sls.HTTP-POST", RealmsResource.protocolUrl(uriInfo).build(realm.getName(), SamlProtocol.LOGIN_PROTOCOL).toString());
StringBuilder keysString = new StringBuilder();
Set<KeyMetadata> keys = new TreeSet<>((o1, o2) -> o1.getStatus() == o2.getStatus() // Status can be only PASSIVE OR ACTIVE, push PASSIVE to end of list
Set<RsaKeyMetadata> keys = new TreeSet<>((o1, o2) -> o1.getStatus() == o2.getStatus() // Status can be only PASSIVE OR ACTIVE, push PASSIVE to end of list
? (int) (o2.getProviderPriority() - o1.getProviderPriority())
: (o1.getStatus() == KeyMetadata.Status.PASSIVE ? 1 : -1));
keys.addAll(session.keys().getKeys(realm, false));
for (KeyMetadata key : keys) {
keys.addAll(session.keys().getRsaKeys(realm, false));
for (RsaKeyMetadata key : keys) {
addKeyInfo(keysString, key, KeyTypes.SIGNING.value());
}
props.put("idp.signing.certificates", keysString.toString());
return StringPropertyReplacer.replaceProperties(template, props);
}
private static void addKeyInfo(StringBuilder target, KeyMetadata key, String purpose) {
private static void addKeyInfo(StringBuilder target, RsaKeyMetadata key, String purpose) {
if (key == null) {
return;
}

View file

@ -19,6 +19,7 @@ package org.keycloak.protocol.saml.installation;
import org.keycloak.Config;
import org.keycloak.common.util.PemUtils;
import org.keycloak.keys.RsaKeyMetadata;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
@ -87,11 +88,11 @@ public class SamlIDPDescriptorClientInstallation implements ClientInstallationPr
}
// keys
Set<KeyMetadata> keys = new TreeSet<>((o1, o2) -> o1.getStatus() == o2.getStatus() // Status can be only PASSIVE OR ACTIVE, push PASSIVE to end of list
Set<RsaKeyMetadata> keys = new TreeSet<>((o1, o2) -> o1.getStatus() == o2.getStatus() // Status can be only PASSIVE OR ACTIVE, push PASSIVE to end of list
? (int) (o2.getProviderPriority() - o1.getProviderPriority())
: (o1.getStatus() == KeyMetadata.Status.PASSIVE ? 1 : -1));
keys.addAll(session.keys().getKeys(realm, false));
for (KeyMetadata key : keys) {
keys.addAll(session.keys().getRsaKeys(realm, false));
for (RsaKeyMetadata key : keys) {
addKeyInfo(sb, key, KeyTypes.SIGNING.value());
}
@ -100,7 +101,7 @@ public class SamlIDPDescriptorClientInstallation implements ClientInstallationPr
return sb.toString();
}
private static void addKeyInfo(StringBuilder target, KeyMetadata key, String purpose) {
private static void addKeyInfo(StringBuilder target, RsaKeyMetadata key, String purpose) {
if (key == null) {
return;
}

View file

@ -75,7 +75,7 @@ public class ClientRegistrationTokenUtils {
return TokenVerification.error(new RuntimeException("Invalid token", e));
}
PublicKey publicKey = session.keys().getPublicKey(realm, input.getHeader().getKeyId());
PublicKey publicKey = session.keys().getRsaPublicKey(realm, input.getHeader().getKeyId());
if (!RSAProvider.verify(input, publicKey)) {
return TokenVerification.error(new RuntimeException("Failed verify token"));
@ -115,7 +115,7 @@ public class ClientRegistrationTokenUtils {
jwt.issuer(issuer);
jwt.audience(issuer);
KeyManager.ActiveKey keys = session.keys().getActiveKey(realm);
KeyManager.ActiveRsaKey keys = session.keys().getActiveRsaKey(realm);
String token = new JWSBuilder().kid(keys.getKid()).jsonContent(jwt).rsa256(keys.getPrivateKey());
return token;

View file

@ -19,7 +19,7 @@ package org.keycloak.services.managers;
import org.jboss.logging.Logger;
import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.RSATokenVerifier;
import org.keycloak.TokenVerifier;
import org.keycloak.authentication.RequiredActionContext;
import org.keycloak.authentication.RequiredActionContextResult;
import org.keycloak.authentication.RequiredActionFactory;
@ -33,6 +33,7 @@ import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.forms.login.LoginFormsProvider;
import org.keycloak.jose.jws.AlgorithmType;
import org.keycloak.jose.jws.JWSBuilder;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionModel;
@ -58,6 +59,7 @@ import org.keycloak.services.resources.RealmsResource;
import org.keycloak.services.util.CookieHelper;
import org.keycloak.services.util.P3PHelper;
import javax.crypto.SecretKey;
import javax.ws.rs.core.Cookie;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MultivaluedMap;
@ -113,12 +115,12 @@ public class AuthenticationManager {
if (cookie == null) return;
String tokenString = cookie.getValue();
RSATokenVerifier verifier = RSATokenVerifier.create(tokenString).realmUrl(Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName())).checkActive(false).checkTokenType(false);
TokenVerifier verifier = TokenVerifier.create(tokenString).realmUrl(Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName())).checkActive(false).checkTokenType(false);
String kid = verifier.getHeader().getKeyId();
PublicKey publicKey = session.keys().getPublicKey(realm, kid);
SecretKey secretKey = session.keys().getHmacSecretKey(realm, kid);
AccessToken token = verifier.publicKey(publicKey).verify().getToken();
AccessToken token = verifier.secretKey(secretKey).verify().getToken();
UserSessionModel cookieSession = session.sessions().getUserSession(realm, token.getSessionState());
if (cookieSession == null || !cookieSession.getId().equals(userSession.getId())) return;
expireIdentityCookie(realm, uriInfo, connection);
@ -337,14 +339,14 @@ public class AuthenticationManager {
}
protected static String encodeToken(KeycloakSession session, RealmModel realm, Object token) {
KeyManager.ActiveKey activeKey = session.keys().getActiveKey(realm);
KeyManager.ActiveHmacKey activeKey = session.keys().getActiveHmacKey(realm);
logger.tracef("Encoding token with kid '%s'", activeKey.getKid());
String encodedToken = new JWSBuilder()
.kid(activeKey.getKid())
.jsonContent(token)
.rsa256(activeKey.getPrivateKey());
.hmac256(activeKey.getSecretKey());
return encodedToken;
}
@ -691,15 +693,25 @@ public class AuthenticationManager {
protected static AuthResult verifyIdentityToken(KeycloakSession session, RealmModel realm, UriInfo uriInfo, ClientConnection connection, boolean checkActive, boolean checkTokenType,
String tokenString, HttpHeaders headers) {
try {
RSATokenVerifier verifier = RSATokenVerifier.create(tokenString).realmUrl(Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName())).checkActive(checkActive).checkTokenType(checkTokenType);
TokenVerifier verifier = TokenVerifier.create(tokenString).realmUrl(Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName())).checkActive(checkActive).checkTokenType(checkTokenType);
String kid = verifier.getHeader().getKeyId();
AlgorithmType algorithmType = verifier.getHeader().getAlgorithm().getType();
PublicKey publicKey = session.keys().getPublicKey(realm, kid);
if (publicKey == null) {
logger.debugf("Identity cookie signed with unknown kid '%s'", kid);
return null;
if (AlgorithmType.RSA.equals(algorithmType)) {
PublicKey publicKey = session.keys().getRsaPublicKey(realm, kid);
if (publicKey == null) {
logger.debugf("Identity cookie signed with unknown kid '%s'", kid);
return null;
}
verifier.publicKey(publicKey);
} else if (AlgorithmType.HMAC.equals(algorithmType)) {
SecretKey secretKey = session.keys().getHmacSecretKey(realm, kid);
if (secretKey == null) {
logger.debugf("Identity cookie signed with unknown kid '%s'", kid);
return null;
}
verifier.secretKey(secretKey);
}
verifier.publicKey(publicKey);
AccessToken token = verifier.verify().getToken();
if (checkActive) {

View file

@ -93,7 +93,7 @@ public class PublicRealmResource {
rep.setTokenServiceUrl(OIDCLoginProtocolService.tokenServiceBaseUrl(uriInfo).build(realm.getName()).toString());
rep.setAccountServiceUrl(AccountService.accountServiceBaseUrl(uriInfo).build(realm.getName()).toString());
rep.setAdminApiUrl(uriInfo.getBaseUriBuilder().path(AdminRoot.class).build().toString());
rep.setPublicKeyPem(PemUtils.encodeKey(session.keys().getActiveKey(realm).getPublicKey()));
rep.setPublicKeyPem(PemUtils.encodeKey(session.keys().getActiveRsaKey(realm).getPublicKey()));
rep.setNotBefore(realm.getNotBefore());
return rep;
}

View file

@ -34,7 +34,6 @@ import org.keycloak.models.KeyManager;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.utils.JWKSHttpUtils;
import org.keycloak.representations.KeyStoreConfig;
import org.keycloak.representations.idm.CertificateRepresentation;
import org.keycloak.services.ErrorResponseException;
@ -374,8 +373,8 @@ public class ClientAttributeCertificateResource {
if (config.isRealmCertificate() == null || config.isRealmCertificate().booleanValue()) {
KeyManager keys = session.keys();
String kid = keys.getActiveKey(realm).getKid();
Certificate certificate = keys.getCertificate(realm, kid);
String kid = keys.getActiveRsaKey(realm).getKid();
Certificate certificate = keys.getRsaCertificate(realm, kid);
String certificateAlias = config.getRealmAlias();
if (certificateAlias == null) certificateAlias = realm.getName();
keyStore.setCertificateEntry(certificateAlias, certificate);

View file

@ -19,7 +19,9 @@ package org.keycloak.services.resources.admin;
import org.jboss.resteasy.annotations.cache.NoCache;
import org.keycloak.common.util.PemUtils;
import org.keycloak.keys.KeyMetadata;
import org.keycloak.jose.jws.AlgorithmType;
import org.keycloak.keys.HmacKeyMetadata;
import org.keycloak.keys.RsaKeyMetadata;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeyManager;
import org.keycloak.models.RealmModel;
@ -28,9 +30,10 @@ import org.keycloak.representations.idm.KeysMetadataRepresentation;
import javax.ws.rs.GET;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -56,20 +59,33 @@ public class KeyResource {
KeyManager keystore = session.keys();
KeysMetadataRepresentation keys = new KeysMetadataRepresentation();
keys.setActive(Collections.singletonMap(KeyMetadata.Type.RSA.name(), keystore.getActiveKey(realm).getKid()));
Map<String, String> active = new HashMap<>();
active.put(AlgorithmType.RSA.name(), keystore.getActiveRsaKey(realm).getKid());
active.put(AlgorithmType.HMAC.name(), keystore.getActiveHmacKey(realm).getKid());
keys.setActive(active);
List<KeysMetadataRepresentation.KeyMetadataRepresentation> l = new LinkedList<>();
for (KeyMetadata m : session.keys().getKeys(realm, true)) {
for (RsaKeyMetadata m : session.keys().getRsaKeys(realm, true)) {
KeysMetadataRepresentation.KeyMetadataRepresentation r = new KeysMetadataRepresentation.KeyMetadataRepresentation();
r.setProviderId(m.getProviderId());
r.setProviderPriority(m.getProviderPriority());
r.setKid(m.getKid());
r.setStatus(m.getStatus() != null ? m.getStatus().name() : null);
r.setType(m.getType() != null ? m.getType().name() : null);
r.setType(AlgorithmType.RSA.name());
r.setPublicKey(PemUtils.encodeKey(m.getPublicKey()));
r.setCertificate(PemUtils.encodeCertificate(m.getCertificate()));
l.add(r);
}
for (HmacKeyMetadata m : session.keys().getHmacKeys(realm, true)) {
KeysMetadataRepresentation.KeyMetadataRepresentation r = new KeysMetadataRepresentation.KeyMetadataRepresentation();
r.setProviderId(m.getProviderId());
r.setProviderPriority(m.getProviderPriority());
r.setKid(m.getKid());
r.setStatus(m.getStatus() != null ? m.getStatus().name() : null);
r.setType(AlgorithmType.HMAC.name());
l.add(r);
}
keys.setKeys(l);

View file

@ -15,6 +15,7 @@
# limitations under the License.
#
org.keycloak.keys.GeneratedHmacKeyProviderFactory
org.keycloak.keys.GeneratedRsaKeyProviderFactory
org.keycloak.keys.JavaKeystoreKeyProviderFactory
org.keycloak.keys.RsaKeyProviderFactory
org.keycloak.keys.ImportedRsaKeyProviderFactory

View file

@ -20,6 +20,7 @@ package org.keycloak.testsuite.rest;
import org.infinispan.Cache;
import org.jboss.resteasy.annotations.cache.NoCache;
import org.jboss.resteasy.spi.BadRequestException;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.common.util.Time;
import org.keycloak.component.ComponentModel;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
@ -633,6 +634,14 @@ public class TestingResourceProvider implements RealmResourceProvider {
return reps;
}
@GET
@Path("/component")
@Produces(MediaType.APPLICATION_JSON)
public MultivaluedHashMap<String, String> getComponentConfig(@QueryParam("componentId") String componentId) {
RealmModel realm = session.getContext().getRealm();
return realm.getComponent(componentId).getConfig();
}
@GET
@Path("/smtp-config")
@Produces(MediaType.APPLICATION_JSON)

View file

@ -18,6 +18,7 @@
package org.keycloak.testsuite.client.resources;
import org.jboss.resteasy.annotations.cache.NoCache;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.representations.idm.AdminEventRepresentation;
import org.keycloak.representations.idm.AuthenticationFlowRepresentation;
import org.keycloak.representations.idm.EventRepresentation;
@ -251,4 +252,8 @@ public interface TestingResource {
@Produces(MediaType.APPLICATION_JSON)
Map<String, String> getIdentityProviderConfig(@QueryParam("alias") String alias);
@GET
@Path("/component")
@Produces(MediaType.APPLICATION_JSON)
MultivaluedHashMap<String, String> getComponentConfig(@QueryParam("componentId") String componentId);
}

View file

@ -19,6 +19,7 @@ package org.keycloak.testsuite.adapter.servlet;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import javax.ws.rs.core.Response;
@ -41,6 +42,7 @@ import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.common.util.StreamUtil;
import org.keycloak.common.util.Time;
import org.keycloak.constants.AdapterConstants;
import org.keycloak.jose.jws.AlgorithmType;
import org.keycloak.keys.KeyProvider;
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
@ -48,6 +50,7 @@ import org.keycloak.representations.AccessToken;
import org.keycloak.representations.adapters.action.GlobalRequestResult;
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.adapter.AbstractServletsAdapterTest;
import org.keycloak.testsuite.adapter.filter.AdapterActionsFilter;
@ -58,9 +61,7 @@ import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.util.URLAssert;
import org.openqa.selenium.By;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.*;
import static org.keycloak.testsuite.auth.page.AuthRealm.DEMO;
import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlEquals;
import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith;
@ -201,9 +202,9 @@ public abstract class AbstractOIDCPublicKeyRotationAdapterTest extends AbstractS
Assert.assertEquals(200, status);
// Re-generate realm public key and remove the old key
String oldKeyId = getActiveKeyId();
String oldActiveKeyProviderId = getActiveKeyProvider();
generateNewRealmKey();
adminClient.realm(DEMO).components().component(oldKeyId).remove();
adminClient.realm(DEMO).components().component(oldActiveKeyProviderId).remove();
// Send REST request to the customer-db app. Should be still succcessfully authenticated as the JWKPublicKeyLocator cache is still valid
status = invokeRESTEndpoint(accessTokenString);
@ -237,7 +238,8 @@ public abstract class AbstractOIDCPublicKeyRotationAdapterTest extends AbstractS
String accessTokenString = tokenMinTTLPage.getAccessTokenString();
// Generate new realm public key
String oldKeyId = getActiveKeyId();
String oldActiveKeyProviderId = getActiveKeyProvider();
generateNewRealmKey();
// Send REST request to customer-db app. It should be successfully authenticated even that token is signed by the old key
@ -245,7 +247,7 @@ public abstract class AbstractOIDCPublicKeyRotationAdapterTest extends AbstractS
Assert.assertEquals(200, status);
// Remove the old realm key now
adminClient.realm(DEMO).components().component(oldKeyId).remove();
adminClient.realm(DEMO).components().component(oldActiveKeyProviderId).remove();
// Set some offset to ensure pushing notBefore will pass
setAdapterAndServerTimeOffset(130, customerDb.toString() + "/unsecured/foo", tokenMinTTLPage.toString() + "/unsecured/foo");
@ -295,13 +297,17 @@ public abstract class AbstractOIDCPublicKeyRotationAdapterTest extends AbstractS
response.close();
}
private String getActiveKeyId() {
String realmId = adminClient.realm(DEMO).toRepresentation().getId();
return adminClient.realm(DEMO).components().query(realmId, KeyProvider.class.getName())
.get(0).getId();
private String getActiveKeyProvider() {
KeysMetadataRepresentation keyMetadata = adminClient.realm(DEMO).keys().getKeyMetadata();
String activeKid = keyMetadata.getActive().get(AlgorithmType.RSA.name());
for (KeysMetadataRepresentation.KeyMetadataRepresentation rep : keyMetadata.getKeys()) {
if (rep.getKid().equals(activeKid)) {
return rep.getProviderId();
}
}
return null;
}
private int invokeRESTEndpoint(String accessTokenString) {
HttpClient client = new DefaultHttpClient();

View file

@ -30,8 +30,7 @@ import org.keycloak.common.util.KeyUtils;
import org.keycloak.common.util.PemUtils;
import org.keycloak.keys.Attributes;
import org.keycloak.keys.KeyProvider;
import org.keycloak.keys.RsaKeyProviderFactory;
import org.keycloak.protocol.saml.SamlClient;
import org.keycloak.keys.ImportedRsaKeyProviderFactory;
import org.keycloak.protocol.saml.SamlConfigAttributes;
import org.keycloak.representations.idm.ComponentRepresentation;
import org.keycloak.protocol.saml.mappers.AttributeStatementHelper;
@ -452,7 +451,7 @@ public abstract class AbstractSAMLServletsAdapterTest extends AbstractServletsAd
ComponentRepresentation rep = new ComponentRepresentation();
rep.setName("mycomponent");
rep.setParentId("demo");
rep.setProviderId(RsaKeyProviderFactory.ID);
rep.setProviderId(ImportedRsaKeyProviderFactory.ID);
rep.setProviderType(KeyProvider.class.getName());
org.keycloak.common.util.MultivaluedHashMap config = new org.keycloak.common.util.MultivaluedHashMap();

View file

@ -0,0 +1,182 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.testsuite.keys;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.common.util.Base64Url;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.common.util.PemUtils;
import org.keycloak.jose.jws.AlgorithmType;
import org.keycloak.jose.jws.crypto.HMACProvider;
import org.keycloak.keys.GeneratedHmacKeyProviderFactory;
import org.keycloak.keys.GeneratedRsaKeyProviderFactory;
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.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;
import javax.ws.rs.core.Response;
import java.security.interfaces.RSAPublicKey;
import java.util.List;
import static org.junit.Assert.*;
import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class GeneratedHmacKeyProviderTest extends AbstractKeycloakTest {
@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 defaultKeysize() throws Exception {
long priority = System.currentTimeMillis();
ComponentRepresentation rep = createRep("valid", GeneratedHmacKeyProviderFactory.ID);
rep.setConfig(new MultivaluedHashMap<>());
rep.getConfig().putSingle("priority", Long.toString(priority));
Response response = adminClient.realm("test").components().add(rep);
String id = ApiUtil.getCreatedId(response);
ComponentRepresentation createdRep = adminClient.realm("test").components().component(id).toRepresentation();
assertEquals(1, createdRep.getConfig().size());
assertEquals(Long.toString(priority), createdRep.getConfig().getFirst("priority"));
KeysMetadataRepresentation keys = adminClient.realm("test").keys().getKeyMetadata();
KeysMetadataRepresentation.KeyMetadataRepresentation key = null;
for (KeysMetadataRepresentation.KeyMetadataRepresentation k : keys.getKeys()) {
if (k.getType().equals(AlgorithmType.HMAC.name())) {
key = k;
break;
}
}
assertEquals(id, key.getProviderId());
assertEquals(AlgorithmType.HMAC.name(), key.getType());
assertEquals(priority, key.getProviderPriority());
assertEquals(32, Base64Url.decode(testingClient.testing("test").getComponentConfig(id).getFirst("secret")).length);
}
@Test
public void largeKeysize() throws Exception {
long priority = System.currentTimeMillis();
ComponentRepresentation rep = createRep("valid", GeneratedHmacKeyProviderFactory.ID);
rep.setConfig(new MultivaluedHashMap<>());
rep.getConfig().putSingle("priority", Long.toString(priority));
rep.getConfig().putSingle("secretSize", "512");
Response response = adminClient.realm("test").components().add(rep);
String id = ApiUtil.getCreatedId(response);
ComponentRepresentation createdRep = adminClient.realm("test").components().component(id).toRepresentation();
assertEquals(2, createdRep.getConfig().size());
assertEquals("512", createdRep.getConfig().getFirst("secretSize"));
KeysMetadataRepresentation keys = adminClient.realm("test").keys().getKeyMetadata();
KeysMetadataRepresentation.KeyMetadataRepresentation key = null;
for (KeysMetadataRepresentation.KeyMetadataRepresentation k : keys.getKeys()) {
if (k.getType().equals(AlgorithmType.HMAC.name())) {
key = k;
break;
}
}
assertEquals(id, key.getProviderId());
assertEquals(AlgorithmType.HMAC.name(), key.getType());
assertEquals(priority, key.getProviderPriority());
assertEquals(512, Base64Url.decode(testingClient.testing("test").getComponentConfig(id).getFirst("secret")).length);
}
@Test
public void updateKeysize() throws Exception {
long priority = System.currentTimeMillis();
ComponentRepresentation rep = createRep("valid", GeneratedHmacKeyProviderFactory.ID);
rep.setConfig(new MultivaluedHashMap<>());
rep.getConfig().putSingle("priority", Long.toString(priority));
Response response = adminClient.realm("test").components().add(rep);
String id = ApiUtil.getCreatedId(response);
assertEquals(32, Base64Url.decode(testingClient.testing("test").getComponentConfig(id).getFirst("secret")).length);
ComponentRepresentation createdRep = adminClient.realm("test").components().component(id).toRepresentation();
createdRep.getConfig().putSingle("secretSize", "512");
adminClient.realm("test").components().component(id).update(createdRep);
assertEquals(512, Base64Url.decode(testingClient.testing("test").getComponentConfig(id).getFirst("secret")).length);
}
@Test
public void invalidKeysize() throws Exception {
ComponentRepresentation rep = createRep("invalid", GeneratedHmacKeyProviderFactory.ID);
rep.getConfig().putSingle("secretSize", "1234");
Response response = adminClient.realm("test").components().add(rep);
assertErrror(response, "'Secret size' should be 32, 64, 128, 256 or 512");
}
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());
}
protected ComponentRepresentation createRep(String name, String providerId) {
ComponentRepresentation rep = new ComponentRepresentation();
rep.setName(name);
rep.setParentId("test");
rep.setProviderId(providerId);
rep.setProviderType(KeyProvider.class.getName());
rep.setConfig(new MultivaluedHashMap<>());
return rep;
}
}

View file

@ -22,6 +22,7 @@ import org.junit.Rule;
import org.junit.Test;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.common.util.PemUtils;
import org.keycloak.jose.jws.AlgorithmType;
import org.keycloak.keys.GeneratedRsaKeyProviderFactory;
import org.keycloak.keys.KeyMetadata;
import org.keycloak.keys.KeyProvider;
@ -45,7 +46,7 @@ import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class RsaGeneratedKeyProviderTest extends AbstractKeycloakTest {
public class GeneratedRsaKeyProviderTest extends AbstractKeycloakTest {
@Rule
public AssertEvents events = new AssertEvents(this);
@ -74,16 +75,15 @@ public class RsaGeneratedKeyProviderTest extends AbstractKeycloakTest {
String id = ApiUtil.getCreatedId(response);
ComponentRepresentation createdRep = adminClient.realm("test").components().component(id).toRepresentation();
assertEquals(2, createdRep.getConfig().size());
assertEquals(1, createdRep.getConfig().size());
assertEquals(Long.toString(priority), createdRep.getConfig().getFirst("priority"));
assertEquals("2048", createdRep.getConfig().getFirst("keySize"));
KeysMetadataRepresentation keys = adminClient.realm("test").keys().getKeyMetadata();
KeysMetadataRepresentation.KeyMetadataRepresentation key = keys.getKeys().get(0);
assertEquals(id, key.getProviderId());
assertEquals(KeyMetadata.Type.RSA.name(), key.getType());
assertEquals(AlgorithmType.RSA.name(), key.getType());
assertEquals(priority, key.getProviderPriority());
assertEquals(2048, ((RSAPublicKey) PemUtils.decodePublicKey(keys.getKeys().get(0).getPublicKey())).getModulus().bitLength());
}
@ -109,7 +109,7 @@ public class RsaGeneratedKeyProviderTest extends AbstractKeycloakTest {
KeysMetadataRepresentation.KeyMetadataRepresentation key = keys.getKeys().get(0);
assertEquals(id, key.getProviderId());
assertEquals(KeyMetadata.Type.RSA.name(), key.getType());
assertEquals(AlgorithmType.RSA.name(), key.getType());
assertEquals(priority, key.getProviderPriority());
assertEquals(4096, ((RSAPublicKey) PemUtils.decodePublicKey(keys.getKeys().get(0).getPublicKey())).getModulus().bitLength());
}
@ -177,7 +177,7 @@ public class RsaGeneratedKeyProviderTest extends AbstractKeycloakTest {
rep.getConfig().putSingle("keySize", "1234");
Response response = adminClient.realm("test").components().add(rep);
assertErrror(response, "Keysize should be 1024, 2048 or 4096");
assertErrror(response, "'Key size' should be 1024, 2048 or 4096");
}
protected void assertErrror(Response response, String error) {

View file

@ -24,10 +24,11 @@ import org.keycloak.common.util.CertificateUtils;
import org.keycloak.common.util.KeyUtils;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.common.util.PemUtils;
import org.keycloak.jose.jws.AlgorithmType;
import org.keycloak.keys.Attributes;
import org.keycloak.keys.ImportedRsaKeyProviderFactory;
import org.keycloak.keys.KeyMetadata;
import org.keycloak.keys.KeyProvider;
import org.keycloak.keys.RsaKeyProviderFactory;
import org.keycloak.representations.idm.ComponentRepresentation;
import org.keycloak.representations.idm.ErrorRepresentation;
import org.keycloak.representations.idm.KeysMetadataRepresentation;
@ -49,7 +50,7 @@ import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class RsaKeyProviderTest extends AbstractKeycloakTest {
public class ImportedRsaKeyProviderTest extends AbstractKeycloakTest {
@Rule
public AssertEvents events = new AssertEvents(this);
@ -73,7 +74,7 @@ public class RsaKeyProviderTest extends AbstractKeycloakTest {
KeyPair keyPair = KeyUtils.generateRsaKeyPair(2048);
String kid = KeyUtils.createKeyId(keyPair.getPublic());
ComponentRepresentation rep = createRep("valid", RsaKeyProviderFactory.ID);
ComponentRepresentation rep = createRep("valid", ImportedRsaKeyProviderFactory.ID);
rep.getConfig().putSingle(Attributes.PRIVATE_KEY_KEY, PemUtils.encodeKey(keyPair.getPrivate()));
rep.getConfig().putSingle(Attributes.PRIORITY_KEY, Long.toString(priority));
@ -88,12 +89,12 @@ public class RsaKeyProviderTest extends AbstractKeycloakTest {
KeysMetadataRepresentation keys = adminClient.realm("test").keys().getKeyMetadata();
assertEquals(kid, keys.getActive().get(KeyMetadata.Type.RSA.name()));
assertEquals(kid, keys.getActive().get(AlgorithmType.RSA.name()));
KeysMetadataRepresentation.KeyMetadataRepresentation key = keys.getKeys().get(0);
assertEquals(id, key.getProviderId());
assertEquals(KeyMetadata.Type.RSA.name(), key.getType());
assertEquals(AlgorithmType.RSA.name(), key.getType());
assertEquals(priority, key.getProviderPriority());
assertEquals(kid, key.getKid());
assertEquals(PemUtils.encodeKey(keyPair.getPublic()), keys.getKeys().get(0).getPublicKey());
@ -108,7 +109,7 @@ public class RsaKeyProviderTest extends AbstractKeycloakTest {
Certificate certificate = CertificateUtils.generateV1SelfSignedCertificate(keyPair, "test");
String certificatePem = PemUtils.encodeCertificate(certificate);
ComponentRepresentation rep = createRep("valid", RsaKeyProviderFactory.ID);
ComponentRepresentation rep = createRep("valid", ImportedRsaKeyProviderFactory.ID);
rep.getConfig().putSingle(Attributes.PRIVATE_KEY_KEY, PemUtils.encodeKey(keyPair.getPrivate()));
rep.getConfig().putSingle(Attributes.CERTIFICATE_KEY, certificatePem);
rep.getConfig().putSingle(Attributes.PRIORITY_KEY, Long.toString(priority));
@ -130,7 +131,7 @@ public class RsaKeyProviderTest extends AbstractKeycloakTest {
public void invalidPriority() throws Exception {
KeyPair keyPair = KeyUtils.generateRsaKeyPair(2048);
ComponentRepresentation rep = createRep("invalid", RsaKeyProviderFactory.ID);
ComponentRepresentation rep = createRep("invalid", ImportedRsaKeyProviderFactory.ID);
rep.getConfig().putSingle(Attributes.PRIVATE_KEY_KEY, PemUtils.encodeKey(keyPair.getPrivate()));
rep.getConfig().putSingle(Attributes.PRIORITY_KEY, "invalid");
@ -142,7 +143,7 @@ public class RsaKeyProviderTest extends AbstractKeycloakTest {
public void invalidEnabled() throws Exception {
KeyPair keyPair = KeyUtils.generateRsaKeyPair(2048);
ComponentRepresentation rep = createRep("invalid", RsaKeyProviderFactory.ID);
ComponentRepresentation rep = createRep("invalid", ImportedRsaKeyProviderFactory.ID);
rep.getConfig().putSingle(Attributes.PRIVATE_KEY_KEY, PemUtils.encodeKey(keyPair.getPrivate()));
rep.getConfig().putSingle(Attributes.ENABLED_KEY, "invalid");
@ -154,7 +155,7 @@ public class RsaKeyProviderTest extends AbstractKeycloakTest {
public void invalidActive() throws Exception {
KeyPair keyPair = KeyUtils.generateRsaKeyPair(2048);
ComponentRepresentation rep = createRep("invalid", RsaKeyProviderFactory.ID);
ComponentRepresentation rep = createRep("invalid", ImportedRsaKeyProviderFactory.ID);
rep.getConfig().putSingle(Attributes.PRIVATE_KEY_KEY, PemUtils.encodeKey(keyPair.getPrivate()));
rep.getConfig().putSingle(Attributes.ACTIVE_KEY, "invalid");
@ -166,7 +167,7 @@ public class RsaKeyProviderTest extends AbstractKeycloakTest {
public void invalidPrivateKey() throws Exception {
KeyPair keyPair = KeyUtils.generateRsaKeyPair(2048);
ComponentRepresentation rep = createRep("invalid", RsaKeyProviderFactory.ID);
ComponentRepresentation rep = createRep("invalid", ImportedRsaKeyProviderFactory.ID);
Response response = adminClient.realm("test").components().add(rep);
assertErrror(response, "'Private RSA Key' is required");
@ -185,7 +186,7 @@ public class RsaKeyProviderTest extends AbstractKeycloakTest {
KeyPair keyPair = KeyUtils.generateRsaKeyPair(2048);
Certificate invalidCertificate = CertificateUtils.generateV1SelfSignedCertificate(KeyUtils.generateRsaKeyPair(2048), "test");
ComponentRepresentation rep = createRep("invalid", RsaKeyProviderFactory.ID);
ComponentRepresentation rep = createRep("invalid", ImportedRsaKeyProviderFactory.ID);
rep.getConfig().putSingle(Attributes.PRIVATE_KEY_KEY, PemUtils.encodeKey(keyPair.getPrivate()));
rep.getConfig().putSingle(Attributes.CERTIFICATE_KEY, "nonsense");

View file

@ -23,6 +23,7 @@ import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.jose.jws.AlgorithmType;
import org.keycloak.keys.JavaKeystoreKeyProviderFactory;
import org.keycloak.keys.KeyMetadata;
import org.keycloak.keys.KeyProvider;
@ -103,7 +104,7 @@ public class JavaKeystoreKeyProviderTest extends AbstractKeycloakTest {
KeysMetadataRepresentation.KeyMetadataRepresentation key = keys.getKeys().get(0);
assertEquals(id, key.getProviderId());
assertEquals(KeyMetadata.Type.RSA.name(), key.getType());
assertEquals(AlgorithmType.RSA.name(), key.getType());
assertEquals(priority, key.getProviderPriority());
assertEquals(PUBLIC_KEY, key.getPublicKey());
assertEquals(CERTIFICATE, key.getCertificate());

View file

@ -28,14 +28,13 @@ import org.keycloak.common.util.KeyUtils;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.common.util.PemUtils;
import org.keycloak.keys.Attributes;
import org.keycloak.keys.GeneratedHmacKeyProviderFactory;
import org.keycloak.keys.KeyProvider;
import org.keycloak.keys.RsaKeyProviderFactory;
import org.keycloak.representations.UserInfo;
import org.keycloak.keys.ImportedRsaKeyProviderFactory;
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.storage.UserStorageProvider;
import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.pages.AppPage;
@ -237,7 +236,7 @@ public class KeyRotationTest extends AbstractKeycloakTest {
ComponentRepresentation rep = new ComponentRepresentation();
rep.setName("mycomponent");
rep.setParentId("test");
rep.setProviderId(RsaKeyProviderFactory.ID);
rep.setProviderId(ImportedRsaKeyProviderFactory.ID);
rep.setProviderType(KeyProvider.class.getName());
org.keycloak.common.util.MultivaluedHashMap config = new org.keycloak.common.util.MultivaluedHashMap();
@ -247,6 +246,18 @@ public class KeyRotationTest extends AbstractKeycloakTest {
adminClient.realm("test").components().add(rep);
rep = new ComponentRepresentation();
rep.setName("mycomponent2");
rep.setParentId("test");
rep.setProviderId(GeneratedHmacKeyProviderFactory.ID);
rep.setProviderType(KeyProvider.class.getName());
config = new org.keycloak.common.util.MultivaluedHashMap();
config.addFirst("priority", priority);
rep.setConfig(config);
adminClient.realm("test").components().add(rep);
return publicKey;
}
@ -259,13 +270,16 @@ public class KeyRotationTest extends AbstractKeycloakTest {
}
private void dropKeys(String priority) {
int r = 0;
for (ComponentRepresentation c : adminClient.realm("test").components().query("test", KeyProvider.class.getName())) {
if (c.getConfig().getFirst("priority").equals(priority)) {
adminClient.realm("test").components().component(c.getId()).remove();
return;
r++;
}
}
throw new RuntimeException("Failed to find keys1");
if (r != 2) {
throw new RuntimeException("Failed to find keys1");
}
}
private void assertUserInfo(String token, int expectedStatus) {

View file

@ -0,0 +1,92 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.testsuite.model;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.common.util.KeyUtils;
import org.keycloak.jose.jws.JWSBuilder;
import org.keycloak.protocol.RestartLoginCookie;
import org.keycloak.testsuite.rule.WebResource;
import org.keycloak.testsuite.rule.WebRule;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.security.KeyPair;
import static org.junit.Assert.assertEquals;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
@Ignore
public class SimplePerfTest {
@Rule
public WebRule webRule = new WebRule(this);
@WebResource
public WebDriver driver;
public static final String PORT = "8080";
public static final int WARMUP = 1000;
public static final int COUNT = 10000;
@Test
public void simplePerf() {
// Warm-up
for (int i = 0; i < WARMUP; i++) {
doLoginLogout(PORT);
}
long start = System.currentTimeMillis();
long s = start;
for (int i = 0; i < COUNT; i++) {
doLoginLogout(PORT);
if (i % 100 == 0) {
System.out.println(i + " " + (System.currentTimeMillis() - s) + " ms");
}
s = System.currentTimeMillis();
}
System.out.println("");
System.out.println("Average: " + ((System.currentTimeMillis() - start) / COUNT) + " ms");
System.out.println("Total: " + ((System.currentTimeMillis() - start)) + " ms");
}
private void doLoginLogout(String port) {
driver.navigate().to("http://localhost:" + port + "/auth/realms/master/account/");
driver.findElement(By.id("username")).sendKeys("admin");
driver.findElement(By.id("password")).sendKeys("admin");
driver.findElement(By.name("login")).click();
assertEquals("http://localhost:" + port + "/auth/realms/master/account/", driver.getCurrentUrl());
driver.findElement(By.linkText("Sign Out")).click();
}
}

View file

@ -39,6 +39,7 @@ sslRequired.option.all=all requests
sslRequired.option.external=external requests
sslRequired.option.none=none
sslRequired.tooltip=Is HTTPS required? 'None' means HTTPS is not required for any client IP address. 'External requests' means localhost and private IP addresses can access without HTTPS. 'All requests' means HTTPS is required for all IP addresses.
publicKeys=Public keys
publicKey=Public key
privateKey=Private key
gen-new-keys=Generate new keys

View file

@ -1126,6 +1126,14 @@ module.controller('RealmKeysProvidersCtrl', function($scope, Realm, realm, $http
type: 'org.keycloak.keys.KeyProvider'
}, function(data) {
$scope.instances = data;
for (var i = 0; i < $scope.instances.length; i++) {
for (var j = 0; j < $scope.providers.length; j++) {
if ($scope.providers[j].id === $scope.instances[i].providerId) {
$scope.instances[i].provider = $scope.providers[j];
}
}
}
});
$scope.addProvider = function(provider) {

View file

@ -27,28 +27,26 @@
<table class="table table-striped table-bordered">
<thead>
<tr>
<th>{{:: 'status' | translate}}</th>
<th>{{:: 'type' | translate}}</th>
<th>{{:: 'status' | translate}}</th>
<th>{{:: 'kid' | translate}}</th>
<th>{{:: 'priority' | translate}}</th>
<th>{{:: 'provider' | translate}}</th>
<th>{{:: 'publicKey' | translate}}</th>
<th>{{:: 'certificate' | translate}}</th>
<th colspan="2">{{:: 'publicKeys' | translate}}</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="key in keys">
<td>{{key.status}}</td>
<td>{{key.type}}</td>
<td>{{key.status}}</td>
<td>{{key.kid}}</td>
<td>{{key.providerPriority}}</td>
<td><a href="#/realms/{{realm.realm}}/keys/providers/{{key.provider.providerId}}/{{key.provider.id}}">{{key.provider.name}}</a></td>
<td data-ng-show="key.publicKey" class="kc-action-cell" data-ng-click="viewKey(key.publicKey)">{{:: 'view' | translate}}</td>
<td data-ng-hide="key.publicKey"></td>
<td data-ng-show="key.type === 'RSA'" class="kc-action-cell" data-ng-click="viewKey(key.publicKey)">{{:: 'publicKey' | translate}}</td>
<td data-ng-show="key.type === 'RSA'" class="kc-action-cell" data-ng-click="viewKey(key.certificate)">{{:: 'certificate' | translate}}</td>
<td data-ng-show="key.certificate" class="kc-action-cell" data-ng-click="viewKey(key.certificate)">{{:: 'view' | translate}}</td>
<td data-ng-hide="key.certificate"></td>
<td data-ng-show="key.type !== 'RSA'" colspan="2"></td>
</tr>
</tbody>
</table>

View file

@ -27,7 +27,7 @@
<table class="table table-striped table-bordered">
<thead>
<tr ng-show="providers.length > 0 && access.manageRealm">
<th colspan="6" class="kc-table-actions">
<th colspan="7" class="kc-table-actions">
<div class="pull-right">
<div>
<select class="form-control" ng-model="selectedProvider"
@ -40,6 +40,7 @@
</th>
</tr>
<tr data-ng-show="instances && instances.length > 0">
<th>{{:: 'type' | translate}}</th>
<th>{{:: 'name' | translate}}</th>
<th>{{:: 'id' | translate}}</th>
<th>{{:: 'provider' | translate}}</th>
@ -49,6 +50,7 @@
</thead>
<tbody>
<tr ng-repeat="instance in instances">
<td>{{instance.provider.metadata.algorithmType}}</td>
<td>{{instance.name}}</td>
<td><a href="#/realms/{{realm.realm}}/keys/providers/{{instance.providerId}}/{{instance.id}}">{{instance.id}}</a></td>
<td>{{instance.providerId}}</td>

View file

@ -13,8 +13,7 @@
<th>{{:: 'type' | translate}}</th>
<th>{{:: 'kid' | translate}}</th>
<th>{{:: 'provider' | translate}}</th>
<th>{{:: 'publicKey' | translate}}</th>
<th>{{:: 'certificate' | translate}}</th>
<th colspan="2">{{:: 'publicKeys' | translate}}</th>
</tr>
</thead>
<tbody>
@ -23,11 +22,10 @@
<td>{{key.kid}}</td>
<td><a href="#/realms/{{realm.realm}}/keys/providers/{{key.provider.providerId}}/{{key.provider.id}}">{{key.provider.name}}</a></td>
<td data-ng-show="key.publicKey" class="kc-action-cell" data-ng-click="viewKey(key.publicKey)">{{:: 'view' | translate}}</td>
<td data-ng-hide="key.publicKey"></td>
<td data-ng-show="key.type === 'RSA'" class="kc-action-cell" data-ng-click="viewKey(key.publicKey)">{{:: 'publicKey' | translate}}</td>
<td data-ng-show="key.type === 'RSA'" class="kc-action-cell" data-ng-click="viewKey(key.certificate)">{{:: 'certificate' | translate}}</td>
<td data-ng-show="key.certificate" class="kc-action-cell" data-ng-click="viewKey(key.certificate)">{{:: 'view' | translate}}</td>
<td data-ng-hide="key.certificate"></td>
<td data-ng-show="key.type !== 'RSA'" colspan="2"></td>
</tr>
</tbody>
</table>