KEYCLOAK-6768 Signed and Encrypted ID Token Support
This commit is contained in:
parent
c158105e2e
commit
8225157a1c
67 changed files with 1851 additions and 130 deletions
|
@ -163,7 +163,7 @@ public class KcinitDriver {
|
||||||
JWE jwe = new JWE();
|
JWE jwe = new JWE();
|
||||||
final SecretKey aesSecret = new SecretKeySpec(aesKey, "AES");
|
final SecretKey aesSecret = new SecretKeySpec(aesKey, "AES");
|
||||||
jwe.getKeyStorage()
|
jwe.getKeyStorage()
|
||||||
.setEncryptionKey(aesSecret);
|
.setDecryptionKey(aesSecret);
|
||||||
return jwe;
|
return jwe;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -46,7 +46,7 @@ public class AsymmetricSignatureSignerContext implements SignatureSignerContext
|
||||||
public byte[] sign(byte[] data) throws SignatureException {
|
public byte[] sign(byte[] data) throws SignatureException {
|
||||||
try {
|
try {
|
||||||
Signature signature = Signature.getInstance(JavaAlgorithm.getJavaAlgorithm(key.getAlgorithm()));
|
Signature signature = Signature.getInstance(JavaAlgorithm.getJavaAlgorithm(key.getAlgorithm()));
|
||||||
signature.initSign((PrivateKey) key.getSignKey());
|
signature.initSign((PrivateKey) key.getPrivateKey());
|
||||||
signature.update(data);
|
signature.update(data);
|
||||||
return signature.sign();
|
return signature.sign();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|
|
@ -43,7 +43,7 @@ public class AsymmetricSignatureVerifierContext implements SignatureVerifierCont
|
||||||
public boolean verify(byte[] data, byte[] signature) throws VerificationException {
|
public boolean verify(byte[] data, byte[] signature) throws VerificationException {
|
||||||
try {
|
try {
|
||||||
Signature verifier = Signature.getInstance(JavaAlgorithm.getJavaAlgorithm(key.getAlgorithm()));
|
Signature verifier = Signature.getInstance(JavaAlgorithm.getJavaAlgorithm(key.getAlgorithm()));
|
||||||
verifier.initVerify((PublicKey) key.getVerifyKey());
|
verifier.initVerify((PublicKey) key.getPublicKey());
|
||||||
verifier.update(data);
|
verifier.update(data);
|
||||||
return verifier.verify(signature);
|
return verifier.verify(signature);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|
|
@ -18,7 +18,17 @@ package org.keycloak.crypto;
|
||||||
|
|
||||||
public enum KeyUse {
|
public enum KeyUse {
|
||||||
|
|
||||||
SIG,
|
SIG("sig"),
|
||||||
ENC
|
ENC("enc");
|
||||||
|
|
||||||
|
private String specName;
|
||||||
|
|
||||||
|
KeyUse(String specName) {
|
||||||
|
this.specName = specName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSpecName() {
|
||||||
|
return specName;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,8 +30,8 @@ public class KeyWrapper {
|
||||||
private KeyUse use;
|
private KeyUse use;
|
||||||
private KeyStatus status;
|
private KeyStatus status;
|
||||||
private SecretKey secretKey;
|
private SecretKey secretKey;
|
||||||
private Key signKey;
|
private Key publicKey;
|
||||||
private Key verifyKey;
|
private Key privateKey;
|
||||||
private X509Certificate certificate;
|
private X509Certificate certificate;
|
||||||
|
|
||||||
public String getProviderId() {
|
public String getProviderId() {
|
||||||
|
@ -98,20 +98,20 @@ public class KeyWrapper {
|
||||||
this.secretKey = secretKey;
|
this.secretKey = secretKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Key getSignKey() {
|
public Key getPrivateKey() {
|
||||||
return signKey;
|
return privateKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setSignKey(Key signKey) {
|
public void setPrivateKey(Key privateKey) {
|
||||||
this.signKey = signKey;
|
this.privateKey = privateKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Key getVerifyKey() {
|
public Key getPublicKey() {
|
||||||
return verifyKey;
|
return publicKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setVerifyKey(Key verifyKey) {
|
public void setPublicKey(Key publicKey) {
|
||||||
this.verifyKey = verifyKey;
|
this.publicKey = publicKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
public X509Certificate getCertificate() {
|
public X509Certificate getCertificate() {
|
||||||
|
@ -121,4 +121,5 @@ public class KeyWrapper {
|
||||||
public void setCertificate(X509Certificate certificate) {
|
public void setCertificate(X509Certificate certificate) {
|
||||||
this.certificate = certificate;
|
this.certificate = certificate;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,8 +19,6 @@ package org.keycloak.jose.jwe;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.security.spec.InvalidKeySpecException;
|
|
||||||
import java.security.spec.KeySpec;
|
import java.security.spec.KeySpec;
|
||||||
|
|
||||||
import org.keycloak.common.util.Base64;
|
import org.keycloak.common.util.Base64;
|
||||||
|
@ -121,6 +119,15 @@ public class JWE {
|
||||||
|
|
||||||
|
|
||||||
public String encodeJwe() throws JWEException {
|
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 {
|
try {
|
||||||
if (header == null) {
|
if (header == null) {
|
||||||
throw new IllegalStateException("Header must be set");
|
throw new IllegalStateException("Header must be set");
|
||||||
|
@ -129,12 +136,10 @@ public class JWE {
|
||||||
throw new IllegalStateException("Content must be set");
|
throw new IllegalStateException("Content must be set");
|
||||||
}
|
}
|
||||||
|
|
||||||
JWEAlgorithmProvider algorithmProvider = JWERegistry.getAlgProvider(header.getAlgorithm());
|
|
||||||
if (algorithmProvider == null) {
|
if (algorithmProvider == null) {
|
||||||
throw new IllegalArgumentException("No provider for alg '" + header.getAlgorithm() + "'");
|
throw new IllegalArgumentException("No provider for alg '" + header.getAlgorithm() + "'");
|
||||||
}
|
}
|
||||||
|
|
||||||
JWEEncryptionProvider encryptionProvider = JWERegistry.getEncProvider(header.getEncryptionAlgorithm());
|
|
||||||
if (encryptionProvider == null) {
|
if (encryptionProvider == null) {
|
||||||
throw new IllegalArgumentException("No provider for enc '" + header.getAlgorithm() + "'");
|
throw new IllegalArgumentException("No provider for enc '" + header.getAlgorithm() + "'");
|
||||||
}
|
}
|
||||||
|
@ -153,7 +158,6 @@ public class JWE {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private String getEncodedJweString() {
|
private String getEncodedJweString() {
|
||||||
StringBuilder builder = new StringBuilder();
|
StringBuilder builder = new StringBuilder();
|
||||||
builder.append(base64Header).append(".")
|
builder.append(base64Header).append(".")
|
||||||
|
@ -165,39 +169,53 @@ public class JWE {
|
||||||
return builder.toString();
|
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 {
|
public JWE verifyAndDecodeJwe(String jweStr) throws JWEException {
|
||||||
try {
|
try {
|
||||||
String[] parts = jweStr.split("\\.");
|
setupJWEHeader(jweStr);
|
||||||
if (parts.length != 5) {
|
return getProcessedJWE(JWERegistry.getAlgProvider(header.getAlgorithm()), JWERegistry.getEncProvider(header.getEncryptionAlgorithm()));
|
||||||
throw new IllegalStateException("Not a JWE String");
|
} catch (Exception e) {
|
||||||
}
|
throw new JWEException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.base64Header = parts[0];
|
public JWE verifyAndDecodeJwe(String jweStr, JWEAlgorithmProvider algorithmProvider, JWEEncryptionProvider encryptionProvider) throws JWEException {
|
||||||
this.base64Cek = parts[1];
|
try {
|
||||||
this.initializationVector = Base64Url.decode(parts[2]);
|
setupJWEHeader(jweStr);
|
||||||
this.encryptedContent = Base64Url.decode(parts[3]);
|
return getProcessedJWE(algorithmProvider, encryptionProvider);
|
||||||
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;
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new JWEException(e);
|
throw new JWEException(e);
|
||||||
}
|
}
|
||||||
|
@ -247,7 +265,7 @@ public class JWE {
|
||||||
|
|
||||||
JWE jwe = new JWE();
|
JWE jwe = new JWE();
|
||||||
jwe.getKeyStorage()
|
jwe.getKeyStorage()
|
||||||
.setEncryptionKey(aesKey);
|
.setDecryptionKey(aesKey);
|
||||||
|
|
||||||
jwe.verifyAndDecodeJwe(encodedJwe);
|
jwe.verifyAndDecodeJwe(encodedJwe);
|
||||||
return jwe.getContent();
|
return jwe.getContent();
|
||||||
|
|
|
@ -24,9 +24,13 @@ public class JWEConstants {
|
||||||
|
|
||||||
public static final String DIR = "dir";
|
public static final String DIR = "dir";
|
||||||
public static final String A128KW = "A128KW";
|
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 A128CBC_HS256 = "A128CBC-HS256";
|
||||||
public static final String A192CBC_HS384 = "A192CBC-HS384";
|
public static final String A192CBC_HS384 = "A192CBC-HS384";
|
||||||
public static final String A256CBC_HS512 = "A256CBC-HS512";
|
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";
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,6 +59,13 @@ public class JWEHeader implements Serializable {
|
||||||
this.compressionAlgorithm = compressionAlgorithm;
|
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() {
|
public String getAlgorithm() {
|
||||||
return algorithm;
|
return algorithm;
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,7 @@ import org.keycloak.jose.jwe.enc.JWEEncryptionProvider;
|
||||||
public class JWEKeyStorage {
|
public class JWEKeyStorage {
|
||||||
|
|
||||||
private Key encryptionKey;
|
private Key encryptionKey;
|
||||||
|
private Key decryptionKey;
|
||||||
|
|
||||||
private byte[] cekBytes;
|
private byte[] cekBytes;
|
||||||
|
|
||||||
|
@ -46,6 +47,14 @@ public class JWEKeyStorage {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Key getDecryptionKey() {
|
||||||
|
return decryptionKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JWEKeyStorage setDecryptionKey(Key decryptionKey) {
|
||||||
|
this.decryptionKey = decryptionKey;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public void setCEKBytes(byte[] cekBytes) {
|
public void setCEKBytes(byte[] cekBytes) {
|
||||||
this.cekBytes = cekBytes;
|
this.cekBytes = cekBytes;
|
||||||
|
@ -100,4 +109,5 @@ public class JWEKeyStorage {
|
||||||
ENCRYPTION,
|
ENCRYPTION,
|
||||||
SIGNATURE
|
SIGNATURE
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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();
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -22,6 +22,7 @@ import org.keycloak.common.util.KeyUtils;
|
||||||
import org.keycloak.common.util.PemUtils;
|
import org.keycloak.common.util.PemUtils;
|
||||||
import org.keycloak.crypto.Algorithm;
|
import org.keycloak.crypto.Algorithm;
|
||||||
import org.keycloak.crypto.KeyType;
|
import org.keycloak.crypto.KeyType;
|
||||||
|
import org.keycloak.crypto.KeyUse;
|
||||||
|
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.security.Key;
|
import java.security.Key;
|
||||||
|
@ -64,7 +65,7 @@ public class JWKBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
public JWK rsa(Key key) {
|
public JWK rsa(Key key) {
|
||||||
return rsa(key, null);
|
return rsa(key, (X509Certificate)null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public JWK rsa(Key key, X509Certificate certificate) {
|
public JWK rsa(Key key, X509Certificate certificate) {
|
||||||
|
@ -87,6 +88,14 @@ public class JWKBuilder {
|
||||||
return k;
|
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) {
|
public JWK ec(Key key) {
|
||||||
ECPublicKey ecKey = (ECPublicKey) key;
|
ECPublicKey ecKey = (ECPublicKey) key;
|
||||||
|
|
||||||
|
|
|
@ -55,7 +55,7 @@ public class JWKSUtils {
|
||||||
keyWrapper.setAlgorithm(jwk.getAlgorithm());
|
keyWrapper.setAlgorithm(jwk.getAlgorithm());
|
||||||
keyWrapper.setType(jwk.getKeyType());
|
keyWrapper.setType(jwk.getKeyType());
|
||||||
keyWrapper.setUse(getKeyUse(jwk.getPublicKeyUse()));
|
keyWrapper.setUse(getKeyUse(jwk.getPublicKeyUse()));
|
||||||
keyWrapper.setVerifyKey(parser.toPublicKey());
|
keyWrapper.setPublicKey(parser.toPublicKey());
|
||||||
result.put(keyWrapper.getKid(), keyWrapper);
|
result.put(keyWrapper.getKid(), keyWrapper);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,8 @@ import org.keycloak.jose.jwe.JWEConstants;
|
||||||
import org.keycloak.jose.jwe.JWEException;
|
import org.keycloak.jose.jwe.JWEException;
|
||||||
import org.keycloak.jose.jwe.JWEHeader;
|
import org.keycloak.jose.jwe.JWEHeader;
|
||||||
import org.keycloak.jose.jwe.JWEKeyStorage;
|
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.JWSInput;
|
||||||
import org.keycloak.jose.jws.JWSInputException;
|
import org.keycloak.jose.jws.JWSInputException;
|
||||||
import org.keycloak.representations.JsonWebToken;
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,18 +19,26 @@ package org.keycloak.jose;
|
||||||
|
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
import java.security.Key;
|
import java.security.Key;
|
||||||
import java.security.spec.KeySpec;
|
import java.security.KeyPair;
|
||||||
|
|
||||||
|
import javax.crypto.Cipher;
|
||||||
import javax.crypto.SecretKey;
|
import javax.crypto.SecretKey;
|
||||||
import javax.crypto.SecretKeyFactory;
|
|
||||||
import javax.crypto.spec.PBEKeySpec;
|
|
||||||
import javax.crypto.spec.SecretKeySpec;
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.keycloak.common.util.Base64;
|
import org.keycloak.common.util.Base64;
|
||||||
import org.keycloak.common.util.Base64Url;
|
import org.keycloak.common.util.Base64Url;
|
||||||
|
import org.keycloak.common.util.KeyUtils;
|
||||||
import org.keycloak.jose.jwe.*;
|
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>
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
@ -151,7 +159,7 @@ public class JWETest {
|
||||||
|
|
||||||
jwe = new JWE();
|
jwe = new JWE();
|
||||||
jwe.getKeyStorage()
|
jwe.getKeyStorage()
|
||||||
.setEncryptionKey(aesKey);
|
.setDecryptionKey(aesKey);
|
||||||
|
|
||||||
jwe.verifyAndDecodeJwe(encodedContent);
|
jwe.verifyAndDecodeJwe(encodedContent);
|
||||||
|
|
||||||
|
@ -223,7 +231,7 @@ public class JWETest {
|
||||||
|
|
||||||
JWE jwe = new JWE();
|
JWE jwe = new JWE();
|
||||||
jwe.getKeyStorage()
|
jwe.getKeyStorage()
|
||||||
.setEncryptionKey(aesKeySpec);
|
.setDecryptionKey(aesKeySpec);
|
||||||
|
|
||||||
jwe.verifyAndDecodeJwe(externalJwe);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -128,10 +128,19 @@ public class InfinispanPublicKeyStorageProvider implements PublicKeyStorageProvi
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public KeyWrapper getPublicKey(String modelKey, String kid, PublicKeyLoader loader) {
|
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
|
// Check if key is in cache
|
||||||
PublicKeysEntry entry = keys.get(modelKey);
|
PublicKeysEntry entry = keys.get(modelKey);
|
||||||
if (entry != null) {
|
if (entry != null) {
|
||||||
KeyWrapper publicKey = getPublicKey(entry.getCurrentKeys(), kid);
|
KeyWrapper publicKey = algorithm != null ? getPublicKeyByAlg(entry.getCurrentKeys(), algorithm) : getPublicKey(entry.getCurrentKeys(), kid);
|
||||||
if (publicKey != null) {
|
if (publicKey != null) {
|
||||||
return publicKey;
|
return publicKey;
|
||||||
}
|
}
|
||||||
|
@ -157,7 +166,7 @@ public class InfinispanPublicKeyStorageProvider implements PublicKeyStorageProvi
|
||||||
entry = task.get();
|
entry = task.get();
|
||||||
|
|
||||||
// Computation finished. Let's see if key is available
|
// 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) {
|
if (publicKey != null) {
|
||||||
return publicKey;
|
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
|
@Override
|
||||||
public void close() {
|
public void close() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private class WrapperCallable implements Callable<PublicKeysEntry> {
|
private class WrapperCallable implements Callable<PublicKeysEntry> {
|
||||||
|
|
||||||
private final String modelKey;
|
private final String modelKey;
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
|
|
||||||
package org.keycloak.keys.infinispan;
|
package org.keycloak.keys.infinispan;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.FutureTask;
|
import java.util.concurrent.FutureTask;
|
||||||
|
@ -27,6 +28,7 @@ import org.keycloak.Config;
|
||||||
import org.keycloak.cluster.ClusterEvent;
|
import org.keycloak.cluster.ClusterEvent;
|
||||||
import org.keycloak.cluster.ClusterProvider;
|
import org.keycloak.cluster.ClusterProvider;
|
||||||
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
|
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
|
||||||
|
import org.keycloak.jose.jwk.JWK;
|
||||||
import org.keycloak.keys.PublicKeyStorageProvider;
|
import org.keycloak.keys.PublicKeyStorageProvider;
|
||||||
import org.keycloak.keys.PublicKeyStorageProviderFactory;
|
import org.keycloak.keys.PublicKeyStorageProviderFactory;
|
||||||
import org.keycloak.keys.PublicKeyStorageUtils;
|
import org.keycloak.keys.PublicKeyStorageUtils;
|
||||||
|
@ -105,7 +107,7 @@ public class InfinispanPublicKeyStorageProviderFactory implements PublicKeyStora
|
||||||
if (cacheKey != null) {
|
if (cacheKey != null) {
|
||||||
log.debugf("Invalidating %s from keysCache", cacheKey);
|
log.debugf("Invalidating %s from keysCache", cacheKey);
|
||||||
InfinispanPublicKeyStorageProvider provider = (InfinispanPublicKeyStorageProvider) cacheKey.session.getProvider(PublicKeyStorageProvider.class, getId());
|
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) {
|
private SessionAndKeyHolder getCacheKeyToInvalidate(ProviderEvent event) {
|
||||||
|
ArrayList<String> cacheKeys = new ArrayList<>();
|
||||||
|
String cacheKey = null;
|
||||||
if (event instanceof RealmModel.ClientUpdatedEvent) {
|
if (event instanceof RealmModel.ClientUpdatedEvent) {
|
||||||
RealmModel.ClientUpdatedEvent eventt = (RealmModel.ClientUpdatedEvent) event;
|
RealmModel.ClientUpdatedEvent eventt = (RealmModel.ClientUpdatedEvent) event;
|
||||||
String cacheKey = PublicKeyStorageUtils.getClientModelCacheKey(eventt.getUpdatedClient().getRealm().getId(), eventt.getUpdatedClient().getId());
|
cacheKey = PublicKeyStorageUtils.getClientModelCacheKey(eventt.getUpdatedClient().getRealm().getId(), eventt.getUpdatedClient().getId(), JWK.Use.SIG);
|
||||||
return new SessionAndKeyHolder(eventt.getKeycloakSession(), cacheKey);
|
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) {
|
} else if (event instanceof RealmModel.ClientRemovedEvent) {
|
||||||
RealmModel.ClientRemovedEvent eventt = (RealmModel.ClientRemovedEvent) event;
|
RealmModel.ClientRemovedEvent eventt = (RealmModel.ClientRemovedEvent) event;
|
||||||
String cacheKey = PublicKeyStorageUtils.getClientModelCacheKey(eventt.getClient().getRealm().getId(), eventt.getClient().getId());
|
cacheKey = PublicKeyStorageUtils.getClientModelCacheKey(eventt.getClient().getRealm().getId(), eventt.getClient().getId(), JWK.Use.SIG);
|
||||||
return new SessionAndKeyHolder(eventt.getKeycloakSession(), cacheKey);
|
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) {
|
} else if (event instanceof RealmModel.IdentityProviderUpdatedEvent) {
|
||||||
RealmModel.IdentityProviderUpdatedEvent eventt = (RealmModel.IdentityProviderUpdatedEvent) event;
|
RealmModel.IdentityProviderUpdatedEvent eventt = (RealmModel.IdentityProviderUpdatedEvent) event;
|
||||||
String cacheKey = PublicKeyStorageUtils.getIdpModelCacheKey(eventt.getRealm().getId(), eventt.getUpdatedIdentityProvider().getInternalId());
|
cacheKey = PublicKeyStorageUtils.getIdpModelCacheKey(eventt.getRealm().getId(), eventt.getUpdatedIdentityProvider().getInternalId());
|
||||||
return new SessionAndKeyHolder(eventt.getKeycloakSession(), cacheKey);
|
cacheKeys.add(cacheKey);
|
||||||
|
return new SessionAndKeyHolder(eventt.getKeycloakSession(), cacheKeys);
|
||||||
} else if (event instanceof RealmModel.IdentityProviderRemovedEvent) {
|
} else if (event instanceof RealmModel.IdentityProviderRemovedEvent) {
|
||||||
RealmModel.IdentityProviderRemovedEvent eventt = (RealmModel.IdentityProviderRemovedEvent) event;
|
RealmModel.IdentityProviderRemovedEvent eventt = (RealmModel.IdentityProviderRemovedEvent) event;
|
||||||
String cacheKey = PublicKeyStorageUtils.getIdpModelCacheKey(eventt.getRealm().getId(), eventt.getRemovedIdentityProvider().getInternalId());
|
cacheKey = PublicKeyStorageUtils.getIdpModelCacheKey(eventt.getRealm().getId(), eventt.getRemovedIdentityProvider().getInternalId());
|
||||||
return new SessionAndKeyHolder(eventt.getKeycloakSession(), cacheKey);
|
cacheKeys.add(cacheKey);
|
||||||
|
return new SessionAndKeyHolder(eventt.getKeycloakSession(), cacheKeys);
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -136,13 +148,12 @@ public class InfinispanPublicKeyStorageProviderFactory implements PublicKeyStora
|
||||||
|
|
||||||
private class SessionAndKeyHolder {
|
private class SessionAndKeyHolder {
|
||||||
private final KeycloakSession session;
|
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.session = session;
|
||||||
this.cacheKey = cacheKey;
|
this.cacheKeys = cacheKeys;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -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() {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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() {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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() {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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() {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -36,6 +36,17 @@ public interface PublicKeyStorageProvider extends Provider {
|
||||||
*/
|
*/
|
||||||
KeyWrapper getPublicKey(String modelKey, String kid, PublicKeyLoader loader);
|
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
|
* Clears all the cached public keys, so they need to be loaded again
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -17,17 +17,25 @@
|
||||||
|
|
||||||
package org.keycloak.keys;
|
package org.keycloak.keys;
|
||||||
|
|
||||||
|
import org.keycloak.jose.jwk.JWK;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
*/
|
*/
|
||||||
public class PublicKeyStorageUtils {
|
public class PublicKeyStorageUtils {
|
||||||
|
|
||||||
|
static final JWK.Use DEFAULT_KEYUSE = JWK.Use.SIG;
|
||||||
|
|
||||||
public static String getClientModelCacheKey(String realmId, String clientUuid) {
|
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) {
|
public static String getIdpModelCacheKey(String realmId, String idpInternalId) {
|
||||||
return realmId + "::idp::" + idpInternalId;
|
return realmId + "::idp::" + idpInternalId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String getClientModelCacheKey(String realmId, String clientUuid, JWK.Use keyUse) {
|
||||||
|
return realmId + "::client::" + clientUuid + "::keyuse::" + keyUse;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,4 +75,6 @@ org.keycloak.storage.client.ClientStorageProviderSpi
|
||||||
org.keycloak.crypto.SignatureSpi
|
org.keycloak.crypto.SignatureSpi
|
||||||
org.keycloak.crypto.ClientSignatureVerifierSpi
|
org.keycloak.crypto.ClientSignatureVerifierSpi
|
||||||
org.keycloak.crypto.HashSpi
|
org.keycloak.crypto.HashSpi
|
||||||
org.keycloak.vault.VaultSpi
|
org.keycloak.vault.VaultSpi
|
||||||
|
org.keycloak.crypto.CekManagementSpi
|
||||||
|
org.keycloak.crypto.ContentEncryptionSpi
|
|
@ -43,4 +43,7 @@ public interface TokenManager {
|
||||||
|
|
||||||
<T> T decodeClientJWT(String token, ClientModel client, Class<T> clazz);
|
<T> T decodeClientJWT(String token, ClientModel client, Class<T> clazz);
|
||||||
|
|
||||||
|
String encodeAndEncrypt(Token token);
|
||||||
|
String cekManagementAlgorithm(TokenCategory category);
|
||||||
|
String encryptAlgorithm(TokenCategory category);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -16,19 +16,34 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.jose.jws;
|
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.jboss.logging.Logger;
|
||||||
import org.keycloak.Token;
|
import org.keycloak.Token;
|
||||||
import org.keycloak.TokenCategory;
|
import org.keycloak.TokenCategory;
|
||||||
import org.keycloak.crypto.Algorithm;
|
import org.keycloak.crypto.Algorithm;
|
||||||
|
import org.keycloak.crypto.CekManagementProvider;
|
||||||
import org.keycloak.crypto.ClientSignatureVerifierProvider;
|
import org.keycloak.crypto.ClientSignatureVerifierProvider;
|
||||||
|
import org.keycloak.crypto.ContentEncryptionProvider;
|
||||||
import org.keycloak.crypto.KeyUse;
|
import org.keycloak.crypto.KeyUse;
|
||||||
|
import org.keycloak.crypto.KeyWrapper;
|
||||||
import org.keycloak.crypto.SignatureProvider;
|
import org.keycloak.crypto.SignatureProvider;
|
||||||
import org.keycloak.crypto.SignatureSignerContext;
|
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.ClientModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.TokenManager;
|
import org.keycloak.models.TokenManager;
|
||||||
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
|
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
|
||||||
|
import org.keycloak.util.TokenUtil;
|
||||||
|
|
||||||
public class DefaultTokenManager implements TokenManager {
|
public class DefaultTokenManager implements TokenManager {
|
||||||
|
|
||||||
|
@ -142,4 +157,86 @@ public class DefaultTokenManager implements TokenManager {
|
||||||
return DEFAULT_ALGORITHM_NAME;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,8 +66,8 @@ public abstract class AbstractEcdsaKeyProvider implements KeyProvider {
|
||||||
key.setType(KeyType.EC);
|
key.setType(KeyType.EC);
|
||||||
key.setAlgorithm(AbstractEcdsaKeyProviderFactory.convertECDomainParmNistRepToAlgorithm(ecInNistRep));
|
key.setAlgorithm(AbstractEcdsaKeyProviderFactory.convertECDomainParmNistRepToAlgorithm(ecInNistRep));
|
||||||
key.setStatus(status);
|
key.setStatus(status);
|
||||||
key.setSignKey(keyPair.getPrivate());
|
key.setPrivateKey(keyPair.getPrivate());
|
||||||
key.setVerifyKey(keyPair.getPublic());
|
key.setPublicKey(keyPair.getPublic());
|
||||||
|
|
||||||
return key;
|
return key;
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,8 +75,8 @@ public abstract class AbstractRsaKeyProvider implements KeyProvider {
|
||||||
key.setType(KeyType.RSA);
|
key.setType(KeyType.RSA);
|
||||||
key.setAlgorithm(algorithm);
|
key.setAlgorithm(algorithm);
|
||||||
key.setStatus(status);
|
key.setStatus(status);
|
||||||
key.setSignKey(keyPair.getPrivate());
|
key.setPrivateKey(keyPair.getPrivate());
|
||||||
key.setVerifyKey(keyPair.getPublic());
|
key.setPublicKey(keyPair.getPublic());
|
||||||
key.setCertificate(certificate);
|
key.setCertificate(certificate);
|
||||||
|
|
||||||
return key;
|
return key;
|
||||||
|
|
|
@ -148,7 +148,7 @@ public class DefaultKeyManager implements KeyManager {
|
||||||
@Deprecated
|
@Deprecated
|
||||||
public ActiveRsaKey getActiveRsaKey(RealmModel realm) {
|
public ActiveRsaKey getActiveRsaKey(RealmModel realm) {
|
||||||
KeyWrapper key = getActiveKey(realm, KeyUse.SIG, Algorithm.RS256);
|
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
|
@Override
|
||||||
|
@ -169,7 +169,7 @@ public class DefaultKeyManager implements KeyManager {
|
||||||
@Deprecated
|
@Deprecated
|
||||||
public PublicKey getRsaPublicKey(RealmModel realm, String kid) {
|
public PublicKey getRsaPublicKey(RealmModel realm, String kid) {
|
||||||
KeyWrapper key = getKey(realm, kid, KeyUse.SIG, Algorithm.RS256);
|
KeyWrapper key = getKey(realm, kid, KeyUse.SIG, Algorithm.RS256);
|
||||||
return key != null ? (PublicKey) key.getVerifyKey() : null;
|
return key != null ? (PublicKey) key.getPublicKey() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -200,7 +200,7 @@ public class DefaultKeyManager implements KeyManager {
|
||||||
for (KeyWrapper key : getKeys(realm, KeyUse.SIG, Algorithm.RS256)) {
|
for (KeyWrapper key : getKeys(realm, KeyUse.SIG, Algorithm.RS256)) {
|
||||||
RsaKeyMetadata m = new RsaKeyMetadata();
|
RsaKeyMetadata m = new RsaKeyMetadata();
|
||||||
m.setCertificate(key.getCertificate());
|
m.setCertificate(key.getCertificate());
|
||||||
m.setPublicKey((PublicKey) key.getVerifyKey());
|
m.setPublicKey((PublicKey) key.getPublicKey());
|
||||||
m.setKid(key.getKid());
|
m.setKid(key.getKid());
|
||||||
m.setProviderId(key.getProviderId());
|
m.setProviderId(key.getProviderId());
|
||||||
m.setProviderPriority(key.getProviderPriority());
|
m.setProviderPriority(key.getProviderPriority());
|
||||||
|
|
|
@ -52,12 +52,19 @@ public class ClientPublicKeyLoader implements PublicKeyLoader {
|
||||||
|
|
||||||
private final KeycloakSession session;
|
private final KeycloakSession session;
|
||||||
private final ClientModel client;
|
private final ClientModel client;
|
||||||
|
private final JWK.Use keyUse;
|
||||||
|
|
||||||
public ClientPublicKeyLoader(KeycloakSession session, ClientModel client) {
|
public ClientPublicKeyLoader(KeycloakSession session, ClientModel client) {
|
||||||
this.session = session;
|
this.session = session;
|
||||||
this.client = client;
|
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
|
@Override
|
||||||
public Map<String, KeyWrapper> loadKeys() throws Exception {
|
public Map<String, KeyWrapper> loadKeys() throws Exception {
|
||||||
|
@ -66,8 +73,8 @@ public class ClientPublicKeyLoader implements PublicKeyLoader {
|
||||||
String jwksUrl = config.getJwksUrl();
|
String jwksUrl = config.getJwksUrl();
|
||||||
jwksUrl = ResolveRelative.resolveRelativeUri(session.getContext().getUri().getRequestUri(), client.getRootUrl(), jwksUrl);
|
jwksUrl = ResolveRelative.resolveRelativeUri(session.getContext().getUri().getRequestUri(), client.getRootUrl(), jwksUrl);
|
||||||
JSONWebKeySet jwks = JWKSHttpUtils.sendJwksRequest(session, jwksUrl);
|
JSONWebKeySet jwks = JWKSHttpUtils.sendJwksRequest(session, jwksUrl);
|
||||||
return JWKSUtils.getKeyWrappersForUse(jwks, JWK.Use.SIG);
|
return JWKSUtils.getKeyWrappersForUse(jwks, keyUse);
|
||||||
} else {
|
} else if (keyUse == JWK.Use.SIG) {
|
||||||
try {
|
try {
|
||||||
CertificateRepresentation certInfo = CertificateInfoHelper.getCertificateFromClient(client, JWTClientAuthenticator.ATTR_PREFIX);
|
CertificateRepresentation certInfo = CertificateInfoHelper.getCertificateFromClient(client, JWTClientAuthenticator.ATTR_PREFIX);
|
||||||
KeyWrapper publicKey = getSignatureValidationKey(certInfo);
|
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());
|
logger.warnf(me, "Unable to retrieve publicKey for verify signature of client '%s' . Error details: %s", client.getClientId(), me.getMessage());
|
||||||
return Collections.emptyMap();
|
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
|
// Check if we have kid in DB, generate otherwise
|
||||||
kid = certInfo.getKid() != null ? certInfo.getKid() : KeyUtils.createKeyId(clientCert.getPublicKey());
|
kid = certInfo.getKid() != null ? certInfo.getKid() : KeyUtils.createKeyId(clientCert.getPublicKey());
|
||||||
keyWrapper.setKid(kid);
|
keyWrapper.setKid(kid);
|
||||||
keyWrapper.setVerifyKey(clientCert.getPublicKey());
|
keyWrapper.setPublicKey(clientCert.getPublicKey());
|
||||||
keyWrapper.setCertificate(clientCert);
|
keyWrapper.setCertificate(clientCert);
|
||||||
} else {
|
} else {
|
||||||
PublicKey publicKey = KeycloakModelUtils.getPublicKey(encodedPublicKey);
|
PublicKey publicKey = KeycloakModelUtils.getPublicKey(encodedPublicKey);
|
||||||
// Check if we have kid in DB, generate otherwise
|
// Check if we have kid in DB, generate otherwise
|
||||||
kid = certInfo.getKid() != null ? certInfo.getKid() : KeyUtils.createKeyId(publicKey);
|
kid = certInfo.getKid() != null ? certInfo.getKid() : KeyUtils.createKeyId(publicKey);
|
||||||
keyWrapper.setKid(kid);
|
keyWrapper.setKid(kid);
|
||||||
keyWrapper.setVerifyKey(publicKey);
|
keyWrapper.setPublicKey(publicKey);
|
||||||
}
|
}
|
||||||
return keyWrapper;
|
return keyWrapper;
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,7 +53,7 @@ public class HardcodedPublicKeyLoader implements PublicKeyLoader {
|
||||||
keyWrapper.setType(KeyType.RSA);
|
keyWrapper.setType(KeyType.RSA);
|
||||||
keyWrapper.setAlgorithm(Algorithm.RS256);
|
keyWrapper.setAlgorithm(Algorithm.RS256);
|
||||||
keyWrapper.setUse(KeyUse.SIG);
|
keyWrapper.setUse(KeyUse.SIG);
|
||||||
keyWrapper.setVerifyKey(PemUtils.decodePublicKey(pem));
|
keyWrapper.setPublicKey(PemUtils.decodePublicKey(pem));
|
||||||
}
|
}
|
||||||
return keyWrapper;
|
return keyWrapper;
|
||||||
}
|
}
|
||||||
|
|
|
@ -84,7 +84,7 @@ public class OIDCIdentityProviderPublicKeyLoader implements PublicKeyLoader {
|
||||||
keyWrapper.setType(KeyType.RSA);
|
keyWrapper.setType(KeyType.RSA);
|
||||||
keyWrapper.setAlgorithm(Algorithm.RS256);
|
keyWrapper.setAlgorithm(Algorithm.RS256);
|
||||||
keyWrapper.setUse(KeyUse.SIG);
|
keyWrapper.setUse(KeyUse.SIG);
|
||||||
keyWrapper.setVerifyKey(publicKey);
|
keyWrapper.setPublicKey(publicKey);
|
||||||
} else {
|
} else {
|
||||||
logger.warnf("No public key saved on identityProvider %s", config.getAlias());
|
logger.warnf("No public key saved on identityProvider %s", config.getAlias());
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ package org.keycloak.keys.loader;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
|
import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
|
||||||
import org.keycloak.crypto.KeyWrapper;
|
import org.keycloak.crypto.KeyWrapper;
|
||||||
|
import org.keycloak.jose.jwk.JWK;
|
||||||
import org.keycloak.jose.jws.JWSInput;
|
import org.keycloak.jose.jws.JWSInput;
|
||||||
import org.keycloak.keys.PublicKeyLoader;
|
import org.keycloak.keys.PublicKeyLoader;
|
||||||
import org.keycloak.keys.PublicKeyStorageProvider;
|
import org.keycloak.keys.PublicKeyStorageProvider;
|
||||||
|
@ -41,7 +42,7 @@ public class PublicKeyStorageManager {
|
||||||
KeyWrapper keyWrapper = getClientPublicKeyWrapper(session, client, input);
|
KeyWrapper keyWrapper = getClientPublicKeyWrapper(session, client, input);
|
||||||
PublicKey publicKey = null;
|
PublicKey publicKey = null;
|
||||||
if (keyWrapper != null) {
|
if (keyWrapper != null) {
|
||||||
publicKey = (PublicKey)keyWrapper.getVerifyKey();
|
publicKey = (PublicKey)keyWrapper.getPublicKey();
|
||||||
}
|
}
|
||||||
return publicKey;
|
return publicKey;
|
||||||
}
|
}
|
||||||
|
@ -54,6 +55,13 @@ public class PublicKeyStorageManager {
|
||||||
return keyStorage.getPublicKey(modelKey, kid, loader);
|
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) {
|
public static PublicKey getIdentityProviderPublicKey(KeycloakSession session, RealmModel realm, OIDCIdentityProviderConfig idpConfig, JWSInput input) {
|
||||||
boolean keyIdSetInConfiguration = idpConfig.getPublicKeySignatureVerifierKeyId() != null
|
boolean keyIdSetInConfiguration = idpConfig.getPublicKeySignatureVerifierKeyId() != null
|
||||||
&& ! idpConfig.getPublicKeySignatureVerifierKeyId().trim().isEmpty();
|
&& ! idpConfig.getPublicKeySignatureVerifierKeyId().trim().isEmpty();
|
||||||
|
@ -80,6 +88,6 @@ public class PublicKeyStorageManager {
|
||||||
: kid, pem);
|
: kid, pem);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (PublicKey)keyStorage.getPublicKey(modelKey, kid, loader).getVerifyKey();
|
return (PublicKey)keyStorage.getPublicKey(modelKey, kid, loader).getPublicKey();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -133,6 +133,22 @@ public class OIDCAdvancedConfigWrapper {
|
||||||
setAttribute(OIDCConfigAttributes.ID_TOKEN_SIGNED_RESPONSE_ALG, algName);
|
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) {
|
private String getAttribute(String attrKey) {
|
||||||
if (clientModel != null) {
|
if (clientModel != null) {
|
||||||
return clientModel.getAttribute(attrKey);
|
return clientModel.getAttribute(attrKey);
|
||||||
|
|
|
@ -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_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_SIGNED_RESPONSE_ALG = "access.token.signed.response.alg";
|
||||||
|
|
||||||
public static final String ACCESS_TOKEN_LIFESPAN = "access.token.lifespan";
|
public static final String ACCESS_TOKEN_LIFESPAN = "access.token.lifespan";
|
||||||
|
|
|
@ -199,12 +199,12 @@ public class OIDCLoginProtocolService {
|
||||||
public Response certs() {
|
public Response certs() {
|
||||||
List<JWK> keys = new LinkedList<>();
|
List<JWK> keys = new LinkedList<>();
|
||||||
for (KeyWrapper k : session.keys().getKeys(realm)) {
|
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());
|
JWKBuilder b = JWKBuilder.create().kid(k.getKid()).algorithm(k.getAlgorithm());
|
||||||
if (k.getType().equals(KeyType.RSA)) {
|
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)) {
|
} else if (k.getType().equals(KeyType.EC)) {
|
||||||
keys.add(b.ec(k.getVerifyKey()));
|
keys.add(b.ec(k.getPublicKey()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,9 @@ package org.keycloak.protocol.oidc;
|
||||||
import org.keycloak.OAuth2Constants;
|
import org.keycloak.OAuth2Constants;
|
||||||
import org.keycloak.authentication.ClientAuthenticator;
|
import org.keycloak.authentication.ClientAuthenticator;
|
||||||
import org.keycloak.authentication.ClientAuthenticatorFactory;
|
import org.keycloak.authentication.ClientAuthenticatorFactory;
|
||||||
|
import org.keycloak.crypto.CekManagementProvider;
|
||||||
import org.keycloak.crypto.ClientSignatureVerifierProvider;
|
import org.keycloak.crypto.ClientSignatureVerifierProvider;
|
||||||
|
import org.keycloak.crypto.ContentEncryptionProvider;
|
||||||
import org.keycloak.crypto.SignatureProvider;
|
import org.keycloak.crypto.SignatureProvider;
|
||||||
import org.keycloak.jose.jws.Algorithm;
|
import org.keycloak.jose.jws.Algorithm;
|
||||||
import org.keycloak.models.ClientScopeModel;
|
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.setRegistrationEndpoint(RealmsResource.clientRegistrationUrl(uriInfo).path(ClientRegistrationService.class, "provider").build(realm.getName(), OIDCClientRegistrationProviderFactory.ID).toString());
|
||||||
|
|
||||||
config.setIdTokenSigningAlgValuesSupported(getSupportedSigningAlgorithms(false));
|
config.setIdTokenSigningAlgValuesSupported(getSupportedSigningAlgorithms(false));
|
||||||
|
config.setIdTokenEncryptionAlgValuesSupported(getSupportedIdTokenEncryptionAlg(false));
|
||||||
|
config.setIdTokenEncryptionEncValuesSupported(getSupportedIdTokenEncryptionEnc(false));
|
||||||
config.setUserInfoSigningAlgValuesSupported(getSupportedSigningAlgorithms(true));
|
config.setUserInfoSigningAlgValuesSupported(getSupportedSigningAlgorithms(true));
|
||||||
config.setRequestObjectSigningAlgValuesSupported(getSupportedClientSigningAlgorithms(true));
|
config.setRequestObjectSigningAlgValuesSupported(getSupportedClientSigningAlgorithms(true));
|
||||||
config.setResponseTypesSupported(DEFAULT_RESPONSE_TYPES_SUPPORTED);
|
config.setResponseTypesSupported(DEFAULT_RESPONSE_TYPES_SUPPORTED);
|
||||||
|
@ -172,4 +176,26 @@ public class OIDCWellKnownProvider implements WellKnownProvider {
|
||||||
}
|
}
|
||||||
return result;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -824,7 +824,7 @@ public class TokenManager {
|
||||||
idToken.setStateHash(stateHash);
|
idToken.setStateHash(stateHash);
|
||||||
}
|
}
|
||||||
if (idToken != null) {
|
if (idToken != null) {
|
||||||
String encodedToken = session.tokens().encode(idToken);
|
String encodedToken = session.tokens().encodeAndEncrypt(idToken);
|
||||||
res.setIdToken(encodedToken);
|
res.setIdToken(encodedToken);
|
||||||
}
|
}
|
||||||
if (refreshToken != null) {
|
if (refreshToken != null) {
|
||||||
|
|
|
@ -411,9 +411,18 @@ public class TokenEndpoint {
|
||||||
if (TokenUtil.isOIDCRequest(scopeParam)) {
|
if (TokenUtil.isOIDCRequest(scopeParam)) {
|
||||||
responseBuilder.generateIDToken();
|
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();
|
event.success();
|
||||||
|
|
||||||
return cors.builder(Response.ok(res).type(MediaType.APPLICATION_JSON_TYPE)).build();
|
return cors.builder(Response.ok(res).type(MediaType.APPLICATION_JSON_TYPE)).build();
|
||||||
|
@ -605,6 +614,7 @@ public class TokenEndpoint {
|
||||||
responseBuilder.generateIDToken();
|
responseBuilder.generateIDToken();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO : do the same as codeToToken()
|
||||||
AccessTokenResponse res = responseBuilder.build();
|
AccessTokenResponse res = responseBuilder.build();
|
||||||
|
|
||||||
|
|
||||||
|
@ -678,6 +688,7 @@ public class TokenEndpoint {
|
||||||
responseBuilder.generateIDToken();
|
responseBuilder.generateIDToken();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO : do the same as codeToToken()
|
||||||
AccessTokenResponse res = responseBuilder.build();
|
AccessTokenResponse res = responseBuilder.build();
|
||||||
|
|
||||||
event.success();
|
event.success();
|
||||||
|
|
|
@ -71,6 +71,12 @@ public class OIDCConfigurationRepresentation {
|
||||||
@JsonProperty("id_token_signing_alg_values_supported")
|
@JsonProperty("id_token_signing_alg_values_supported")
|
||||||
private List<String> idTokenSigningAlgValuesSupported;
|
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")
|
@JsonProperty("userinfo_signing_alg_values_supported")
|
||||||
private List<String> userInfoSigningAlgValuesSupported;
|
private List<String> userInfoSigningAlgValuesSupported;
|
||||||
|
|
||||||
|
@ -224,6 +230,22 @@ public class OIDCConfigurationRepresentation {
|
||||||
this.idTokenSigningAlgValuesSupported = idTokenSigningAlgValuesSupported;
|
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() {
|
public List<String> getUserInfoSigningAlgValuesSupported() {
|
||||||
return userInfoSigningAlgValuesSupported;
|
return userInfoSigningAlgValuesSupported;
|
||||||
}
|
}
|
||||||
|
|
|
@ -125,6 +125,14 @@ public class DescriptionConverter {
|
||||||
configWrapper.setIdTokenSignedResponseAlg(clientOIDC.getIdTokenSignedResponseAlg());
|
configWrapper.setIdTokenSignedResponseAlg(clientOIDC.getIdTokenSignedResponseAlg());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (clientOIDC.getIdTokenEncryptedResponseAlg() != null) {
|
||||||
|
configWrapper.setIdTokenEncryptedResponseAlg(clientOIDC.getIdTokenEncryptedResponseAlg());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clientOIDC.getIdTokenEncryptedResponseEnc() != null) {
|
||||||
|
configWrapper.setIdTokenEncryptedResponseEnc(clientOIDC.getIdTokenEncryptedResponseEnc());
|
||||||
|
}
|
||||||
|
|
||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -208,6 +216,12 @@ public class DescriptionConverter {
|
||||||
if (config.getIdTokenSignedResponseAlg() != null) {
|
if (config.getIdTokenSignedResponseAlg() != null) {
|
||||||
response.setIdTokenSignedResponseAlg(config.getIdTokenSignedResponseAlg());
|
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);
|
List<ProtocolMapperRepresentation> foundPairwiseMappers = PairwiseSubMapperUtils.getPairwiseSubMappers(client);
|
||||||
SubjectType subjectType = foundPairwiseMappers.isEmpty() ? SubjectType.PUBLIC : SubjectType.PAIRWISE;
|
SubjectType subjectType = foundPairwiseMappers.isEmpty() ? SubjectType.PUBLIC : SubjectType.PAIRWISE;
|
||||||
|
|
|
@ -70,7 +70,7 @@ public class KeyResource {
|
||||||
r.setStatus(key.getStatus() != null ? key.getStatus().name() : null);
|
r.setStatus(key.getStatus() != null ? key.getStatus().name() : null);
|
||||||
r.setType(key.getType());
|
r.setType(key.getType());
|
||||||
r.setAlgorithm(key.getAlgorithm());
|
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);
|
r.setCertificate(key.getCertificate() != null ? PemUtils.encodeCertificate(key.getCertificate()) : null);
|
||||||
keys.getKeys().add(r);
|
keys.getKeys().add(r);
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
org.keycloak.crypto.RsaesPkcs1CekManagementProviderFactory
|
||||||
|
org.keycloak.crypto.RsaesOaepCekManagementProviderFactory
|
|
@ -0,0 +1,2 @@
|
||||||
|
org.keycloak.crypto.Aes128CbcHmacSha256ContentEncryptionProviderFactory
|
||||||
|
org.keycloak.crypto.Aes128GcmContentEncryptionProviderFactory
|
|
@ -20,6 +20,7 @@ package org.keycloak.testsuite.rest;
|
||||||
import org.keycloak.Config.Scope;
|
import org.keycloak.Config.Scope;
|
||||||
import org.keycloak.crypto.Algorithm;
|
import org.keycloak.crypto.Algorithm;
|
||||||
import org.keycloak.crypto.KeyType;
|
import org.keycloak.crypto.KeyType;
|
||||||
|
import org.keycloak.crypto.KeyUse;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.KeycloakSessionFactory;
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
import org.keycloak.representations.adapters.action.LogoutAction;
|
import org.keycloak.representations.adapters.action.LogoutAction;
|
||||||
|
@ -69,18 +70,19 @@ public class TestApplicationResourceProviderFactory implements RealmResourceProv
|
||||||
|
|
||||||
public static class OIDCClientData {
|
public static class OIDCClientData {
|
||||||
|
|
||||||
private KeyPair signingKeyPair;
|
private KeyPair keyPair;
|
||||||
private String oidcRequest;
|
private String oidcRequest;
|
||||||
private List<String> sectorIdentifierRedirectUris;
|
private List<String> sectorIdentifierRedirectUris;
|
||||||
private String signingKeyType = KeyType.RSA;
|
private String keyType = KeyType.RSA;
|
||||||
private String signingKeyAlgorithm = Algorithm.RS256;
|
private String keyAlgorithm = Algorithm.RS256;
|
||||||
|
private KeyUse keyUse = KeyUse.SIG;
|
||||||
|
|
||||||
public KeyPair getSigningKeyPair() {
|
public KeyPair getSigningKeyPair() {
|
||||||
return signingKeyPair;
|
return keyPair;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setSigningKeyPair(KeyPair signingKeyPair) {
|
public void setSigningKeyPair(KeyPair signingKeyPair) {
|
||||||
this.signingKeyPair = signingKeyPair;
|
this.keyPair = signingKeyPair;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getOidcRequest() {
|
public String getOidcRequest() {
|
||||||
|
@ -100,19 +102,51 @@ public class TestApplicationResourceProviderFactory implements RealmResourceProv
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getSigningKeyType() {
|
public String getSigningKeyType() {
|
||||||
return signingKeyType;
|
return keyType;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setSigningKeyType(String signingKeyType) {
|
public void setSigningKeyType(String signingKeyType) {
|
||||||
this.signingKeyType = signingKeyType;
|
this.keyType = signingKeyType;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getSigningKeyAlgorithm() {
|
public String getSigningKeyAlgorithm() {
|
||||||
return signingKeyAlgorithm;
|
return keyAlgorithm;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setSigningKeyAlgorithm(String signingKeyAlgorithm) {
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,8 +25,10 @@ import org.keycloak.common.util.PemUtils;
|
||||||
import org.keycloak.crypto.Algorithm;
|
import org.keycloak.crypto.Algorithm;
|
||||||
import org.keycloak.crypto.AsymmetricSignatureSignerContext;
|
import org.keycloak.crypto.AsymmetricSignatureSignerContext;
|
||||||
import org.keycloak.crypto.KeyType;
|
import org.keycloak.crypto.KeyType;
|
||||||
|
import org.keycloak.crypto.KeyUse;
|
||||||
import org.keycloak.crypto.KeyWrapper;
|
import org.keycloak.crypto.KeyWrapper;
|
||||||
import org.keycloak.crypto.SignatureSignerContext;
|
import org.keycloak.crypto.SignatureSignerContext;
|
||||||
|
import org.keycloak.jose.jwe.JWEConstants;
|
||||||
import org.keycloak.jose.jwk.JSONWebKeySet;
|
import org.keycloak.jose.jwk.JSONWebKeySet;
|
||||||
import org.keycloak.jose.jwk.JWK;
|
import org.keycloak.jose.jwk.JWK;
|
||||||
import org.keycloak.jose.jwk.JWKBuilder;
|
import org.keycloak.jose.jwk.JWKBuilder;
|
||||||
|
@ -73,7 +75,8 @@ public class TestingOIDCEndpointsApplicationResource {
|
||||||
public Map<String, String> generateKeys(@QueryParam("jwaAlgorithm") String jwaAlgorithm) {
|
public Map<String, String> generateKeys(@QueryParam("jwaAlgorithm") String jwaAlgorithm) {
|
||||||
try {
|
try {
|
||||||
KeyPair keyPair = null;
|
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;
|
String keyType = null;
|
||||||
|
|
||||||
switch (jwaAlgorithm) {
|
switch (jwaAlgorithm) {
|
||||||
|
@ -98,13 +101,21 @@ public class TestingOIDCEndpointsApplicationResource {
|
||||||
keyType = KeyType.EC;
|
keyType = KeyType.EC;
|
||||||
keyPair = generateEcdsaKey("secp521r1");
|
keyPair = generateEcdsaKey("secp521r1");
|
||||||
break;
|
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 :
|
default :
|
||||||
throw new RuntimeException("Unsupported signature algorithm");
|
throw new RuntimeException("Unsupported signature algorithm");
|
||||||
}
|
}
|
||||||
|
|
||||||
clientData.setSigningKeyPair(keyPair);
|
clientData.setKeyPair(keyPair);
|
||||||
clientData.setSigningKeyType(keyType);
|
clientData.setKeyType(keyType);
|
||||||
clientData.setSigningKeyAlgorithm(jwaAlgorithm);
|
clientData.setKeyAlgorithm(jwaAlgorithm);
|
||||||
|
clientData.setKeyUse(keyUse);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new BadRequestException("Error generating signing keypair", e);
|
throw new BadRequestException("Error generating signing keypair", e);
|
||||||
}
|
}
|
||||||
|
@ -140,21 +151,23 @@ public class TestingOIDCEndpointsApplicationResource {
|
||||||
@NoCache
|
@NoCache
|
||||||
public JSONWebKeySet getJwks() {
|
public JSONWebKeySet getJwks() {
|
||||||
JSONWebKeySet keySet = new JSONWebKeySet();
|
JSONWebKeySet keySet = new JSONWebKeySet();
|
||||||
KeyPair signingKeyPair = clientData.getSigningKeyPair();
|
KeyPair keyPair = clientData.getKeyPair();
|
||||||
String signingKeyAlgorithm = clientData.getSigningKeyAlgorithm();
|
String keyAlgorithm = clientData.getKeyAlgorithm();
|
||||||
String signingKeyType = clientData.getSigningKeyType();
|
String keyType = clientData.getKeyType();
|
||||||
|
KeyUse keyUse = clientData.getKeyUse();
|
||||||
|
|
||||||
if (signingKeyPair == null || !isSupportedSigningAlgorithm(signingKeyAlgorithm)) {
|
if (keyPair == null || !isSupportedAlgorithm(keyAlgorithm)) {
|
||||||
keySet.setKeys(new JWK[] {});
|
keySet.setKeys(new JWK[] {});
|
||||||
} else if (KeyType.RSA.equals(signingKeyType)) {
|
} else if (KeyType.RSA.equals(keyType)) {
|
||||||
keySet.setKeys(new JWK[] { JWKBuilder.create().algorithm(signingKeyAlgorithm).rsa(signingKeyPair.getPublic()) });
|
keySet.setKeys(new JWK[] { JWKBuilder.create().algorithm(keyAlgorithm).rsa(keyPair.getPublic(), keyUse) });
|
||||||
} else if (KeyType.EC.equals(signingKeyType)) {
|
} else if (KeyType.EC.equals(keyType)) {
|
||||||
keySet.setKeys(new JWK[] { JWKBuilder.create().algorithm(signingKeyAlgorithm).ec(signingKeyPair.getPublic()) });
|
keySet.setKeys(new JWK[] { JWKBuilder.create().algorithm(keyAlgorithm).ec(keyPair.getPublic()) });
|
||||||
} else {
|
} else {
|
||||||
keySet.setKeys(new JWK[] {});
|
keySet.setKeys(new JWK[] {});
|
||||||
}
|
}
|
||||||
|
|
||||||
return keySet;
|
return keySet;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -174,7 +187,7 @@ public class TestingOIDCEndpointsApplicationResource {
|
||||||
oidcRequest.put(OIDCLoginProtocol.MAX_AGE_PARAM, Integer.parseInt(maxAge));
|
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)) {
|
if ("none".equals(jwaAlgorithm)) {
|
||||||
clientData.setOidcRequest(new JWSBuilder().jsonContent(oidcRequest).none());
|
clientData.setOidcRequest(new JWSBuilder().jsonContent(oidcRequest).none());
|
||||||
|
@ -186,13 +199,14 @@ public class TestingOIDCEndpointsApplicationResource {
|
||||||
KeyWrapper keyWrapper = new KeyWrapper();
|
KeyWrapper keyWrapper = new KeyWrapper();
|
||||||
keyWrapper.setAlgorithm(clientData.getSigningKeyAlgorithm());
|
keyWrapper.setAlgorithm(clientData.getSigningKeyAlgorithm());
|
||||||
keyWrapper.setKid(kid);
|
keyWrapper.setKid(kid);
|
||||||
keyWrapper.setSignKey(privateKey);
|
keyWrapper.setPrivateKey(privateKey);
|
||||||
SignatureSignerContext signer = new AsymmetricSignatureSignerContext(keyWrapper);
|
SignatureSignerContext signer = new AsymmetricSignatureSignerContext(keyWrapper);
|
||||||
clientData.setOidcRequest(new JWSBuilder().kid(kid).jsonContent(oidcRequest).sign(signer));
|
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;
|
boolean ret = false;
|
||||||
switch (signingAlgorithm) {
|
switch (signingAlgorithm) {
|
||||||
case "none":
|
case "none":
|
||||||
|
@ -205,6 +219,8 @@ public class TestingOIDCEndpointsApplicationResource {
|
||||||
case Algorithm.ES256:
|
case Algorithm.ES256:
|
||||||
case Algorithm.ES384:
|
case Algorithm.ES384:
|
||||||
case Algorithm.ES512:
|
case Algorithm.ES512:
|
||||||
|
case JWEConstants.RSA1_5:
|
||||||
|
case JWEConstants.RSA_OAEP:
|
||||||
ret = true;
|
ret = true;
|
||||||
}
|
}
|
||||||
return ret;
|
return ret;
|
||||||
|
|
|
@ -1236,7 +1236,7 @@ public class OAuthClient {
|
||||||
KeyWrapper key = new KeyWrapper();
|
KeyWrapper key = new KeyWrapper();
|
||||||
key.setKid(k.getKeyId());
|
key.setKid(k.getKeyId());
|
||||||
key.setAlgorithm(k.getAlgorithm());
|
key.setAlgorithm(k.getAlgorithm());
|
||||||
key.setVerifyKey(publicKey);
|
key.setPublicKey(publicKey);
|
||||||
key.setUse(KeyUse.SIG);
|
key.setUse(KeyUse.SIG);
|
||||||
|
|
||||||
return key;
|
return key;
|
||||||
|
|
|
@ -75,6 +75,20 @@ public class TokenSignatureUtil {
|
||||||
clientResource.update(clientRep);
|
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 {
|
public static boolean verifySignature(String sigAlgName, String token, Keycloak adminClient) throws Exception {
|
||||||
PublicKey publicKey = getRealmPublicKey(TEST_REALM_NAME, sigAlgName, adminClient);
|
PublicKey publicKey = getRealmPublicKey(TEST_REALM_NAME, sigAlgName, adminClient);
|
||||||
JWSInput jws = new JWSInput(token);
|
JWSInput jws = new JWSInput(token);
|
||||||
|
|
|
@ -27,6 +27,7 @@ import org.keycloak.client.registration.ClientRegistrationException;
|
||||||
import org.keycloak.client.registration.HttpErrorException;
|
import org.keycloak.client.registration.HttpErrorException;
|
||||||
import org.keycloak.common.util.CollectionUtil;
|
import org.keycloak.common.util.CollectionUtil;
|
||||||
import org.keycloak.events.Errors;
|
import org.keycloak.events.Errors;
|
||||||
|
import org.keycloak.jose.jwe.JWEConstants;
|
||||||
import org.keycloak.jose.jws.Algorithm;
|
import org.keycloak.jose.jws.Algorithm;
|
||||||
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
|
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
|
||||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
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
|
@Test
|
||||||
public void testOIDCEndpointCreateWithSamlClient() throws Exception {
|
public void testOIDCEndpointCreateWithSamlClient() throws Exception {
|
||||||
ClientsResource clientsResource = adminClient.realm(TEST).clients();
|
ClientsResource clientsResource = adminClient.realm(TEST).clients();
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -796,7 +796,7 @@ public class OIDCAdvancedRequestParamsTest extends AbstractTestRealmKeycloakTest
|
||||||
assertEquals("Invalid Request", errorPage.getError());
|
assertEquals("Invalid Request", errorPage.getError());
|
||||||
|
|
||||||
// Generate keypair for client
|
// 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
|
// Verify signed request_uri will fail due to failed signature validation
|
||||||
oidcClientEndpointsResource.setOIDCRequest("test", "test-app", validRedirectUri, "10", Algorithm.RS256.toString());
|
oidcClientEndpointsResource.setOIDCRequest("test", "test-app", validRedirectUri, "10", Algorithm.RS256.toString());
|
||||||
|
|
|
@ -26,6 +26,7 @@ import org.junit.Test;
|
||||||
import org.keycloak.OAuth2Constants;
|
import org.keycloak.OAuth2Constants;
|
||||||
import org.keycloak.broker.provider.util.SimpleHttp;
|
import org.keycloak.broker.provider.util.SimpleHttp;
|
||||||
import org.keycloak.crypto.Algorithm;
|
import org.keycloak.crypto.Algorithm;
|
||||||
|
import org.keycloak.jose.jwe.JWEConstants;
|
||||||
import org.keycloak.jose.jwk.JSONWebKeySet;
|
import org.keycloak.jose.jwk.JSONWebKeySet;
|
||||||
import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory;
|
import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory;
|
||||||
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
|
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
|
||||||
|
@ -126,9 +127,13 @@ public class OIDCWellKnownProviderTest extends AbstractKeycloakTest {
|
||||||
Assert.assertNames(oidcConfig.getSubjectTypesSupported(), "pairwise", "public");
|
Assert.assertNames(oidcConfig.getSubjectTypesSupported(), "pairwise", "public");
|
||||||
|
|
||||||
// Signature algorithms
|
// 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.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.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.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.RS256, Algorithm.RS384, Algorithm.RS512, Algorithm.ES256, Algorithm.ES384, Algorithm.ES512, Algorithm.PS256, Algorithm.PS384, Algorithm.PS512);
|
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
|
// Client authentication
|
||||||
Assert.assertNames(oidcConfig.getTokenEndpointAuthMethodsSupported(), "client_secret_basic", "client_secret_post", "private_key_jwt", "client_secret_jwt");
|
Assert.assertNames(oidcConfig.getTokenEndpointAuthMethodsSupported(), "client_secret_basic", "client_secret_post", "private_key_jwt", "client_secret_jwt");
|
||||||
|
|
|
@ -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.
|
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=ID Token Signature Algorithm
|
||||||
id-token-signed-response-alg.tooltip=JWA algorithm used for signing ID tokens.
|
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=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.
|
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
|
request-object-signature-alg=Request Object Signature Algorithm
|
||||||
|
|
|
@ -1171,6 +1171,8 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, flows, $ro
|
||||||
|
|
||||||
$scope.accessTokenSignedResponseAlg = $scope.client.attributes['access.token.signed.response.alg'];
|
$scope.accessTokenSignedResponseAlg = $scope.client.attributes['access.token.signed.response.alg'];
|
||||||
$scope.idTokenSignedResponseAlg = $scope.client.attributes['id.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'];
|
var attrVal1 = $scope.client.attributes['user.info.response.signature.alg'];
|
||||||
$scope.userInfoSignedResponseAlg = attrVal1==null ? 'unsigned' : attrVal1;
|
$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.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() {
|
$scope.changeUserInfoSignedResponseAlg = function() {
|
||||||
if ($scope.userInfoSignedResponseAlg === 'unsigned') {
|
if ($scope.userInfoSignedResponseAlg === 'unsigned') {
|
||||||
$scope.clientEdit.attributes['user.info.response.signature.alg'] = null;
|
$scope.clientEdit.attributes['user.info.response.signature.alg'] = null;
|
||||||
|
|
|
@ -418,6 +418,36 @@
|
||||||
<kc-tooltip>{{:: 'id-token-signed-response-alg.tooltip' | translate}}</kc-tooltip>
|
<kc-tooltip>{{:: 'id-token-signed-response-alg.tooltip' | translate}}</kc-tooltip>
|
||||||
</div>
|
</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">
|
<div class="form-group clearfix block">
|
||||||
<label class="col-md-2 control-label" for="userInfoSignedResponseAlg">{{:: 'user-info-signed-response-alg' | translate}}</label>
|
<label class="col-md-2 control-label" for="userInfoSignedResponseAlg">{{:: 'user-info-signed-response-alg' | translate}}</label>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
|
|
Loading…
Reference in a new issue