KEYCLOAK-6768 Signed and Encrypted ID Token Support

This commit is contained in:
Takashi Norimatsu 2018-12-05 15:31:48 +09:00 committed by Marek Posolda
parent c158105e2e
commit 8225157a1c
67 changed files with 1851 additions and 130 deletions

View file

@ -163,7 +163,7 @@ public class KcinitDriver {
JWE jwe = new JWE();
final SecretKey aesSecret = new SecretKeySpec(aesKey, "AES");
jwe.getKeyStorage()
.setEncryptionKey(aesSecret);
.setDecryptionKey(aesSecret);
return jwe;
}

View file

@ -46,7 +46,7 @@ public class AsymmetricSignatureSignerContext implements SignatureSignerContext
public byte[] sign(byte[] data) throws SignatureException {
try {
Signature signature = Signature.getInstance(JavaAlgorithm.getJavaAlgorithm(key.getAlgorithm()));
signature.initSign((PrivateKey) key.getSignKey());
signature.initSign((PrivateKey) key.getPrivateKey());
signature.update(data);
return signature.sign();
} catch (Exception e) {

View file

@ -43,7 +43,7 @@ public class AsymmetricSignatureVerifierContext implements SignatureVerifierCont
public boolean verify(byte[] data, byte[] signature) throws VerificationException {
try {
Signature verifier = Signature.getInstance(JavaAlgorithm.getJavaAlgorithm(key.getAlgorithm()));
verifier.initVerify((PublicKey) key.getVerifyKey());
verifier.initVerify((PublicKey) key.getPublicKey());
verifier.update(data);
return verifier.verify(signature);
} catch (Exception e) {

View file

@ -18,7 +18,17 @@ package org.keycloak.crypto;
public enum KeyUse {
SIG,
ENC
SIG("sig"),
ENC("enc");
private String specName;
KeyUse(String specName) {
this.specName = specName;
}
public String getSpecName() {
return specName;
}
}

View file

@ -30,8 +30,8 @@ public class KeyWrapper {
private KeyUse use;
private KeyStatus status;
private SecretKey secretKey;
private Key signKey;
private Key verifyKey;
private Key publicKey;
private Key privateKey;
private X509Certificate certificate;
public String getProviderId() {
@ -98,20 +98,20 @@ public class KeyWrapper {
this.secretKey = secretKey;
}
public Key getSignKey() {
return signKey;
public Key getPrivateKey() {
return privateKey;
}
public void setSignKey(Key signKey) {
this.signKey = signKey;
public void setPrivateKey(Key privateKey) {
this.privateKey = privateKey;
}
public Key getVerifyKey() {
return verifyKey;
public Key getPublicKey() {
return publicKey;
}
public void setVerifyKey(Key verifyKey) {
this.verifyKey = verifyKey;
public void setPublicKey(Key publicKey) {
this.publicKey = publicKey;
}
public X509Certificate getCertificate() {
@ -121,4 +121,5 @@ public class KeyWrapper {
public void setCertificate(X509Certificate certificate) {
this.certificate = certificate;
}
}

View file

@ -19,8 +19,6 @@ package org.keycloak.jose.jwe;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import org.keycloak.common.util.Base64;
@ -121,6 +119,15 @@ public class JWE {
public String encodeJwe() throws JWEException {
try {
if (header == null) throw new IllegalStateException("Header must be set");
return encodeJwe(JWERegistry.getAlgProvider(header.getAlgorithm()), JWERegistry.getEncProvider(header.getEncryptionAlgorithm()));
} catch (Exception e) {
throw new JWEException(e);
}
}
public String encodeJwe(JWEAlgorithmProvider algorithmProvider, JWEEncryptionProvider encryptionProvider) throws JWEException {
try {
if (header == null) {
throw new IllegalStateException("Header must be set");
@ -129,12 +136,10 @@ public class JWE {
throw new IllegalStateException("Content must be set");
}
JWEAlgorithmProvider algorithmProvider = JWERegistry.getAlgProvider(header.getAlgorithm());
if (algorithmProvider == null) {
throw new IllegalArgumentException("No provider for alg '" + header.getAlgorithm() + "'");
}
JWEEncryptionProvider encryptionProvider = JWERegistry.getEncProvider(header.getEncryptionAlgorithm());
if (encryptionProvider == null) {
throw new IllegalArgumentException("No provider for enc '" + header.getAlgorithm() + "'");
}
@ -153,7 +158,6 @@ public class JWE {
}
}
private String getEncodedJweString() {
StringBuilder builder = new StringBuilder();
builder.append(base64Header).append(".")
@ -165,39 +169,53 @@ public class JWE {
return builder.toString();
}
private void setupJWEHeader(String jweStr) throws IllegalStateException {
String[] parts = jweStr.split("\\.");
if (parts.length != 5) {
throw new IllegalStateException("Not a JWE String");
}
this.base64Header = parts[0];
this.base64Cek = parts[1];
this.initializationVector = Base64Url.decode(parts[2]);
this.encryptedContent = Base64Url.decode(parts[3]);
this.authenticationTag = Base64Url.decode(parts[4]);
this.header = getHeader();
}
private JWE getProcessedJWE(JWEAlgorithmProvider algorithmProvider, JWEEncryptionProvider encryptionProvider) throws Exception {
if (algorithmProvider == null) {
throw new IllegalArgumentException("No provider for alg ");
}
if (encryptionProvider == null) {
throw new IllegalArgumentException("No provider for enc ");
}
keyStorage.setEncryptionProvider(encryptionProvider);
byte[] decodedCek = algorithmProvider.decodeCek(Base64Url.decode(base64Cek), keyStorage.getDecryptionKey());
keyStorage.setCEKBytes(decodedCek);
encryptionProvider.verifyAndDecodeJwe(this);
return this;
}
public JWE verifyAndDecodeJwe(String jweStr) throws JWEException {
try {
String[] parts = jweStr.split("\\.");
if (parts.length != 5) {
throw new IllegalStateException("Not a JWE String");
}
setupJWEHeader(jweStr);
return getProcessedJWE(JWERegistry.getAlgProvider(header.getAlgorithm()), JWERegistry.getEncProvider(header.getEncryptionAlgorithm()));
} catch (Exception e) {
throw new JWEException(e);
}
}
this.base64Header = parts[0];
this.base64Cek = parts[1];
this.initializationVector = Base64Url.decode(parts[2]);
this.encryptedContent = Base64Url.decode(parts[3]);
this.authenticationTag = Base64Url.decode(parts[4]);
this.header = getHeader();
JWEAlgorithmProvider algorithmProvider = JWERegistry.getAlgProvider(header.getAlgorithm());
if (algorithmProvider == null) {
throw new IllegalArgumentException("No provider for alg '" + header.getAlgorithm() + "'");
}
JWEEncryptionProvider encryptionProvider = JWERegistry.getEncProvider(header.getEncryptionAlgorithm());
if (encryptionProvider == null) {
throw new IllegalArgumentException("No provider for enc '" + header.getAlgorithm() + "'");
}
keyStorage.setEncryptionProvider(encryptionProvider);
byte[] decodedCek = algorithmProvider.decodeCek(Base64Url.decode(base64Cek), keyStorage.getEncryptionKey());
keyStorage.setCEKBytes(decodedCek);
encryptionProvider.verifyAndDecodeJwe(this);
return this;
public JWE verifyAndDecodeJwe(String jweStr, JWEAlgorithmProvider algorithmProvider, JWEEncryptionProvider encryptionProvider) throws JWEException {
try {
setupJWEHeader(jweStr);
return getProcessedJWE(algorithmProvider, encryptionProvider);
} catch (Exception e) {
throw new JWEException(e);
}
@ -247,7 +265,7 @@ public class JWE {
JWE jwe = new JWE();
jwe.getKeyStorage()
.setEncryptionKey(aesKey);
.setDecryptionKey(aesKey);
jwe.verifyAndDecodeJwe(encodedJwe);
return jwe.getContent();

View file

@ -24,9 +24,13 @@ public class JWEConstants {
public static final String DIR = "dir";
public static final String A128KW = "A128KW";
public static final String RSA1_5 = "RSA1_5";
public static final String RSA_OAEP = "RSA-OAEP";
public static final String A128CBC_HS256 = "A128CBC-HS256";
public static final String A192CBC_HS384 = "A192CBC-HS384";
public static final String A256CBC_HS512 = "A256CBC-HS512";
public static final String A128GCM = "A128GCM";
public static final String A192GCM = "A192GCM";
public static final String A256GCM = "A256GCM";
}

View file

@ -59,6 +59,13 @@ public class JWEHeader implements Serializable {
this.compressionAlgorithm = compressionAlgorithm;
}
public JWEHeader(String algorithm, String encryptionAlgorithm, String compressionAlgorithm, String keyId) {
this.algorithm = algorithm;
this.encryptionAlgorithm = encryptionAlgorithm;
this.compressionAlgorithm = compressionAlgorithm;
this.keyId = keyId;
}
public String getAlgorithm() {
return algorithm;
}

View file

@ -29,6 +29,7 @@ import org.keycloak.jose.jwe.enc.JWEEncryptionProvider;
public class JWEKeyStorage {
private Key encryptionKey;
private Key decryptionKey;
private byte[] cekBytes;
@ -46,6 +47,14 @@ public class JWEKeyStorage {
return this;
}
public Key getDecryptionKey() {
return decryptionKey;
}
public JWEKeyStorage setDecryptionKey(Key decryptionKey) {
this.decryptionKey = decryptionKey;
return this;
}
public void setCEKBytes(byte[] cekBytes) {
this.cekBytes = cekBytes;
@ -100,4 +109,5 @@ public class JWEKeyStorage {
ENCRYPTION,
SIGNATURE
}
}

View file

@ -0,0 +1,46 @@
/*
* Copyright 2018 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.jwe.alg;
import java.security.Key;
import javax.crypto.Cipher;
import org.keycloak.jose.jwe.JWEKeyStorage;
import org.keycloak.jose.jwe.enc.JWEEncryptionProvider;
public abstract class KeyEncryptionJWEAlgorithmProvider implements JWEAlgorithmProvider {
@Override
public byte[] decodeCek(byte[] encodedCek, Key privateKey) throws Exception {
Cipher cipher = getCipherProvider();
cipher.init(Cipher.DECRYPT_MODE, privateKey);
return cipher.doFinal(encodedCek);
}
@Override
public byte[] encodeCek(JWEEncryptionProvider encryptionProvider, JWEKeyStorage keyStorage, Key publicKey) throws Exception {
Cipher cipher = getCipherProvider();
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] cekBytes = keyStorage.getCekBytes();
return cipher.doFinal(cekBytes);
}
protected abstract Cipher getCipherProvider() throws Exception;
}

View file

@ -0,0 +1,35 @@
/*
* Copyright 2018 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.jwe.alg;
import javax.crypto.Cipher;
public class RsaKeyEncryptionJWEAlgorithmProvider extends KeyEncryptionJWEAlgorithmProvider {
private final String jcaAlgorithmName;
public RsaKeyEncryptionJWEAlgorithmProvider(String jcaAlgorithmName) {
this.jcaAlgorithmName = jcaAlgorithmName;
}
@Override
protected Cipher getCipherProvider() throws Exception {
return Cipher.getInstance(jcaAlgorithmName);
}
}

View file

@ -0,0 +1,64 @@
/*
* Copyright 2018 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.jwe.enc;
import org.keycloak.jose.jwe.JWEConstants;
public class AesCbcHmacShaJWEEncryptionProvider extends AesCbcHmacShaEncryptionProvider {
private final int expectedCEKLength;
private final int expectedAesKeyLength;
private final String hmacShaAlgorithm;
private final int authenticationTagLength;
public AesCbcHmacShaJWEEncryptionProvider(String jwaAlgorithmName) {
if (JWEConstants.A128CBC_HS256.equals(jwaAlgorithmName)) {
expectedCEKLength = 32;
expectedAesKeyLength = 16;
hmacShaAlgorithm = "HMACSHA256";
authenticationTagLength = 16;
} else {
expectedCEKLength = 0;
expectedAesKeyLength = 0;
hmacShaAlgorithm = null;
authenticationTagLength = 0;
}
}
@Override
public int getExpectedCEKLength() {
return expectedCEKLength;
}
@Override
protected int getExpectedAesKeyLength() {
return expectedAesKeyLength;
}
@Override
protected String getHmacShaAlgorithm() {
return hmacShaAlgorithm;
}
@Override
protected int getAuthenticationTagLength() {
return authenticationTagLength;
}
}

View file

@ -0,0 +1,156 @@
/*
* Copyright 2018 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.jwe.enc;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.keycloak.jose.jwe.JWE;
import org.keycloak.jose.jwe.JWEKeyStorage;
import org.keycloak.jose.jwe.JWEUtils;
public abstract class AesGcmEncryptionProvider implements JWEEncryptionProvider {
// 96 bits of IV is required
// Authentication Tag size must be 128 bits
// https://tools.ietf.org/html/rfc7518#section-5.3
private static final int AUTH_TAG_SIZE_BYTE = 16;
private static final int IV_SIZE_BYTE = 12;
@Override
public void encodeJwe(JWE jwe) throws Exception {
byte[] contentBytes = jwe.getContent();
// IV must be nonce (number used once)
byte[] initializationVector = JWEUtils.generateSecret(IV_SIZE_BYTE);
Key aesKey = jwe.getKeyStorage().getCEKKey(JWEKeyStorage.KeyUse.ENCRYPTION, false);
if (aesKey == null) {
throw new IllegalArgumentException("AES CEK key not present");
}
int expectedAesKeyLength = getExpectedAesKeyLength();
if (expectedAesKeyLength != aesKey.getEncoded().length) {
throw new IllegalStateException("Length of aes key should be " + expectedAesKeyLength +", but was " + aesKey.getEncoded().length);
}
// https://tools.ietf.org/html/rfc7516#appendix-A.1.5
byte[] aad = jwe.getBase64Header().getBytes("UTF-8");
byte[] cipherBytes = encryptBytes(contentBytes, initializationVector, aesKey, aad);
byte[] authenticationTag = getAuthenticationTag(cipherBytes);
byte[] encryptedContent = getEncryptedContent(cipherBytes);
jwe.setEncryptedContentInfo(initializationVector, encryptedContent, authenticationTag);
}
@Override
public void verifyAndDecodeJwe(JWE jwe) throws Exception {
Key aesKey = jwe.getKeyStorage().getCEKKey(JWEKeyStorage.KeyUse.ENCRYPTION, false);
if (aesKey == null) {
throw new IllegalArgumentException("AES CEK key not present");
}
int expectedAesKeyLength = getExpectedAesKeyLength();
if (expectedAesKeyLength != aesKey.getEncoded().length) {
throw new IllegalStateException("Length of aes key should be " + expectedAesKeyLength +", but was " + aesKey.getEncoded().length);
}
// https://tools.ietf.org/html/rfc7516#appendix-A.1.5
byte[] aad = jwe.getBase64Header().getBytes("UTF-8");
byte[] decryptedTargetContent = getAeadDecryptedTargetContent(jwe);
byte[] contentBytes = decryptBytes(decryptedTargetContent, jwe.getInitializationVector(), aesKey, aad);
jwe.content(contentBytes);
}
private byte[] encryptBytes(byte[] contentBytes, byte[] ivBytes, Key aesKey, byte[] aad) throws GeneralSecurityException {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding", "BC");
GCMParameterSpec gcmParams = new GCMParameterSpec(AUTH_TAG_SIZE_BYTE * 8, ivBytes);
cipher.init(Cipher.ENCRYPT_MODE, aesKey, gcmParams);
cipher.updateAAD(aad);
byte[] cipherText = new byte[cipher.getOutputSize(contentBytes.length)];
cipher.doFinal(contentBytes, 0, contentBytes.length, cipherText);
return cipherText;
}
private byte[] decryptBytes(byte[] encryptedBytes, byte[] ivBytes, Key aesKey, byte[] aad) throws GeneralSecurityException {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding", "BC");
GCMParameterSpec gcmParams = new GCMParameterSpec(AUTH_TAG_SIZE_BYTE * 8, ivBytes);
cipher.init(Cipher.DECRYPT_MODE, aesKey, gcmParams);
cipher.updateAAD(aad);
return cipher.doFinal(encryptedBytes);
}
private byte[] getAuthenticationTag(byte[] cipherBytes) {
// AES GCM cipher text consists of a cipher text an authentication tag.
// The authentication tag be encoded as an individual term in JWE.
// So extract it from the AES GCM cipher text.
// https://tools.ietf.org/html/rfc5116#section-5.1
byte[] authenticationTag = new byte[AUTH_TAG_SIZE_BYTE];
System.arraycopy(cipherBytes, cipherBytes.length - authenticationTag.length, authenticationTag, 0, authenticationTag.length);
return authenticationTag;
}
private byte[] getEncryptedContent(byte[] cipherBytes) throws NoSuchAlgorithmException, InvalidKeyException {
// AES GCM cipher text consists of a cipher text an authentication tag.
// The cipher text be encoded as an individual term in JWE.
// So extract it from the AES GCM cipher text.
// https://tools.ietf.org/html/rfc5116#section-5.1
byte[] encryptedContent = new byte[cipherBytes.length - AUTH_TAG_SIZE_BYTE];
System.arraycopy(cipherBytes, 0, encryptedContent, 0, encryptedContent.length);
return encryptedContent;
}
private byte[] getAeadDecryptedTargetContent(JWE jwe) {
// In order to decrypt, need to construct AES GCM cipher text from JWE cipher text and authentication tag
byte[] encryptedContent = jwe.getEncryptedContent();
byte[] authTag = jwe.getAuthenticationTag();
byte[] decryptedTargetContent = new byte[authTag.length + encryptedContent.length];
System.arraycopy(encryptedContent, 0, decryptedTargetContent, 0, encryptedContent.length);
System.arraycopy(authTag, 0, decryptedTargetContent, encryptedContent.length, authTag.length);
return decryptedTargetContent;
}
@Override
public byte[] serializeCEK(JWEKeyStorage keyStorage) {
Key aesKey = keyStorage.getCEKKey(JWEKeyStorage.KeyUse.ENCRYPTION, false);
if (aesKey == null) {
throw new IllegalArgumentException("AES CEK key not present");
}
byte[] aesBytes = aesKey.getEncoded();
return aesBytes;
}
@Override
public void deserializeCEK(JWEKeyStorage keyStorage) {
byte[] cekBytes = keyStorage.getCekBytes();
SecretKeySpec aesKey = new SecretKeySpec(cekBytes, "AES");
keyStorage.setCEKKey(aesKey, JWEKeyStorage.KeyUse.ENCRYPTION);
}
protected abstract int getExpectedAesKeyLength();
}

View file

@ -0,0 +1,48 @@
/*
* Copyright 2018 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.jwe.enc;
import org.keycloak.jose.jwe.JWEConstants;
public class AesGcmJWEEncryptionProvider extends AesGcmEncryptionProvider {
private final int expectedAesKeyLength;
private final int expectedCEKLength;
public AesGcmJWEEncryptionProvider(String jwaAlgorithmName) {
if (JWEConstants.A128GCM.equals(jwaAlgorithmName)) {
expectedAesKeyLength = 16;
expectedCEKLength = 16;
} else {
expectedAesKeyLength = 0;
expectedCEKLength = 0;
}
}
@Override
protected int getExpectedAesKeyLength() {
return expectedAesKeyLength;
}
@Override
public int getExpectedCEKLength() {
return expectedCEKLength;
}
}

View file

@ -22,6 +22,7 @@ import org.keycloak.common.util.KeyUtils;
import org.keycloak.common.util.PemUtils;
import org.keycloak.crypto.Algorithm;
import org.keycloak.crypto.KeyType;
import org.keycloak.crypto.KeyUse;
import java.math.BigInteger;
import java.security.Key;
@ -64,7 +65,7 @@ public class JWKBuilder {
}
public JWK rsa(Key key) {
return rsa(key, null);
return rsa(key, (X509Certificate)null);
}
public JWK rsa(Key key, X509Certificate certificate) {
@ -87,6 +88,14 @@ public class JWKBuilder {
return k;
}
public JWK rsa(Key key, KeyUse keyUse) {
JWK k = rsa(key);
String keyUseString = keyUse == null ? DEFAULT_PUBLIC_KEY_USE : keyUse.getSpecName();
if (KeyUse.ENC == keyUse) keyUseString = "enc";
k.setPublicKeyUse(keyUseString);
return k;
}
public JWK ec(Key key) {
ECPublicKey ecKey = (ECPublicKey) key;

View file

@ -55,7 +55,7 @@ public class JWKSUtils {
keyWrapper.setAlgorithm(jwk.getAlgorithm());
keyWrapper.setType(jwk.getKeyType());
keyWrapper.setUse(getKeyUse(jwk.getPublicKeyUse()));
keyWrapper.setVerifyKey(parser.toPublicKey());
keyWrapper.setPublicKey(parser.toPublicKey());
result.put(keyWrapper.getKid(), keyWrapper);
}
}

View file

@ -23,6 +23,8 @@ import org.keycloak.jose.jwe.JWEConstants;
import org.keycloak.jose.jwe.JWEException;
import org.keycloak.jose.jwe.JWEHeader;
import org.keycloak.jose.jwe.JWEKeyStorage;
import org.keycloak.jose.jwe.alg.JWEAlgorithmProvider;
import org.keycloak.jose.jwe.enc.JWEEncryptionProvider;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.JWSInputException;
import org.keycloak.representations.JsonWebToken;
@ -174,4 +176,34 @@ public class TokenUtil {
}
}
public static String jweKeyEncryptionEncode(Key encryptionKEK, byte[] contentBytes, String algAlgorithm, String encAlgorithm, String kid, JWEAlgorithmProvider jweAlgorithmProvider, JWEEncryptionProvider jweEncryptionProvider) throws JWEException {
JWEHeader jweHeader = new JWEHeader(algAlgorithm, encAlgorithm, null, kid);
return jweKeyEncryptionEncode(encryptionKEK, contentBytes, jweHeader, jweAlgorithmProvider, jweEncryptionProvider);
}
private static String jweKeyEncryptionEncode(Key encryptionKEK, byte[] contentBytes, JWEHeader jweHeader, JWEAlgorithmProvider jweAlgorithmProvider, JWEEncryptionProvider jweEncryptionProvider) throws JWEException {
JWE jwe = new JWE()
.header(jweHeader)
.content(contentBytes);
jwe.getKeyStorage()
.setEncryptionKey(encryptionKEK);
String encodedContent = jwe.encodeJwe(jweAlgorithmProvider, jweEncryptionProvider);
return encodedContent;
}
public static byte[] jweKeyEncryptionVerifyAndDecode(Key decryptionKEK, String encodedContent) throws JWEException {
JWE jwe = new JWE();
jwe.getKeyStorage()
.setDecryptionKey(decryptionKEK);
jwe.verifyAndDecodeJwe(encodedContent);
return jwe.getContent();
}
public static byte[] jweKeyEncryptionVerifyAndDecode(Key decryptionKEK, String encodedContent, JWEAlgorithmProvider algorithmProvider, JWEEncryptionProvider encryptionProvider) throws JWEException {
JWE jwe = new JWE();
jwe.getKeyStorage()
.setDecryptionKey(decryptionKEK);
jwe.verifyAndDecodeJwe(encodedContent, algorithmProvider, encryptionProvider);
return jwe.getContent();
}
}

View file

@ -19,18 +19,26 @@ package org.keycloak.jose;
import java.io.UnsupportedEncodingException;
import java.security.Key;
import java.security.spec.KeySpec;
import java.security.KeyPair;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import org.junit.Assert;
import org.junit.Test;
import org.keycloak.common.util.Base64;
import org.keycloak.common.util.Base64Url;
import org.keycloak.common.util.KeyUtils;
import org.keycloak.jose.jwe.*;
import org.keycloak.jose.jwe.alg.JWEAlgorithmProvider;
import org.keycloak.jose.jwe.alg.KeyEncryptionJWEAlgorithmProvider;
import org.keycloak.jose.jwe.alg.RsaKeyEncryptionJWEAlgorithmProvider;
import org.keycloak.jose.jwe.enc.AesCbcHmacShaEncryptionProvider;
import org.keycloak.jose.jwe.enc.AesCbcHmacShaJWEEncryptionProvider;
import org.keycloak.jose.jwe.enc.AesGcmEncryptionProvider;
import org.keycloak.jose.jwe.enc.AesGcmJWEEncryptionProvider;
import org.keycloak.jose.jwe.enc.JWEEncryptionProvider;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@ -151,7 +159,7 @@ public class JWETest {
jwe = new JWE();
jwe.getKeyStorage()
.setEncryptionKey(aesKey);
.setDecryptionKey(aesKey);
jwe.verifyAndDecodeJwe(encodedContent);
@ -223,7 +231,7 @@ public class JWETest {
JWE jwe = new JWE();
jwe.getKeyStorage()
.setEncryptionKey(aesKeySpec);
.setDecryptionKey(aesKeySpec);
jwe.verifyAndDecodeJwe(externalJwe);
@ -233,4 +241,102 @@ public class JWETest {
}
@Test
public void testRSA1_5_A128GCM() throws Exception {
testKeyEncryption_ContentEncryptionAesGcm(JWEConstants.RSA1_5, JWEConstants.A128GCM);
}
@Test
public void testRSAOAEP_A128GCM() throws Exception {
testKeyEncryption_ContentEncryptionAesGcm(JWEConstants.RSA_OAEP, JWEConstants.A128GCM);
}
@Test
public void testRSA1_5_A128CBCHS256() throws Exception {
testKeyEncryption_ContentEncryptionAesHmacSha(JWEConstants.RSA1_5, JWEConstants.A128CBC_HS256);
}
@Test
public void testRSAOAEP_A128CBCHS256() throws Exception {
testKeyEncryption_ContentEncryptionAesHmacSha(JWEConstants.RSA_OAEP, JWEConstants.A128CBC_HS256);
}
private void testKeyEncryption_ContentEncryptionAesGcm(String jweAlgorithmName, String jweEncryptionName) throws Exception {
// generate key pair for KEK
KeyPair keyPair = KeyUtils.generateRsaKeyPair(2048);
JWEAlgorithmProvider jweAlgorithmProvider = new RsaKeyEncryptionJWEAlgorithmProvider(getJcaAlgorithmName(jweAlgorithmName));
JWEEncryptionProvider jweEncryptionProvider = new AesGcmJWEEncryptionProvider(jweEncryptionName);
JWEHeader jweHeader = new JWEHeader(jweAlgorithmName, jweEncryptionName, null);
JWE jwe = new JWE()
.header(jweHeader)
.content(PAYLOAD.getBytes("UTF-8"));
jwe.getKeyStorage()
.setEncryptionKey(keyPair.getPublic());
String encodedContent = jwe.encodeJwe(jweAlgorithmProvider, jweEncryptionProvider);
System.out.println("Encoded content: " + encodedContent);
System.out.println("Encoded content length: " + encodedContent.length());
jwe = new JWE();
jwe.getKeyStorage()
.setDecryptionKey(keyPair.getPrivate());
jwe.verifyAndDecodeJwe(encodedContent, jweAlgorithmProvider, jweEncryptionProvider);
String decodedContent = new String(jwe.getContent(), "UTF-8");
System.out.println("Decoded content: " + decodedContent);
System.out.println("Decoded content length: " + decodedContent.length());
Assert.assertEquals(PAYLOAD, decodedContent);
}
private void testKeyEncryption_ContentEncryptionAesHmacSha(String jweAlgorithmName, String jweEncryptionName) throws Exception {
// generate key pair for KEK
KeyPair keyPair = KeyUtils.generateRsaKeyPair(2048);
// generate CEK
final SecretKey aesKey = new SecretKeySpec(AES_128_KEY, "AES");
final SecretKey hmacKey = new SecretKeySpec(HMAC_SHA256_KEY, "HMACSHA2");
JWEAlgorithmProvider jweAlgorithmProvider = new RsaKeyEncryptionJWEAlgorithmProvider(getJcaAlgorithmName(jweAlgorithmName));
JWEEncryptionProvider jweEncryptionProvider = new AesCbcHmacShaJWEEncryptionProvider(jweEncryptionName);
JWEHeader jweHeader = new JWEHeader(jweAlgorithmName, jweEncryptionName, null);
JWE jwe = new JWE()
.header(jweHeader)
.content(PAYLOAD.getBytes("UTF-8"));
jwe.getKeyStorage()
.setEncryptionKey(keyPair.getPublic());
jwe.getKeyStorage()
.setCEKKey(aesKey, JWEKeyStorage.KeyUse.ENCRYPTION)
.setCEKKey(hmacKey, JWEKeyStorage.KeyUse.SIGNATURE);
String encodedContent = jwe.encodeJwe(jweAlgorithmProvider, jweEncryptionProvider);
System.out.println("Encoded content: " + encodedContent);
System.out.println("Encoded content length: " + encodedContent.length());
jwe = new JWE();
jwe.getKeyStorage()
.setDecryptionKey(keyPair.getPrivate());
jwe.getKeyStorage()
.setCEKKey(aesKey, JWEKeyStorage.KeyUse.ENCRYPTION)
.setCEKKey(hmacKey, JWEKeyStorage.KeyUse.SIGNATURE);
jwe.verifyAndDecodeJwe(encodedContent, jweAlgorithmProvider, jweEncryptionProvider);
String decodedContent = new String(jwe.getContent(), "UTF-8");
System.out.println("Decoded content: " + decodedContent);
System.out.println("Decoded content length: " + decodedContent.length());
Assert.assertEquals(PAYLOAD, decodedContent);
}
private String getJcaAlgorithmName(String jweAlgorithmName) {
String jcaAlgorithmName = null;
if (JWEConstants.RSA1_5.equals(jweAlgorithmName)) {
jcaAlgorithmName = "RSA/ECB/PKCS1Padding";
} else if (JWEConstants.RSA_OAEP.equals(jweAlgorithmName)) {
jcaAlgorithmName = "RSA/ECB/OAEPWithSHA-1AndMGF1Padding";
}
return jcaAlgorithmName;
}
}

View file

@ -128,10 +128,19 @@ public class InfinispanPublicKeyStorageProvider implements PublicKeyStorageProvi
@Override
public KeyWrapper getPublicKey(String modelKey, String kid, PublicKeyLoader loader) {
return getPublicKey(modelKey, kid, null, loader);
}
@Override
public KeyWrapper getFirstPublicKey(String modelKey, String algorithm, PublicKeyLoader loader) {
return getPublicKey(modelKey, null, algorithm, loader);
}
private KeyWrapper getPublicKey(String modelKey, String kid, String algorithm, PublicKeyLoader loader) {
// Check if key is in cache
PublicKeysEntry entry = keys.get(modelKey);
if (entry != null) {
KeyWrapper publicKey = getPublicKey(entry.getCurrentKeys(), kid);
KeyWrapper publicKey = algorithm != null ? getPublicKeyByAlg(entry.getCurrentKeys(), algorithm) : getPublicKey(entry.getCurrentKeys(), kid);
if (publicKey != null) {
return publicKey;
}
@ -157,7 +166,7 @@ public class InfinispanPublicKeyStorageProvider implements PublicKeyStorageProvi
entry = task.get();
// Computation finished. Let's see if key is available
KeyWrapper publicKey = getPublicKey(entry.getCurrentKeys(), kid);
KeyWrapper publicKey = algorithm != null ? getPublicKeyByAlg(entry.getCurrentKeys(), algorithm) : getPublicKey(entry.getCurrentKeys(), kid);
if (publicKey != null) {
return publicKey;
}
@ -191,13 +200,18 @@ public class InfinispanPublicKeyStorageProvider implements PublicKeyStorageProvi
}
}
private KeyWrapper getPublicKeyByAlg(Map<String, KeyWrapper> publicKeys, String algorithm) {
if (algorithm == null) return null;
for(KeyWrapper keyWrapper : publicKeys.values())
if (algorithm.equals(keyWrapper.getAlgorithm())) return keyWrapper;
return null;
}
@Override
public void close() {
}
private class WrapperCallable implements Callable<PublicKeysEntry> {
private final String modelKey;

View file

@ -17,6 +17,7 @@
package org.keycloak.keys.infinispan;
import java.util.ArrayList;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.FutureTask;
@ -27,6 +28,7 @@ import org.keycloak.Config;
import org.keycloak.cluster.ClusterEvent;
import org.keycloak.cluster.ClusterProvider;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.jose.jwk.JWK;
import org.keycloak.keys.PublicKeyStorageProvider;
import org.keycloak.keys.PublicKeyStorageProviderFactory;
import org.keycloak.keys.PublicKeyStorageUtils;
@ -105,7 +107,7 @@ public class InfinispanPublicKeyStorageProviderFactory implements PublicKeyStora
if (cacheKey != null) {
log.debugf("Invalidating %s from keysCache", cacheKey);
InfinispanPublicKeyStorageProvider provider = (InfinispanPublicKeyStorageProvider) cacheKey.session.getProvider(PublicKeyStorageProvider.class, getId());
provider.addInvalidation(cacheKey.cacheKey);
for (String ck : cacheKey.cacheKeys) provider.addInvalidation(ck);
}
}
@ -113,22 +115,32 @@ public class InfinispanPublicKeyStorageProviderFactory implements PublicKeyStora
}
private SessionAndKeyHolder getCacheKeyToInvalidate(ProviderEvent event) {
ArrayList<String> cacheKeys = new ArrayList<>();
String cacheKey = null;
if (event instanceof RealmModel.ClientUpdatedEvent) {
RealmModel.ClientUpdatedEvent eventt = (RealmModel.ClientUpdatedEvent) event;
String cacheKey = PublicKeyStorageUtils.getClientModelCacheKey(eventt.getUpdatedClient().getRealm().getId(), eventt.getUpdatedClient().getId());
return new SessionAndKeyHolder(eventt.getKeycloakSession(), cacheKey);
cacheKey = PublicKeyStorageUtils.getClientModelCacheKey(eventt.getUpdatedClient().getRealm().getId(), eventt.getUpdatedClient().getId(), JWK.Use.SIG);
cacheKeys.add(cacheKey);
cacheKey = PublicKeyStorageUtils.getClientModelCacheKey(eventt.getUpdatedClient().getRealm().getId(), eventt.getUpdatedClient().getId(), JWK.Use.ENCRYPTION);
cacheKeys.add(cacheKey);
return new SessionAndKeyHolder(eventt.getKeycloakSession(), cacheKeys);
} else if (event instanceof RealmModel.ClientRemovedEvent) {
RealmModel.ClientRemovedEvent eventt = (RealmModel.ClientRemovedEvent) event;
String cacheKey = PublicKeyStorageUtils.getClientModelCacheKey(eventt.getClient().getRealm().getId(), eventt.getClient().getId());
return new SessionAndKeyHolder(eventt.getKeycloakSession(), cacheKey);
cacheKey = PublicKeyStorageUtils.getClientModelCacheKey(eventt.getClient().getRealm().getId(), eventt.getClient().getId(), JWK.Use.SIG);
cacheKeys.add(cacheKey);
cacheKey = PublicKeyStorageUtils.getClientModelCacheKey(eventt.getClient().getRealm().getId(), eventt.getClient().getId(), JWK.Use.ENCRYPTION);
cacheKeys.add(cacheKey);
return new SessionAndKeyHolder(eventt.getKeycloakSession(), cacheKeys);
} else if (event instanceof RealmModel.IdentityProviderUpdatedEvent) {
RealmModel.IdentityProviderUpdatedEvent eventt = (RealmModel.IdentityProviderUpdatedEvent) event;
String cacheKey = PublicKeyStorageUtils.getIdpModelCacheKey(eventt.getRealm().getId(), eventt.getUpdatedIdentityProvider().getInternalId());
return new SessionAndKeyHolder(eventt.getKeycloakSession(), cacheKey);
cacheKey = PublicKeyStorageUtils.getIdpModelCacheKey(eventt.getRealm().getId(), eventt.getUpdatedIdentityProvider().getInternalId());
cacheKeys.add(cacheKey);
return new SessionAndKeyHolder(eventt.getKeycloakSession(), cacheKeys);
} else if (event instanceof RealmModel.IdentityProviderRemovedEvent) {
RealmModel.IdentityProviderRemovedEvent eventt = (RealmModel.IdentityProviderRemovedEvent) event;
String cacheKey = PublicKeyStorageUtils.getIdpModelCacheKey(eventt.getRealm().getId(), eventt.getRemovedIdentityProvider().getInternalId());
return new SessionAndKeyHolder(eventt.getKeycloakSession(), cacheKey);
cacheKey = PublicKeyStorageUtils.getIdpModelCacheKey(eventt.getRealm().getId(), eventt.getRemovedIdentityProvider().getInternalId());
cacheKeys.add(cacheKey);
return new SessionAndKeyHolder(eventt.getKeycloakSession(), cacheKeys);
} else {
return null;
}
@ -136,13 +148,12 @@ public class InfinispanPublicKeyStorageProviderFactory implements PublicKeyStora
private class SessionAndKeyHolder {
private final KeycloakSession session;
private final String cacheKey;
private final ArrayList<String> cacheKeys;
public SessionAndKeyHolder(KeycloakSession session, String cacheKey) {
public SessionAndKeyHolder(KeycloakSession session, ArrayList<String> cacheKeys) {
this.session = session;
this.cacheKey = cacheKey;
this.cacheKeys = cacheKeys;
}
}
@Override

View file

@ -0,0 +1,31 @@
/*
* Copyright 2018 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.crypto;
import org.keycloak.jose.jwe.alg.JWEAlgorithmProvider;
import org.keycloak.provider.Provider;
public interface CekManagementProvider extends Provider {
JWEAlgorithmProvider jweAlgorithmProvider();
@Override
default void close() {
}
}

View file

@ -0,0 +1,38 @@
/*
* Copyright 2018 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.crypto;
import org.keycloak.Config;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.ProviderFactory;
public interface CekManagementProviderFactory extends ProviderFactory<CekManagementProvider> {
@Override
default void init(Config.Scope config) {
}
@Override
default void postInit(KeycloakSessionFactory factory) {
}
@Override
default void close() {
}
}

View file

@ -0,0 +1,46 @@
/*
* Copyright 2018 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.crypto;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.Spi;
public class CekManagementSpi implements Spi{
@Override
public boolean isInternal() {
return true;
}
@Override
public String getName() {
return "cekmanagement";
}
@Override
public Class<? extends Provider> getProviderClass() {
return CekManagementProvider.class;
}
@Override
public Class<? extends ProviderFactory> getProviderFactoryClass() {
return CekManagementProviderFactory.class;
}
}

View file

@ -0,0 +1,31 @@
/*
* Copyright 2018 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.crypto;
import org.keycloak.jose.jwe.enc.JWEEncryptionProvider;
import org.keycloak.provider.Provider;
public interface ContentEncryptionProvider extends Provider {
JWEEncryptionProvider jweEncryptionProvider();
@Override
default void close() {
}
}

View file

@ -0,0 +1,38 @@
/*
* Copyright 2018 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.crypto;
import org.keycloak.Config;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.ProviderFactory;
public interface ContentEncryptionProviderFactory extends ProviderFactory<ContentEncryptionProvider> {
@Override
default void init(Config.Scope config) {
}
@Override
default void postInit(KeycloakSessionFactory factory) {
}
@Override
default void close() {
}
}

View file

@ -0,0 +1,46 @@
/*
* Copyright 2018 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.crypto;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.Spi;
public class ContentEncryptionSpi implements Spi {
@Override
public boolean isInternal() {
return true;
}
@Override
public String getName() {
return "contentencryption";
}
@Override
public Class<? extends Provider> getProviderClass() {
return ContentEncryptionProvider.class;
}
@Override
public Class<? extends ProviderFactory> getProviderFactoryClass() {
return ContentEncryptionProviderFactory.class;
}
}

View file

@ -36,6 +36,17 @@ public interface PublicKeyStorageProvider extends Provider {
*/
KeyWrapper getPublicKey(String modelKey, String kid, PublicKeyLoader loader);
/**
* Get first found public key to verify messages signed by particular client having several public keys. Used for example during JWT client authentication
* or to encrypt content encryption key (CEK) by particular client. Used for example during encrypting a token in JWE
*
* @param modelKey
* @param algorithm
* @param loader
* @return
*/
KeyWrapper getFirstPublicKey(String modelKey, String algorithm, PublicKeyLoader loader);
/**
* Clears all the cached public keys, so they need to be loaded again
*/

View file

@ -17,17 +17,25 @@
package org.keycloak.keys;
import org.keycloak.jose.jwk.JWK;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class PublicKeyStorageUtils {
static final JWK.Use DEFAULT_KEYUSE = JWK.Use.SIG;
public static String getClientModelCacheKey(String realmId, String clientUuid) {
return realmId + "::client::" + clientUuid;
return getClientModelCacheKey(realmId, clientUuid, DEFAULT_KEYUSE);
}
public static String getIdpModelCacheKey(String realmId, String idpInternalId) {
return realmId + "::idp::" + idpInternalId;
}
public static String getClientModelCacheKey(String realmId, String clientUuid, JWK.Use keyUse) {
return realmId + "::client::" + clientUuid + "::keyuse::" + keyUse;
}
}

View file

@ -76,3 +76,5 @@ org.keycloak.crypto.SignatureSpi
org.keycloak.crypto.ClientSignatureVerifierSpi
org.keycloak.crypto.HashSpi
org.keycloak.vault.VaultSpi
org.keycloak.crypto.CekManagementSpi
org.keycloak.crypto.ContentEncryptionSpi

View file

@ -43,4 +43,7 @@ public interface TokenManager {
<T> T decodeClientJWT(String token, ClientModel client, Class<T> clazz);
String encodeAndEncrypt(Token token);
String cekManagementAlgorithm(TokenCategory category);
String encryptAlgorithm(TokenCategory category);
}

View file

@ -0,0 +1,36 @@
/*
* Copyright 2018 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.crypto;
import org.keycloak.jose.jwe.JWEConstants;
import org.keycloak.models.KeycloakSession;
public class Aes128CbcHmacSha256ContentEncryptionProviderFactory implements ContentEncryptionProviderFactory {
public static final String ID = JWEConstants.A128CBC_HS256;
@Override
public String getId() {
return ID;
}
@Override
public ContentEncryptionProvider create(KeycloakSession session) {
return new AesCbcHmacShaContentEncryptionProvider(session, ID);
}
}

View file

@ -0,0 +1,36 @@
/*
* Copyright 2018 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.crypto;
import org.keycloak.jose.jwe.JWEConstants;
import org.keycloak.models.KeycloakSession;
public class Aes128GcmContentEncryptionProviderFactory implements ContentEncryptionProviderFactory {
public static final String ID = JWEConstants.A128GCM;
@Override
public String getId() {
return ID;
}
@Override
public ContentEncryptionProvider create(KeycloakSession session) {
return new AesGcmContentEncryptionProvider(session, ID);
}
}

View file

@ -0,0 +1,39 @@
/*
* Copyright 2018 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.crypto;
import org.keycloak.jose.jwe.enc.AesCbcHmacShaJWEEncryptionProvider;
import org.keycloak.jose.jwe.enc.JWEEncryptionProvider;
import org.keycloak.models.KeycloakSession;
public class AesCbcHmacShaContentEncryptionProvider implements ContentEncryptionProvider {
private final KeycloakSession session;
private final String jweAlgorithmName;
public AesCbcHmacShaContentEncryptionProvider(KeycloakSession session, String jweAlgorithmName) {
this.session = session;
this.jweAlgorithmName = jweAlgorithmName;
}
@Override
public JWEEncryptionProvider jweEncryptionProvider() {
return new AesCbcHmacShaJWEEncryptionProvider(jweAlgorithmName);
}
}

View file

@ -0,0 +1,39 @@
/*
* Copyright 2018 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.crypto;
import org.keycloak.jose.jwe.enc.AesGcmJWEEncryptionProvider;
import org.keycloak.jose.jwe.enc.JWEEncryptionProvider;
import org.keycloak.models.KeycloakSession;
public class AesGcmContentEncryptionProvider implements ContentEncryptionProvider {
private final KeycloakSession session;
private final String jweAlgorithmName;
public AesGcmContentEncryptionProvider(KeycloakSession session, String jweAlgorithmName) {
this.session = session;
this.jweAlgorithmName = jweAlgorithmName;
}
@Override
public JWEEncryptionProvider jweEncryptionProvider() {
return new AesGcmJWEEncryptionProvider(jweAlgorithmName);
}
}

View file

@ -0,0 +1,46 @@
/*
* Copyright 2018 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.crypto;
import org.keycloak.jose.jwe.JWEConstants;
import org.keycloak.jose.jwe.alg.JWEAlgorithmProvider;
import org.keycloak.jose.jwe.alg.RsaKeyEncryptionJWEAlgorithmProvider;
import org.keycloak.models.KeycloakSession;
public class RsaCekManagementProvider implements CekManagementProvider {
private final KeycloakSession session;
private final String jweAlgorithmName;
public RsaCekManagementProvider(KeycloakSession session, String jweAlgorithmName) {
this.session = session;
this.jweAlgorithmName = jweAlgorithmName;
}
@Override
public JWEAlgorithmProvider jweAlgorithmProvider() {
String jcaAlgorithmName = null;
if (JWEConstants.RSA1_5.equals(jweAlgorithmName)) {
jcaAlgorithmName = "RSA/ECB/PKCS1Padding";
} else if (JWEConstants.RSA_OAEP.equals(jweAlgorithmName)) {
jcaAlgorithmName = "RSA/ECB/OAEPWithSHA-1AndMGF1Padding";
}
return new RsaKeyEncryptionJWEAlgorithmProvider(jcaAlgorithmName);
}
}

View file

@ -0,0 +1,37 @@
/*
* Copyright 2018 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.crypto;
import org.keycloak.jose.jwe.JWEConstants;
import org.keycloak.models.KeycloakSession;
public class RsaesOaepCekManagementProviderFactory implements CekManagementProviderFactory {
public static final String ID = JWEConstants.RSA_OAEP;
@Override
public String getId() {
return ID;
}
@Override
public CekManagementProvider create(KeycloakSession session) {
return new RsaCekManagementProvider(session, ID);
}
}

View file

@ -0,0 +1,37 @@
/*
* Copyright 2018 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.crypto;
import org.keycloak.jose.jwe.JWEConstants;
import org.keycloak.models.KeycloakSession;
public class RsaesPkcs1CekManagementProviderFactory implements CekManagementProviderFactory {
public static final String ID = JWEConstants.RSA1_5;
@Override
public String getId() {
return ID;
}
@Override
public CekManagementProvider create(KeycloakSession session) {
return new RsaCekManagementProvider(session, ID);
}
}

View file

@ -16,19 +16,34 @@
*/
package org.keycloak.jose.jws;
import java.io.UnsupportedEncodingException;
import java.security.Key;
import java.util.LinkedList;
import java.util.List;
import org.jboss.logging.Logger;
import org.keycloak.Token;
import org.keycloak.TokenCategory;
import org.keycloak.crypto.Algorithm;
import org.keycloak.crypto.CekManagementProvider;
import org.keycloak.crypto.ClientSignatureVerifierProvider;
import org.keycloak.crypto.ContentEncryptionProvider;
import org.keycloak.crypto.KeyUse;
import org.keycloak.crypto.KeyWrapper;
import org.keycloak.crypto.SignatureProvider;
import org.keycloak.crypto.SignatureSignerContext;
import org.keycloak.jose.jwe.JWEConstants;
import org.keycloak.jose.jwe.JWEException;
import org.keycloak.jose.jwe.alg.JWEAlgorithmProvider;
import org.keycloak.jose.jwe.enc.JWEEncryptionProvider;
import org.keycloak.jose.jwk.JWK;
import org.keycloak.keys.loader.PublicKeyStorageManager;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.TokenManager;
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
import org.keycloak.util.TokenUtil;
public class DefaultTokenManager implements TokenManager {
@ -142,4 +157,86 @@ public class DefaultTokenManager implements TokenManager {
return DEFAULT_ALGORITHM_NAME;
}
@Override
public String encodeAndEncrypt(Token token) {
String encodedToken = encode(token);
if (isTokenEncryptRequired(token.getCategory())) {
encodedToken = getEncryptedToken(token.getCategory(), encodedToken);
}
return encodedToken;
}
private boolean isTokenEncryptRequired(TokenCategory category) {
if (cekManagementAlgorithm(category) == null) return false;
if (encryptAlgorithm(category) == null) return false;
return true;
}
private String getEncryptedToken(TokenCategory category, String encodedToken) {
String encryptedToken = null;
String algAlgorithm = cekManagementAlgorithm(category);
String encAlgorithm = encryptAlgorithm(category);
CekManagementProvider cekManagementProvider = session.getProvider(CekManagementProvider.class, algAlgorithm);
JWEAlgorithmProvider jweAlgorithmProvider = cekManagementProvider.jweAlgorithmProvider();
ContentEncryptionProvider contentEncryptionProvider = session.getProvider(ContentEncryptionProvider.class, encAlgorithm);
JWEEncryptionProvider jweEncryptionProvider = contentEncryptionProvider.jweEncryptionProvider();
ClientModel client = session.getContext().getClient();
KeyWrapper keyWrapper = PublicKeyStorageManager.getClientPublicKeyWrapper(session, client, JWK.Use.ENCRYPTION, algAlgorithm);
if (keyWrapper == null) {
throw new RuntimeException("can not get encryption KEK");
}
Key encryptionKek = keyWrapper.getPublicKey();
String encryptionKekId = keyWrapper.getKid();
try {
encryptedToken = TokenUtil.jweKeyEncryptionEncode(encryptionKek, encodedToken.getBytes("UTF-8"), algAlgorithm, encAlgorithm, encryptionKekId, jweAlgorithmProvider, jweEncryptionProvider);
} catch (JWEException | UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
return encryptedToken;
}
@Override
public String cekManagementAlgorithm(TokenCategory category) {
if (category == null) return null;
switch (category) {
case ID:
return getCekManagementAlgorithm(OIDCConfigAttributes.ID_TOKEN_ENCRYPTED_RESPONSE_ALG);
default:
return null;
}
}
private String getCekManagementAlgorithm(String clientAttribute) {
ClientModel client = session.getContext().getClient();
String algorithm = client != null && clientAttribute != null ? client.getAttribute(clientAttribute) : null;
if (algorithm != null && !algorithm.equals("")) {
return algorithm;
}
return null;
}
@Override
public String encryptAlgorithm(TokenCategory category) {
if (category == null) return null;
switch (category) {
case ID:
return getEncryptAlgorithm(OIDCConfigAttributes.ID_TOKEN_ENCRYPTED_RESPONSE_ENC);
default:
return null;
}
}
private String getEncryptAlgorithm(String clientAttribute) {
ClientModel client = session.getContext().getClient();
String algorithm = client != null && clientAttribute != null ? client.getAttribute(clientAttribute) : null;
if (algorithm != null && !algorithm.equals("")) {
return algorithm;
}
return null;
}
}

View file

@ -66,8 +66,8 @@ public abstract class AbstractEcdsaKeyProvider implements KeyProvider {
key.setType(KeyType.EC);
key.setAlgorithm(AbstractEcdsaKeyProviderFactory.convertECDomainParmNistRepToAlgorithm(ecInNistRep));
key.setStatus(status);
key.setSignKey(keyPair.getPrivate());
key.setVerifyKey(keyPair.getPublic());
key.setPrivateKey(keyPair.getPrivate());
key.setPublicKey(keyPair.getPublic());
return key;
}

View file

@ -75,8 +75,8 @@ public abstract class AbstractRsaKeyProvider implements KeyProvider {
key.setType(KeyType.RSA);
key.setAlgorithm(algorithm);
key.setStatus(status);
key.setSignKey(keyPair.getPrivate());
key.setVerifyKey(keyPair.getPublic());
key.setPrivateKey(keyPair.getPrivate());
key.setPublicKey(keyPair.getPublic());
key.setCertificate(certificate);
return key;

View file

@ -148,7 +148,7 @@ public class DefaultKeyManager implements KeyManager {
@Deprecated
public ActiveRsaKey getActiveRsaKey(RealmModel realm) {
KeyWrapper key = getActiveKey(realm, KeyUse.SIG, Algorithm.RS256);
return new ActiveRsaKey(key.getKid(), (PrivateKey) key.getSignKey(), (PublicKey) key.getVerifyKey(), key.getCertificate());
return new ActiveRsaKey(key.getKid(), (PrivateKey) key.getPrivateKey(), (PublicKey) key.getPublicKey(), key.getCertificate());
}
@Override
@ -169,7 +169,7 @@ public class DefaultKeyManager implements KeyManager {
@Deprecated
public PublicKey getRsaPublicKey(RealmModel realm, String kid) {
KeyWrapper key = getKey(realm, kid, KeyUse.SIG, Algorithm.RS256);
return key != null ? (PublicKey) key.getVerifyKey() : null;
return key != null ? (PublicKey) key.getPublicKey() : null;
}
@Override
@ -200,7 +200,7 @@ public class DefaultKeyManager implements KeyManager {
for (KeyWrapper key : getKeys(realm, KeyUse.SIG, Algorithm.RS256)) {
RsaKeyMetadata m = new RsaKeyMetadata();
m.setCertificate(key.getCertificate());
m.setPublicKey((PublicKey) key.getVerifyKey());
m.setPublicKey((PublicKey) key.getPublicKey());
m.setKid(key.getKid());
m.setProviderId(key.getProviderId());
m.setProviderPriority(key.getProviderPriority());

View file

@ -52,12 +52,19 @@ public class ClientPublicKeyLoader implements PublicKeyLoader {
private final KeycloakSession session;
private final ClientModel client;
private final JWK.Use keyUse;
public ClientPublicKeyLoader(KeycloakSession session, ClientModel client) {
this.session = session;
this.client = client;
this.keyUse = JWK.Use.SIG;
}
public ClientPublicKeyLoader(KeycloakSession session, ClientModel client, JWK.Use keyUse) {
this.session = session;
this.client = client;
this.keyUse = keyUse;
}
@Override
public Map<String, KeyWrapper> loadKeys() throws Exception {
@ -66,8 +73,8 @@ public class ClientPublicKeyLoader implements PublicKeyLoader {
String jwksUrl = config.getJwksUrl();
jwksUrl = ResolveRelative.resolveRelativeUri(session.getContext().getUri().getRequestUri(), client.getRootUrl(), jwksUrl);
JSONWebKeySet jwks = JWKSHttpUtils.sendJwksRequest(session, jwksUrl);
return JWKSUtils.getKeyWrappersForUse(jwks, JWK.Use.SIG);
} else {
return JWKSUtils.getKeyWrappersForUse(jwks, keyUse);
} else if (keyUse == JWK.Use.SIG) {
try {
CertificateRepresentation certInfo = CertificateInfoHelper.getCertificateFromClient(client, JWTClientAuthenticator.ATTR_PREFIX);
KeyWrapper publicKey = getSignatureValidationKey(certInfo);
@ -76,7 +83,9 @@ public class ClientPublicKeyLoader implements PublicKeyLoader {
logger.warnf(me, "Unable to retrieve publicKey for verify signature of client '%s' . Error details: %s", client.getClientId(), me.getMessage());
return Collections.emptyMap();
}
} else {
logger.warnf("Unable to retrieve publicKey of client '%s' for the specified purpose other than verifying signature", client.getClientId());
return Collections.emptyMap();
}
}
@ -102,14 +111,14 @@ public class ClientPublicKeyLoader implements PublicKeyLoader {
// Check if we have kid in DB, generate otherwise
kid = certInfo.getKid() != null ? certInfo.getKid() : KeyUtils.createKeyId(clientCert.getPublicKey());
keyWrapper.setKid(kid);
keyWrapper.setVerifyKey(clientCert.getPublicKey());
keyWrapper.setPublicKey(clientCert.getPublicKey());
keyWrapper.setCertificate(clientCert);
} else {
PublicKey publicKey = KeycloakModelUtils.getPublicKey(encodedPublicKey);
// Check if we have kid in DB, generate otherwise
kid = certInfo.getKid() != null ? certInfo.getKid() : KeyUtils.createKeyId(publicKey);
keyWrapper.setKid(kid);
keyWrapper.setVerifyKey(publicKey);
keyWrapper.setPublicKey(publicKey);
}
return keyWrapper;
}

View file

@ -53,7 +53,7 @@ public class HardcodedPublicKeyLoader implements PublicKeyLoader {
keyWrapper.setType(KeyType.RSA);
keyWrapper.setAlgorithm(Algorithm.RS256);
keyWrapper.setUse(KeyUse.SIG);
keyWrapper.setVerifyKey(PemUtils.decodePublicKey(pem));
keyWrapper.setPublicKey(PemUtils.decodePublicKey(pem));
}
return keyWrapper;
}

View file

@ -84,7 +84,7 @@ public class OIDCIdentityProviderPublicKeyLoader implements PublicKeyLoader {
keyWrapper.setType(KeyType.RSA);
keyWrapper.setAlgorithm(Algorithm.RS256);
keyWrapper.setUse(KeyUse.SIG);
keyWrapper.setVerifyKey(publicKey);
keyWrapper.setPublicKey(publicKey);
} else {
logger.warnf("No public key saved on identityProvider %s", config.getAlias());
}

View file

@ -20,6 +20,7 @@ package org.keycloak.keys.loader;
import org.jboss.logging.Logger;
import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
import org.keycloak.crypto.KeyWrapper;
import org.keycloak.jose.jwk.JWK;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.keys.PublicKeyLoader;
import org.keycloak.keys.PublicKeyStorageProvider;
@ -41,7 +42,7 @@ public class PublicKeyStorageManager {
KeyWrapper keyWrapper = getClientPublicKeyWrapper(session, client, input);
PublicKey publicKey = null;
if (keyWrapper != null) {
publicKey = (PublicKey)keyWrapper.getVerifyKey();
publicKey = (PublicKey)keyWrapper.getPublicKey();
}
return publicKey;
}
@ -54,6 +55,13 @@ public class PublicKeyStorageManager {
return keyStorage.getPublicKey(modelKey, kid, loader);
}
public static KeyWrapper getClientPublicKeyWrapper(KeycloakSession session, ClientModel client, JWK.Use keyUse, String algAlgorithm) {
PublicKeyStorageProvider keyStorage = session.getProvider(PublicKeyStorageProvider.class);
String modelKey = PublicKeyStorageUtils.getClientModelCacheKey(client.getRealm().getId(), client.getId(), keyUse);
ClientPublicKeyLoader loader = new ClientPublicKeyLoader(session, client, keyUse);
return keyStorage.getFirstPublicKey(modelKey, algAlgorithm, loader);
}
public static PublicKey getIdentityProviderPublicKey(KeycloakSession session, RealmModel realm, OIDCIdentityProviderConfig idpConfig, JWSInput input) {
boolean keyIdSetInConfiguration = idpConfig.getPublicKeySignatureVerifierKeyId() != null
&& ! idpConfig.getPublicKeySignatureVerifierKeyId().trim().isEmpty();
@ -80,6 +88,6 @@ public class PublicKeyStorageManager {
: kid, pem);
}
return (PublicKey)keyStorage.getPublicKey(modelKey, kid, loader).getVerifyKey();
return (PublicKey)keyStorage.getPublicKey(modelKey, kid, loader).getPublicKey();
}
}

View file

@ -133,6 +133,22 @@ public class OIDCAdvancedConfigWrapper {
setAttribute(OIDCConfigAttributes.ID_TOKEN_SIGNED_RESPONSE_ALG, algName);
}
public String getIdTokenEncryptedResponseAlg() {
return getAttribute(OIDCConfigAttributes.ID_TOKEN_ENCRYPTED_RESPONSE_ALG);
}
public void setIdTokenEncryptedResponseAlg(String algName) {
setAttribute(OIDCConfigAttributes.ID_TOKEN_ENCRYPTED_RESPONSE_ALG, algName);
}
public String getIdTokenEncryptedResponseEnc() {
return getAttribute(OIDCConfigAttributes.ID_TOKEN_ENCRYPTED_RESPONSE_ENC);
}
public void setIdTokenEncryptedResponseEnc(String encName) {
setAttribute(OIDCConfigAttributes.ID_TOKEN_ENCRYPTED_RESPONSE_ENC, encName);
}
private String getAttribute(String attrKey) {
if (clientModel != null) {
return clientModel.getAttribute(attrKey);

View file

@ -37,6 +37,10 @@ public final class OIDCConfigAttributes {
public static final String ID_TOKEN_SIGNED_RESPONSE_ALG = "id.token.signed.response.alg";
public static final String ID_TOKEN_ENCRYPTED_RESPONSE_ALG = "id.token.encrypted.response.alg";
public static final String ID_TOKEN_ENCRYPTED_RESPONSE_ENC = "id.token.encrypted.response.enc";
public static final String ACCESS_TOKEN_SIGNED_RESPONSE_ALG = "access.token.signed.response.alg";
public static final String ACCESS_TOKEN_LIFESPAN = "access.token.lifespan";

View file

@ -199,12 +199,12 @@ public class OIDCLoginProtocolService {
public Response certs() {
List<JWK> keys = new LinkedList<>();
for (KeyWrapper k : session.keys().getKeys(realm)) {
if (k.getStatus().isEnabled() && k.getUse().equals(KeyUse.SIG) && k.getVerifyKey() != null) {
if (k.getStatus().isEnabled() && k.getUse().equals(KeyUse.SIG) && k.getPublicKey() != null) {
JWKBuilder b = JWKBuilder.create().kid(k.getKid()).algorithm(k.getAlgorithm());
if (k.getType().equals(KeyType.RSA)) {
keys.add(b.rsa(k.getVerifyKey(), k.getCertificate()));
keys.add(b.rsa(k.getPublicKey(), k.getCertificate()));
} else if (k.getType().equals(KeyType.EC)) {
keys.add(b.ec(k.getVerifyKey()));
keys.add(b.ec(k.getPublicKey()));
}
}
}

View file

@ -20,7 +20,9 @@ package org.keycloak.protocol.oidc;
import org.keycloak.OAuth2Constants;
import org.keycloak.authentication.ClientAuthenticator;
import org.keycloak.authentication.ClientAuthenticatorFactory;
import org.keycloak.crypto.CekManagementProvider;
import org.keycloak.crypto.ClientSignatureVerifierProvider;
import org.keycloak.crypto.ContentEncryptionProvider;
import org.keycloak.crypto.SignatureProvider;
import org.keycloak.jose.jws.Algorithm;
import org.keycloak.models.ClientScopeModel;
@ -90,6 +92,8 @@ public class OIDCWellKnownProvider implements WellKnownProvider {
config.setRegistrationEndpoint(RealmsResource.clientRegistrationUrl(uriInfo).path(ClientRegistrationService.class, "provider").build(realm.getName(), OIDCClientRegistrationProviderFactory.ID).toString());
config.setIdTokenSigningAlgValuesSupported(getSupportedSigningAlgorithms(false));
config.setIdTokenEncryptionAlgValuesSupported(getSupportedIdTokenEncryptionAlg(false));
config.setIdTokenEncryptionEncValuesSupported(getSupportedIdTokenEncryptionEnc(false));
config.setUserInfoSigningAlgValuesSupported(getSupportedSigningAlgorithms(true));
config.setRequestObjectSigningAlgValuesSupported(getSupportedClientSigningAlgorithms(true));
config.setResponseTypesSupported(DEFAULT_RESPONSE_TYPES_SUPPORTED);
@ -172,4 +176,26 @@ public class OIDCWellKnownProvider implements WellKnownProvider {
}
return result;
}
private List<String> getSupportedIdTokenEncryptionAlg(boolean includeNone) {
List<String> result = new LinkedList<>();
for (ProviderFactory s : session.getKeycloakSessionFactory().getProviderFactories(CekManagementProvider.class)) {
result.add(s.getId());
}
if (includeNone) {
result.add("none");
}
return result;
}
private List<String> getSupportedIdTokenEncryptionEnc(boolean includeNone) {
List<String> result = new LinkedList<>();
for (ProviderFactory s : session.getKeycloakSessionFactory().getProviderFactories(ContentEncryptionProvider.class)) {
result.add(s.getId());
}
if (includeNone) {
result.add("none");
}
return result;
}
}

View file

@ -824,7 +824,7 @@ public class TokenManager {
idToken.setStateHash(stateHash);
}
if (idToken != null) {
String encodedToken = session.tokens().encode(idToken);
String encodedToken = session.tokens().encodeAndEncrypt(idToken);
res.setIdToken(encodedToken);
}
if (refreshToken != null) {

View file

@ -412,7 +412,16 @@ public class TokenEndpoint {
responseBuilder.generateIDToken();
}
AccessTokenResponse res = responseBuilder.build();
AccessTokenResponse res = null;
try {
res = responseBuilder.build();
} catch (RuntimeException re) {
if ("can not get encryption KEK".equals(re.getMessage())) {
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, "can not get encryption KEK", Response.Status.BAD_REQUEST);
} else {
throw re;
}
}
event.success();
@ -605,6 +614,7 @@ public class TokenEndpoint {
responseBuilder.generateIDToken();
}
// TODO : do the same as codeToToken()
AccessTokenResponse res = responseBuilder.build();
@ -678,6 +688,7 @@ public class TokenEndpoint {
responseBuilder.generateIDToken();
}
// TODO : do the same as codeToToken()
AccessTokenResponse res = responseBuilder.build();
event.success();

View file

@ -71,6 +71,12 @@ public class OIDCConfigurationRepresentation {
@JsonProperty("id_token_signing_alg_values_supported")
private List<String> idTokenSigningAlgValuesSupported;
@JsonProperty("id_token_encryption_alg_values_supported")
private List<String> idTokenEncryptionAlgValuesSupported;
@JsonProperty("id_token_encryption_enc_values_supported")
private List<String> idTokenEncryptionEncValuesSupported;
@JsonProperty("userinfo_signing_alg_values_supported")
private List<String> userInfoSigningAlgValuesSupported;
@ -224,6 +230,22 @@ public class OIDCConfigurationRepresentation {
this.idTokenSigningAlgValuesSupported = idTokenSigningAlgValuesSupported;
}
public List<String> getIdTokenEncryptionAlgValuesSupported() {
return idTokenEncryptionAlgValuesSupported;
}
public void setIdTokenEncryptionAlgValuesSupported(List<String> idTokenEncryptionAlgValuesSupported) {
this.idTokenEncryptionAlgValuesSupported = idTokenEncryptionAlgValuesSupported;
}
public List<String> getIdTokenEncryptionEncValuesSupported() {
return idTokenEncryptionEncValuesSupported;
}
public void setIdTokenEncryptionEncValuesSupported(List<String> idTokenEncryptionEncValuesSupported) {
this.idTokenEncryptionEncValuesSupported = idTokenEncryptionEncValuesSupported;
}
public List<String> getUserInfoSigningAlgValuesSupported() {
return userInfoSigningAlgValuesSupported;
}

View file

@ -125,6 +125,14 @@ public class DescriptionConverter {
configWrapper.setIdTokenSignedResponseAlg(clientOIDC.getIdTokenSignedResponseAlg());
}
if (clientOIDC.getIdTokenEncryptedResponseAlg() != null) {
configWrapper.setIdTokenEncryptedResponseAlg(clientOIDC.getIdTokenEncryptedResponseAlg());
}
if (clientOIDC.getIdTokenEncryptedResponseEnc() != null) {
configWrapper.setIdTokenEncryptedResponseEnc(clientOIDC.getIdTokenEncryptedResponseEnc());
}
return client;
}
@ -208,6 +216,12 @@ public class DescriptionConverter {
if (config.getIdTokenSignedResponseAlg() != null) {
response.setIdTokenSignedResponseAlg(config.getIdTokenSignedResponseAlg());
}
if (config.getIdTokenEncryptedResponseAlg() != null) {
response.setIdTokenEncryptedResponseAlg(config.getIdTokenEncryptedResponseAlg());
}
if (config.getIdTokenEncryptedResponseEnc() != null) {
response.setIdTokenEncryptedResponseEnc(config.getIdTokenEncryptedResponseEnc());
}
List<ProtocolMapperRepresentation> foundPairwiseMappers = PairwiseSubMapperUtils.getPairwiseSubMappers(client);
SubjectType subjectType = foundPairwiseMappers.isEmpty() ? SubjectType.PUBLIC : SubjectType.PAIRWISE;

View file

@ -70,7 +70,7 @@ public class KeyResource {
r.setStatus(key.getStatus() != null ? key.getStatus().name() : null);
r.setType(key.getType());
r.setAlgorithm(key.getAlgorithm());
r.setPublicKey(key.getVerifyKey() != null ? PemUtils.encodeKey(key.getVerifyKey()) : null);
r.setPublicKey(key.getPublicKey() != null ? PemUtils.encodeKey(key.getPublicKey()) : null);
r.setCertificate(key.getCertificate() != null ? PemUtils.encodeCertificate(key.getCertificate()) : null);
keys.getKeys().add(r);

View file

@ -0,0 +1,2 @@
org.keycloak.crypto.RsaesPkcs1CekManagementProviderFactory
org.keycloak.crypto.RsaesOaepCekManagementProviderFactory

View file

@ -0,0 +1,2 @@
org.keycloak.crypto.Aes128CbcHmacSha256ContentEncryptionProviderFactory
org.keycloak.crypto.Aes128GcmContentEncryptionProviderFactory

View file

@ -20,6 +20,7 @@ package org.keycloak.testsuite.rest;
import org.keycloak.Config.Scope;
import org.keycloak.crypto.Algorithm;
import org.keycloak.crypto.KeyType;
import org.keycloak.crypto.KeyUse;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.representations.adapters.action.LogoutAction;
@ -69,18 +70,19 @@ public class TestApplicationResourceProviderFactory implements RealmResourceProv
public static class OIDCClientData {
private KeyPair signingKeyPair;
private KeyPair keyPair;
private String oidcRequest;
private List<String> sectorIdentifierRedirectUris;
private String signingKeyType = KeyType.RSA;
private String signingKeyAlgorithm = Algorithm.RS256;
private String keyType = KeyType.RSA;
private String keyAlgorithm = Algorithm.RS256;
private KeyUse keyUse = KeyUse.SIG;
public KeyPair getSigningKeyPair() {
return signingKeyPair;
return keyPair;
}
public void setSigningKeyPair(KeyPair signingKeyPair) {
this.signingKeyPair = signingKeyPair;
this.keyPair = signingKeyPair;
}
public String getOidcRequest() {
@ -100,19 +102,51 @@ public class TestApplicationResourceProviderFactory implements RealmResourceProv
}
public String getSigningKeyType() {
return signingKeyType;
return keyType;
}
public void setSigningKeyType(String signingKeyType) {
this.signingKeyType = signingKeyType;
this.keyType = signingKeyType;
}
public String getSigningKeyAlgorithm() {
return signingKeyAlgorithm;
return keyAlgorithm;
}
public void setSigningKeyAlgorithm(String signingKeyAlgorithm) {
this.signingKeyAlgorithm = signingKeyAlgorithm;
this.keyAlgorithm = signingKeyAlgorithm;
}
public KeyPair getKeyPair() {
return keyPair;
}
public void setKeyPair(KeyPair keyPair) {
this.keyPair = keyPair;
}
public String getKeyType() {
return keyType;
}
public void setKeyType(String keyType) {
this.keyType = keyType;
}
public String getKeyAlgorithm() {
return keyAlgorithm;
}
public void setKeyAlgorithm(String keyAlgorithm) {
this.keyAlgorithm = keyAlgorithm;
}
public KeyUse getKeyUse() {
return keyUse;
}
public void setKeyUse(KeyUse keyUse) {
this.keyUse = keyUse;
}
}
}

View file

@ -25,8 +25,10 @@ import org.keycloak.common.util.PemUtils;
import org.keycloak.crypto.Algorithm;
import org.keycloak.crypto.AsymmetricSignatureSignerContext;
import org.keycloak.crypto.KeyType;
import org.keycloak.crypto.KeyUse;
import org.keycloak.crypto.KeyWrapper;
import org.keycloak.crypto.SignatureSignerContext;
import org.keycloak.jose.jwe.JWEConstants;
import org.keycloak.jose.jwk.JSONWebKeySet;
import org.keycloak.jose.jwk.JWK;
import org.keycloak.jose.jwk.JWKBuilder;
@ -73,7 +75,8 @@ public class TestingOIDCEndpointsApplicationResource {
public Map<String, String> generateKeys(@QueryParam("jwaAlgorithm") String jwaAlgorithm) {
try {
KeyPair keyPair = null;
if (jwaAlgorithm == null) jwaAlgorithm = org.keycloak.crypto.Algorithm.RS256;
KeyUse keyUse = KeyUse.SIG;
if (jwaAlgorithm == null) jwaAlgorithm = Algorithm.RS256;
String keyType = null;
switch (jwaAlgorithm) {
@ -98,13 +101,21 @@ public class TestingOIDCEndpointsApplicationResource {
keyType = KeyType.EC;
keyPair = generateEcdsaKey("secp521r1");
break;
case JWEConstants.RSA1_5:
case JWEConstants.RSA_OAEP:
// for JWE KEK Key Encryption
keyType = KeyType.RSA;
keyUse = KeyUse.ENC;
keyPair = KeyUtils.generateRsaKeyPair(2048);
break;
default :
throw new RuntimeException("Unsupported signature algorithm");
}
clientData.setSigningKeyPair(keyPair);
clientData.setSigningKeyType(keyType);
clientData.setSigningKeyAlgorithm(jwaAlgorithm);
clientData.setKeyPair(keyPair);
clientData.setKeyType(keyType);
clientData.setKeyAlgorithm(jwaAlgorithm);
clientData.setKeyUse(keyUse);
} catch (Exception e) {
throw new BadRequestException("Error generating signing keypair", e);
}
@ -140,21 +151,23 @@ public class TestingOIDCEndpointsApplicationResource {
@NoCache
public JSONWebKeySet getJwks() {
JSONWebKeySet keySet = new JSONWebKeySet();
KeyPair signingKeyPair = clientData.getSigningKeyPair();
String signingKeyAlgorithm = clientData.getSigningKeyAlgorithm();
String signingKeyType = clientData.getSigningKeyType();
KeyPair keyPair = clientData.getKeyPair();
String keyAlgorithm = clientData.getKeyAlgorithm();
String keyType = clientData.getKeyType();
KeyUse keyUse = clientData.getKeyUse();
if (signingKeyPair == null || !isSupportedSigningAlgorithm(signingKeyAlgorithm)) {
if (keyPair == null || !isSupportedAlgorithm(keyAlgorithm)) {
keySet.setKeys(new JWK[] {});
} else if (KeyType.RSA.equals(signingKeyType)) {
keySet.setKeys(new JWK[] { JWKBuilder.create().algorithm(signingKeyAlgorithm).rsa(signingKeyPair.getPublic()) });
} else if (KeyType.EC.equals(signingKeyType)) {
keySet.setKeys(new JWK[] { JWKBuilder.create().algorithm(signingKeyAlgorithm).ec(signingKeyPair.getPublic()) });
} else if (KeyType.RSA.equals(keyType)) {
keySet.setKeys(new JWK[] { JWKBuilder.create().algorithm(keyAlgorithm).rsa(keyPair.getPublic(), keyUse) });
} else if (KeyType.EC.equals(keyType)) {
keySet.setKeys(new JWK[] { JWKBuilder.create().algorithm(keyAlgorithm).ec(keyPair.getPublic()) });
} else {
keySet.setKeys(new JWK[] {});
}
return keySet;
}
@ -174,7 +187,7 @@ public class TestingOIDCEndpointsApplicationResource {
oidcRequest.put(OIDCLoginProtocol.MAX_AGE_PARAM, Integer.parseInt(maxAge));
}
if (!isSupportedSigningAlgorithm(jwaAlgorithm)) throw new BadRequestException("Unknown argument: " + jwaAlgorithm);
if (!isSupportedAlgorithm(jwaAlgorithm)) throw new BadRequestException("Unknown argument: " + jwaAlgorithm);
if ("none".equals(jwaAlgorithm)) {
clientData.setOidcRequest(new JWSBuilder().jsonContent(oidcRequest).none());
@ -186,13 +199,14 @@ public class TestingOIDCEndpointsApplicationResource {
KeyWrapper keyWrapper = new KeyWrapper();
keyWrapper.setAlgorithm(clientData.getSigningKeyAlgorithm());
keyWrapper.setKid(kid);
keyWrapper.setSignKey(privateKey);
keyWrapper.setPrivateKey(privateKey);
SignatureSignerContext signer = new AsymmetricSignatureSignerContext(keyWrapper);
clientData.setOidcRequest(new JWSBuilder().kid(kid).jsonContent(oidcRequest).sign(signer));
}
}
private boolean isSupportedSigningAlgorithm(String signingAlgorithm) {
private boolean isSupportedAlgorithm(String signingAlgorithm) {
if (signingAlgorithm == null) return false;
boolean ret = false;
switch (signingAlgorithm) {
case "none":
@ -205,6 +219,8 @@ public class TestingOIDCEndpointsApplicationResource {
case Algorithm.ES256:
case Algorithm.ES384:
case Algorithm.ES512:
case JWEConstants.RSA1_5:
case JWEConstants.RSA_OAEP:
ret = true;
}
return ret;

View file

@ -1236,7 +1236,7 @@ public class OAuthClient {
KeyWrapper key = new KeyWrapper();
key.setKid(k.getKeyId());
key.setAlgorithm(k.getAlgorithm());
key.setVerifyKey(publicKey);
key.setPublicKey(publicKey);
key.setUse(KeyUse.SIG);
return key;

View file

@ -75,6 +75,20 @@ public class TokenSignatureUtil {
clientResource.update(clientRep);
}
public static void changeClientIdTokenEncryptionAlgProvider(ClientResource clientResource, String toAlgName) {
ClientRepresentation clientRep = clientResource.toRepresentation();
log.tracef("change client %s id token encryption alg algorithm from %s to %s", clientRep.getClientId(), clientRep.getAttributes().get(OIDCConfigAttributes.ID_TOKEN_ENCRYPTED_RESPONSE_ALG), toAlgName);
clientRep.getAttributes().put(OIDCConfigAttributes.ID_TOKEN_ENCRYPTED_RESPONSE_ALG, toAlgName);
clientResource.update(clientRep);
}
public static void changeClientIdTokenEncryptionEncProvider(ClientResource clientResource, String toEncName) {
ClientRepresentation clientRep = clientResource.toRepresentation();
log.tracef("change client %s id token encryption enc algorithm from %s to %s", clientRep.getClientId(), clientRep.getAttributes().get(OIDCConfigAttributes.ID_TOKEN_ENCRYPTED_RESPONSE_ENC), toEncName);
clientRep.getAttributes().put(OIDCConfigAttributes.ID_TOKEN_ENCRYPTED_RESPONSE_ENC, toEncName);
clientResource.update(clientRep);
}
public static boolean verifySignature(String sigAlgName, String token, Keycloak adminClient) throws Exception {
PublicKey publicKey = getRealmPublicKey(TEST_REALM_NAME, sigAlgName, adminClient);
JWSInput jws = new JWSInput(token);

View file

@ -27,6 +27,7 @@ import org.keycloak.client.registration.ClientRegistrationException;
import org.keycloak.client.registration.HttpErrorException;
import org.keycloak.common.util.CollectionUtil;
import org.keycloak.events.Errors;
import org.keycloak.jose.jwe.JWEConstants;
import org.keycloak.jose.jws.Algorithm;
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
@ -305,6 +306,47 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest {
}
@Test
public void testIdTokenEncryptedResponse() throws Exception {
OIDCClientRepresentation response = null;
OIDCClientRepresentation updated = null;
try {
// create (no specification)
OIDCClientRepresentation clientRep = createRep();
response = reg.oidc().create(clientRep);
Assert.assertEquals(Boolean.FALSE, response.getTlsClientCertificateBoundAccessTokens());
Assert.assertNotNull(response.getClientSecret());
// Test Keycloak representation
ClientRepresentation kcClient = getClient(response.getClientId());
OIDCAdvancedConfigWrapper config = OIDCAdvancedConfigWrapper.fromClientRepresentation(kcClient);
Assert.assertNull(config.getIdTokenEncryptedResponseAlg());
Assert.assertNull(config.getIdTokenEncryptedResponseEnc());
// update (alg RSA1_5, enc A128CBC-HS256)
reg.auth(Auth.token(response));
response.setIdTokenEncryptedResponseAlg(JWEConstants.RSA1_5);
response.setIdTokenEncryptedResponseEnc(JWEConstants.A128CBC_HS256);
updated = reg.oidc().update(response);
Assert.assertEquals(JWEConstants.RSA1_5, updated.getIdTokenEncryptedResponseAlg());
Assert.assertEquals(JWEConstants.A128CBC_HS256, updated.getIdTokenEncryptedResponseEnc());
// Test Keycloak representation
kcClient = getClient(updated.getClientId());
config = OIDCAdvancedConfigWrapper.fromClientRepresentation(kcClient);
Assert.assertEquals(JWEConstants.RSA1_5, config.getIdTokenEncryptedResponseAlg());
Assert.assertEquals(JWEConstants.A128CBC_HS256, config.getIdTokenEncryptedResponseEnc());
} finally {
// revert
reg.auth(Auth.token(updated));
updated.setIdTokenEncryptedResponseAlg(null);
updated.setIdTokenEncryptedResponseEnc(null);
reg.oidc().update(updated);
}
}
@Test
public void testOIDCEndpointCreateWithSamlClient() throws Exception {
ClientsResource clientsResource = adminClient.realm(TEST).clients();

View file

@ -0,0 +1,260 @@
/*
* Copyright 2018 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.oidc;
import java.io.UnsupportedEncodingException;
import java.security.PrivateKey;
import java.util.List;
import java.util.Map;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.graphene.page.Page;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.OAuthErrorException;
import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.common.util.PemUtils;
import org.keycloak.crypto.AesCbcHmacShaContentEncryptionProvider;
import org.keycloak.crypto.AesGcmContentEncryptionProvider;
import org.keycloak.crypto.Algorithm;
import org.keycloak.crypto.RsaCekManagementProvider;
import org.keycloak.jose.jwe.JWEConstants;
import org.keycloak.jose.jwe.JWEException;
import org.keycloak.jose.jwe.alg.JWEAlgorithmProvider;
import org.keycloak.jose.jwe.enc.JWEEncryptionProvider;
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
import org.keycloak.representations.IDToken;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.admin.AbstractAdminTest;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.arquillian.annotation.UncaughtServerErrorExpected;
import org.keycloak.testsuite.client.resources.TestApplicationResourceUrls;
import org.keycloak.testsuite.client.resources.TestOIDCEndpointsApplicationResource;
import org.keycloak.testsuite.pages.AccountUpdateProfilePage;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.ErrorPage;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.pages.OAuthGrantPage;
import org.keycloak.testsuite.runonserver.RunOnServerDeployment;
import org.keycloak.testsuite.util.ClientManager;
import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.OAuthClient.AccessTokenResponse;
import org.keycloak.testsuite.util.TokenSignatureUtil;
import org.keycloak.util.TokenUtil;
public class IdTokenEncryptionTest extends AbstractTestRealmKeycloakTest {
@Rule
public AssertEvents events = new AssertEvents(this);
@Page
protected AppPage appPage;
@Page
protected LoginPage loginPage;
@Page
protected AccountUpdateProfilePage profilePage;
@Page
protected OAuthGrantPage grantPage;
@Page
protected ErrorPage errorPage;
@Deployment
public static WebArchive deploy() {
return RunOnServerDeployment.create(OIDCAdvancedRequestParamsTest.class, AbstractTestRealmKeycloakTest.class);
}
@Override
public void configureTestRealm(RealmRepresentation testRealm) {
}
@Before
public void clientConfiguration() {
ClientManager.realm(adminClient.realm("test")).clientId("test-app").directAccessGrant(true);
/*
* Configure the default client ID. Seems like OAuthClient is keeping the state of clientID
* For example: If some test case configure oauth.clientId("sample-public-client"), other tests
* will faile and the clientID will always be "sample-public-client
* @see AccessTokenTest#testAuthorizationNegotiateHeaderIgnored()
*/
oauth.clientId("test-app");
oauth.maxAge(null);
}
@Override
public void addTestRealms(List<RealmRepresentation> testRealms) {
RealmRepresentation realm = AbstractAdminTest.loadJson(getClass().getResourceAsStream("/testrealm.json"), RealmRepresentation.class);
testRealms.add(realm);
}
@Test
public void testIdTokenEncryptionAlgRSA1_5EncA128CBC_HS256() {
// add key provider explicitly though DefaultKeyManager create fallback key provider if not exist
TokenSignatureUtil.registerKeyProvider("P-256", adminClient, testContext);
testIdTokenSignatureAndEncryption(Algorithm.ES256, JWEConstants.RSA1_5, JWEConstants.A128CBC_HS256);
}
@Test
public void testIdTokenEncryptionAlgRSA1_5EncA128GCM() {
testIdTokenSignatureAndEncryption(Algorithm.RS384, JWEConstants.RSA1_5, JWEConstants.A128GCM);
}
@Test
public void testIdTokenEncryptionAlgRSA_OAEPEncA128CBC_HS256() {
// add key provider explicitly though DefaultKeyManager create fallback key provider if not exist
TokenSignatureUtil.registerKeyProvider("P-521", adminClient, testContext);
testIdTokenSignatureAndEncryption(Algorithm.ES512, JWEConstants.RSA_OAEP, JWEConstants.A128CBC_HS256);
}
@Test
public void testIdTokenEncryptionAlgRSA_OAEPEncA128GCM() {
// add key provider explicitly though DefaultKeyManager create fallback key provider if not exist
TokenSignatureUtil.registerKeyProvider("P-256", adminClient, testContext);
testIdTokenSignatureAndEncryption(Algorithm.ES256, JWEConstants.RSA_OAEP, JWEConstants.A128GCM);
}
private void testIdTokenSignatureAndEncryption(String sigAlgorithm, String algAlgorithm, String encAlgorithm) {
ClientResource clientResource = null;
ClientRepresentation clientRep = null;
try {
// generate and register encryption key onto client
TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
oidcClientEndpointsResource.generateKeys(algAlgorithm);
clientResource = ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app");
clientRep = clientResource.toRepresentation();
// set id token signature algorithm and encryption algorithms
OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setIdTokenSignedResponseAlg(sigAlgorithm);
OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setIdTokenEncryptedResponseAlg(algAlgorithm);
OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setIdTokenEncryptedResponseEnc(encAlgorithm);
// use and set jwks_url
OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setUseJwksUrl(true);
String jwksUrl = TestApplicationResourceUrls.clientJwksUri();
OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setJwksUrl(jwksUrl);
clientResource.update(clientRep);
// get id token
OAuthClient.AuthorizationEndpointResponse response = oauth.doLogin("test-user@localhost", "password");
String code = response.getCode();
OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
// parse JWE and JOSE Header
String jweStr = tokenResponse.getIdToken();
String[] parts = jweStr.split("\\.");
Assert.assertEquals(parts.length, 5);
// get decryption key
// not publickey , use privateKey
Map<String, String> keyPair = oidcClientEndpointsResource.getKeysAsPem();
PrivateKey decryptionKEK = PemUtils.decodePrivateKey(keyPair.get("privateKey"));
// verify and decrypt JWE
JWEAlgorithmProvider algorithmProvider = getJweAlgorithmProvider(algAlgorithm);
JWEEncryptionProvider encryptionProvider = getJweEncryptionProvider(encAlgorithm);
byte[] decodedString = TokenUtil.jweKeyEncryptionVerifyAndDecode(decryptionKEK, jweStr, algorithmProvider, encryptionProvider);
String idTokenString = new String(decodedString, "UTF-8");
// verify JWS
IDToken idToken = oauth.verifyIDToken(idTokenString);
Assert.assertEquals("test-user@localhost", idToken.getPreferredUsername());
Assert.assertEquals("test-app", idToken.getIssuedFor());
} catch (JWEException | UnsupportedEncodingException e) {
Assert.fail();
} finally {
clientResource = ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app");
clientRep = clientResource.toRepresentation();
// revert id token signature algorithm and encryption algorithms
OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setIdTokenSignedResponseAlg(Algorithm.RS256);
OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setIdTokenEncryptedResponseAlg(null);
OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setIdTokenEncryptedResponseEnc(null);
// revert jwks_url settings
OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setUseJwksUrl(false);
OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setJwksUrl(null);
clientResource.update(clientRep);
}
}
private JWEAlgorithmProvider getJweAlgorithmProvider(String algAlgorithm) {
JWEAlgorithmProvider jweAlgorithmProvider = null;
if (JWEConstants.RSA1_5.equals(algAlgorithm) || JWEConstants.RSA_OAEP.equals(algAlgorithm) ) {
jweAlgorithmProvider = new RsaCekManagementProvider(null, algAlgorithm).jweAlgorithmProvider();
}
return jweAlgorithmProvider;
}
private JWEEncryptionProvider getJweEncryptionProvider(String encAlgorithm) {
JWEEncryptionProvider jweEncryptionProvider = null;
if (JWEConstants.A128CBC_HS256.equals(encAlgorithm)) {
jweEncryptionProvider = new AesCbcHmacShaContentEncryptionProvider(null, encAlgorithm).jweEncryptionProvider();
} else if (JWEConstants.A128GCM.equals(encAlgorithm)) {
jweEncryptionProvider = new AesGcmContentEncryptionProvider(null, encAlgorithm).jweEncryptionProvider();
}
return jweEncryptionProvider;
}
@Test
@UncaughtServerErrorExpected
public void testIdTokenEncryptionWithoutEncryptionKEK() {
ClientResource clientResource = null;
ClientRepresentation clientRep = null;
try {
// generate and register signing/verifying key onto client, not encryption key
TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
oidcClientEndpointsResource.generateKeys(Algorithm.RS256);
clientResource = ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app");
clientRep = clientResource.toRepresentation();
// set id token signature algorithm and encryption algorithms
OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setIdTokenSignedResponseAlg(Algorithm.RS256);
OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setIdTokenEncryptedResponseAlg(JWEConstants.RSA1_5);
OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setIdTokenEncryptedResponseEnc(JWEConstants.A128CBC_HS256);
// use and set jwks_url
OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setUseJwksUrl(true);
String jwksUrl = TestApplicationResourceUrls.clientJwksUri();
OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setJwksUrl(jwksUrl);
clientResource.update(clientRep);
// get id token but failed
OAuthClient.AuthorizationEndpointResponse response = oauth.doLogin("test-user@localhost", "password");
AccessTokenResponse atr = oauth.doAccessTokenRequest(response.getCode(), "password");
Assert.assertEquals(OAuthErrorException.INVALID_REQUEST, atr.getError());
Assert.assertEquals("can not get encryption KEK", atr.getErrorDescription());
} finally {
// Revert
clientResource = ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app");
clientRep = clientResource.toRepresentation();
OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setIdTokenSignedResponseAlg(Algorithm.RS256);
OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setIdTokenEncryptedResponseAlg(null);
OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setIdTokenEncryptedResponseEnc(null);
// Revert jwks_url settings
OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setUseJwksUrl(false);
OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setJwksUrl(null);
clientResource.update(clientRep);
}
}
}

View file

@ -796,7 +796,7 @@ public class OIDCAdvancedRequestParamsTest extends AbstractTestRealmKeycloakTest
assertEquals("Invalid Request", errorPage.getError());
// Generate keypair for client
String clientPublicKeyPem = oidcClientEndpointsResource.generateKeys(null).get(TestingOIDCEndpointsApplicationResource.PUBLIC_KEY);
String clientPublicKeyPem = oidcClientEndpointsResource.generateKeys("RS256").get(TestingOIDCEndpointsApplicationResource.PUBLIC_KEY);
// Verify signed request_uri will fail due to failed signature validation
oidcClientEndpointsResource.setOIDCRequest("test", "test-app", validRedirectUri, "10", Algorithm.RS256.toString());

View file

@ -26,6 +26,7 @@ import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.broker.provider.util.SimpleHttp;
import org.keycloak.crypto.Algorithm;
import org.keycloak.jose.jwe.JWEConstants;
import org.keycloak.jose.jwk.JSONWebKeySet;
import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
@ -126,9 +127,13 @@ public class OIDCWellKnownProviderTest extends AbstractKeycloakTest {
Assert.assertNames(oidcConfig.getSubjectTypesSupported(), "pairwise", "public");
// Signature algorithms
Assert.assertNames(oidcConfig.getIdTokenSigningAlgValuesSupported(), Algorithm.RS256, Algorithm.RS384, Algorithm.RS512, Algorithm.ES256, Algorithm.ES384, Algorithm.ES512, Algorithm.HS256, Algorithm.HS384, Algorithm.HS512, Algorithm.PS256, Algorithm.PS384, Algorithm.PS512);
Assert.assertNames(oidcConfig.getUserInfoSigningAlgValuesSupported(), "none", Algorithm.RS256, Algorithm.RS384, Algorithm.RS512, Algorithm.ES256, Algorithm.ES384, Algorithm.ES512, Algorithm.HS256, Algorithm.HS384, Algorithm.HS512, Algorithm.PS256, Algorithm.PS384, Algorithm.PS512);
Assert.assertNames(oidcConfig.getRequestObjectSigningAlgValuesSupported(), "none", Algorithm.RS256, Algorithm.RS384, Algorithm.RS512, Algorithm.ES256, Algorithm.ES384, Algorithm.ES512, Algorithm.PS256, Algorithm.PS384, Algorithm.PS512);
Assert.assertNames(oidcConfig.getIdTokenSigningAlgValuesSupported(), Algorithm.PS256, Algorithm.PS384, Algorithm.PS512, Algorithm.RS256, Algorithm.RS384, Algorithm.RS512, Algorithm.ES256, Algorithm.ES384, Algorithm.ES512, Algorithm.HS256, Algorithm.HS384, Algorithm.HS512);
Assert.assertNames(oidcConfig.getUserInfoSigningAlgValuesSupported(), "none", Algorithm.PS256, Algorithm.PS384, Algorithm.PS512, Algorithm.RS256, Algorithm.RS384, Algorithm.RS512, Algorithm.ES256, Algorithm.ES384, Algorithm.ES512, Algorithm.HS256, Algorithm.HS384, Algorithm.HS512);
Assert.assertNames(oidcConfig.getRequestObjectSigningAlgValuesSupported(), "none", Algorithm.PS256, Algorithm.PS384, Algorithm.PS512, Algorithm.RS256, Algorithm.RS384, Algorithm.RS512, Algorithm.ES256, Algorithm.ES384, Algorithm.ES512);
// Encryption algorithms
Assert.assertNames(oidcConfig.getIdTokenEncryptionAlgValuesSupported(), JWEConstants.RSA1_5, JWEConstants.RSA_OAEP);
Assert.assertNames(oidcConfig.getIdTokenEncryptionEncValuesSupported(), JWEConstants.A128CBC_HS256, JWEConstants.A128GCM);
// Client authentication
Assert.assertNames(oidcConfig.getTokenEndpointAuthMethodsSupported(), "client_secret_basic", "client_secret_post", "private_key_jwt", "client_secret_jwt");

View file

@ -340,6 +340,10 @@ access-token-signed-response-alg=Access Token Signature Algorithm
access-token-signed-response-alg.tooltip=JWA algorithm used for signing access tokens.
id-token-signed-response-alg=ID Token Signature Algorithm
id-token-signed-response-alg.tooltip=JWA algorithm used for signing ID tokens.
id-token-encrypted-response-alg=ID Token Encryption Key Management Algorithm
id-token-encrypted-response-alg.tooltip=JWA Algorithm used for key management in encrypting ID tokens. This option is needed just if you want encrypted ID tokens. If left empty, ID Tokens are just signed, but not encrypted.
id-token-encrypted-response-enc=ID Token Encryption Content Encryption Algorithm
id-token-encrypted-response-enc.tooltip=id-token-encrypted-response-enc.tooltip=JWA Algorithm used for content encryption in encrypting ID tokens. This option is needed just if you want encrypted ID tokens. If left empty, ID Tokens are just signed, but not encrypted.
user-info-signed-response-alg=User Info Signed Response Algorithm
user-info-signed-response-alg.tooltip=JWA algorithm used for signed User Info Endpoint response. If set to 'unsigned', then User Info Response won't be signed and will be returned in application/json format.
request-object-signature-alg=Request Object Signature Algorithm

View file

@ -1171,6 +1171,8 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, flows, $ro
$scope.accessTokenSignedResponseAlg = $scope.client.attributes['access.token.signed.response.alg'];
$scope.idTokenSignedResponseAlg = $scope.client.attributes['id.token.signed.response.alg'];
$scope.idTokenEncryptedResponseAlg = $scope.client.attributes['id.token.encrypted.response.alg'];
$scope.idTokenEncryptedResponseEnc = $scope.client.attributes['id.token.encrypted.response.enc'];
var attrVal1 = $scope.client.attributes['user.info.response.signature.alg'];
$scope.userInfoSignedResponseAlg = attrVal1==null ? 'unsigned' : attrVal1;
@ -1293,6 +1295,14 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, flows, $ro
$scope.clientEdit.attributes['id.token.signed.response.alg'] = $scope.idTokenSignedResponseAlg;
};
$scope.changeIdTokenEncryptedResponseAlg = function() {
$scope.clientEdit.attributes['id.token.encrypted.response.alg'] = $scope.idTokenEncryptedResponseAlg;
};
$scope.changeIdTokenEncryptedResponseEnc = function() {
$scope.clientEdit.attributes['id.token.encrypted.response.enc'] = $scope.idTokenEncryptedResponseEnc;
};
$scope.changeUserInfoSignedResponseAlg = function() {
if ($scope.userInfoSignedResponseAlg === 'unsigned') {
$scope.clientEdit.attributes['user.info.response.signature.alg'] = null;

View file

@ -418,6 +418,36 @@
<kc-tooltip>{{:: 'id-token-signed-response-alg.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix block">
<label class="col-md-2 control-label" for="idTokenEncryptedResponseAlg">{{:: 'id-token-encrypted-response-alg' | translate}}</label>
<div class="col-sm-6">
<div>
<select class="form-control" id="idTokenEncryptedResponseAlg"
ng-change="changeIdTokenEncryptedResponseAlg()"
ng-model="idTokenEncryptedResponseAlg">
<option value=""></option>
<option ng-repeat="provider in serverInfo.listProviderIds('cekmanagement')" value="{{provider}}">{{provider}}</option>
</select>
</div>
</div>
<kc-tooltip>{{:: 'id-token-encrypted-response-alg.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix block">
<label class="col-md-2 control-label" for="idTokenEncryptedResponseEnc">{{:: 'id-token-encrypted-response-enc' | translate}}</label>
<div class="col-sm-6">
<div>
<select class="form-control" id="idTokenEncryptedResponseEnc"
ng-change="changeIdTokenEncryptedResponseEnc()"
ng-model="idTokenEncryptedResponseEnc">
<option value=""></option>
<option ng-repeat="provider in serverInfo.listProviderIds('contentencryption')" value="{{provider}}">{{provider}}</option>
</select>
</div>
</div>
<kc-tooltip>{{:: 'id-token-encrypted-response-enc.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix block">
<label class="col-md-2 control-label" for="userInfoSignedResponseAlg">{{:: 'user-info-signed-response-alg' | translate}}</label>
<div class="col-sm-6">