diff --git a/common/src/main/java/org/keycloak/common/util/KeyUtils.java b/common/src/main/java/org/keycloak/common/util/KeyUtils.java index 37e2b2a319..932417e9d9 100644 --- a/common/src/main/java/org/keycloak/common/util/KeyUtils.java +++ b/common/src/main/java/org/keycloak/common/util/KeyUtils.java @@ -40,8 +40,8 @@ public class KeyUtils { private KeyUtils() { } - public static SecretKey loadSecretKey(byte[] secret) { - return new SecretKeySpec(secret, "HmacSHA256"); + public static SecretKey loadSecretKey(byte[] secret, String javaAlgorithmName) { + return new SecretKeySpec(secret, javaAlgorithmName); } public static KeyPair generateRsaKeyPair(int keysize) { diff --git a/common/src/test/java/org/keycloak/common/util/KeyUtilsTest.java b/common/src/test/java/org/keycloak/common/util/KeyUtilsTest.java index 5e0abf560f..29526ea5ca 100644 --- a/common/src/test/java/org/keycloak/common/util/KeyUtilsTest.java +++ b/common/src/test/java/org/keycloak/common/util/KeyUtilsTest.java @@ -16,7 +16,7 @@ public class KeyUtilsTest { byte[] secretBytes = new byte[32]; ThreadLocalRandom.current().nextBytes(secretBytes); SecretKeySpec expected = new SecretKeySpec(secretBytes, "HmacSHA256"); - SecretKey actual = KeyUtils.loadSecretKey(secretBytes); + SecretKey actual = KeyUtils.loadSecretKey(secretBytes, "HmacSHA256"); assertEquals(expected.getAlgorithm(), actual.getAlgorithm()); assertArrayEquals(expected.getEncoded(), actual.getEncoded()); } diff --git a/core/src/main/java/org/keycloak/jose/jwe/JWE.java b/core/src/main/java/org/keycloak/jose/jwe/JWE.java new file mode 100644 index 0000000000..617783edf4 --- /dev/null +++ b/core/src/main/java/org/keycloak/jose/jwe/JWE.java @@ -0,0 +1,197 @@ +/* + * Copyright 2017 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; + +import java.io.IOException; +import java.security.GeneralSecurityException; + +import org.keycloak.common.util.Base64Url; +import org.keycloak.common.util.BouncyIntegration; +import org.keycloak.jose.jwe.alg.JWEAlgorithmProvider; +import org.keycloak.jose.jwe.enc.JWEEncryptionProvider; +import org.keycloak.util.JsonSerialization; + +/** + * @author Marek Posolda + */ +public class JWE { + + static { + BouncyIntegration.init(); + } + + private JWEHeader header; + private String base64Header; + + private JWEKeyStorage keyStorage = new JWEKeyStorage(); + private String base64Cek; + + private byte[] initializationVector; + + private byte[] content; + private byte[] encryptedContent; + + private byte[] authenticationTag; + + public JWE header(JWEHeader header) { + this.header = header; + this.base64Header = null; + return this; + } + + JWEHeader getHeader() { + if (header == null && base64Header != null) { + try { + byte[] decodedHeader = Base64Url.decode(base64Header); + header = JsonSerialization.readValue(decodedHeader, JWEHeader.class); + } catch (IOException ioe) { + throw new RuntimeException(ioe); + } + } + return header; + } + + public String getBase64Header() throws IOException { + if (base64Header == null && header != null) { + byte[] contentBytes = JsonSerialization.writeValueAsBytes(header); + base64Header = Base64Url.encode(contentBytes); + } + return base64Header; + } + + + public JWEKeyStorage getKeyStorage() { + return keyStorage; + } + + + public byte[] getInitializationVector() { + return initializationVector; + } + + + public JWE content(byte[] content) { + this.content = content; + return this; + } + + public byte[] getContent() { + return content; + } + + public byte[] getEncryptedContent() { + return encryptedContent; + } + + + public byte[] getAuthenticationTag() { + return authenticationTag; + } + + + public void setEncryptedContentInfo(byte[] initializationVector, byte[] encryptedContent, byte[] authenticationTag) { + this.initializationVector = initializationVector; + this.encryptedContent = encryptedContent; + this.authenticationTag = authenticationTag; + } + + + public String encodeJwe() throws JWEException { + try { + if (header == null) { + throw new IllegalStateException("Header must be set"); + } + if (content == null) { + throw new IllegalStateException("Content must be set"); + } + + JWEAlgorithmProvider algorithmProvider = JWERegistry.getAlgProvider(header.getAlgorithm()); + if (algorithmProvider == null) { + throw new IllegalArgumentException("No provider for alg '" + header.getAlgorithm() + "'"); + } + + JWEEncryptionProvider encryptionProvider = JWERegistry.getEncProvider(header.getEncryptionAlgorithm()); + if (encryptionProvider == null) { + throw new IllegalArgumentException("No provider for enc '" + header.getAlgorithm() + "'"); + } + + keyStorage.setEncryptionProvider(encryptionProvider); + keyStorage.getCEKKey(JWEKeyStorage.KeyUse.ENCRYPTION, true); // Will generate CEK if it's not already present + + byte[] encodedCEK = algorithmProvider.encodeCek(encryptionProvider, keyStorage, keyStorage.getEncryptionKey()); + base64Cek = Base64Url.encode(encodedCEK); + + encryptionProvider.encodeJwe(this); + + return getEncodedJweString(); + } catch (Exception e) { + throw new JWEException(e); + } + } + + + private String getEncodedJweString() { + StringBuilder builder = new StringBuilder(); + builder.append(base64Header).append(".") + .append(base64Cek).append(".") + .append(Base64Url.encode(initializationVector)).append(".") + .append(Base64Url.encode(encryptedContent)).append(".") + .append(Base64Url.encode(authenticationTag)); + + return builder.toString(); + } + + + public JWE verifyAndDecodeJwe(String jweStr) throws JWEException { + try { + 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(); + 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) { + throw new JWEException(e); + } + } + +} diff --git a/core/src/main/java/org/keycloak/jose/jwe/JWEConstants.java b/core/src/main/java/org/keycloak/jose/jwe/JWEConstants.java new file mode 100644 index 0000000000..d81141dbd1 --- /dev/null +++ b/core/src/main/java/org/keycloak/jose/jwe/JWEConstants.java @@ -0,0 +1,32 @@ +/* + * Copyright 2017 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; + +/** + * @author Marek Posolda + */ +public class JWEConstants { + + public static final String DIR = "dir"; + public static final String A128KW = "A128KW"; + + public static final String A128CBC_HS256 = "A128CBC-HS256"; + public static final String A192CBC_HS384 = "A192CBC-HS384"; + public static final String A256CBC_HS512 = "A256CBC-HS512"; + +} diff --git a/core/src/main/java/org/keycloak/jose/jwe/JWEException.java b/core/src/main/java/org/keycloak/jose/jwe/JWEException.java new file mode 100644 index 0000000000..02768caba1 --- /dev/null +++ b/core/src/main/java/org/keycloak/jose/jwe/JWEException.java @@ -0,0 +1,35 @@ +/* + * Copyright 2017 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; + +/** + * @author Marek Posolda + */ +public class JWEException extends Exception { + + public JWEException(String s) { + super(s); + } + + public JWEException() { + } + + public JWEException(Throwable throwable) { + super(throwable); + } +} diff --git a/core/src/main/java/org/keycloak/jose/jwe/JWEHeader.java b/core/src/main/java/org/keycloak/jose/jwe/JWEHeader.java new file mode 100644 index 0000000000..30b5150a46 --- /dev/null +++ b/core/src/main/java/org/keycloak/jose/jwe/JWEHeader.java @@ -0,0 +1,103 @@ +/* + * Copyright 2017 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; + +import java.io.IOException; +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * @author Marek Posolda + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class JWEHeader implements Serializable { + + @JsonProperty("alg") + private String algorithm; + + @JsonProperty("enc") + private String encryptionAlgorithm; + + @JsonProperty("zip") + private String compressionAlgorithm; + + + @JsonProperty("typ") + private String type; + + @JsonProperty("cty") + private String contentType; + + @JsonProperty("kid") + private String keyId; + + public JWEHeader() { + } + + public JWEHeader(String algorithm, String encryptionAlgorithm, String compressionAlgorithm) { + this.algorithm = algorithm; + this.encryptionAlgorithm = encryptionAlgorithm; + this.compressionAlgorithm = compressionAlgorithm; + } + + public String getAlgorithm() { + return algorithm; + } + + public String getEncryptionAlgorithm() { + return encryptionAlgorithm; + } + + public String getCompressionAlgorithm() { + return compressionAlgorithm; + } + + public String getType() { + return type; + } + + public String getContentType() { + return contentType; + } + + public String getKeyId() { + return keyId; + } + + private static final ObjectMapper mapper = new ObjectMapper(); + + static { + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + + } + + public String toString() { + try { + return mapper.writeValueAsString(this); + } catch (IOException e) { + throw new RuntimeException(e); + } + + + } + +} diff --git a/core/src/main/java/org/keycloak/jose/jwe/JWEKeyStorage.java b/core/src/main/java/org/keycloak/jose/jwe/JWEKeyStorage.java new file mode 100644 index 0000000000..9ce042ce03 --- /dev/null +++ b/core/src/main/java/org/keycloak/jose/jwe/JWEKeyStorage.java @@ -0,0 +1,103 @@ +/* + * Copyright 2017 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; + +import java.security.Key; +import java.util.HashMap; +import java.util.Map; + +import org.keycloak.jose.jwe.enc.JWEEncryptionProvider; + +/** + * @author Marek Posolda + */ +public class JWEKeyStorage { + + private Key encryptionKey; + + private byte[] cekBytes; + + private Map decodedCEK = new HashMap<>(); + + private JWEEncryptionProvider encryptionProvider; + + + public Key getEncryptionKey() { + return encryptionKey; + } + + public JWEKeyStorage setEncryptionKey(Key encryptionKey) { + this.encryptionKey = encryptionKey; + return this; + } + + + public void setCEKBytes(byte[] cekBytes) { + this.cekBytes = cekBytes; + } + + public byte[] getCekBytes() { + if (cekBytes == null) { + cekBytes = encryptionProvider.serializeCEK(this); + } + return cekBytes; + } + + public JWEKeyStorage setCEKKey(Key key, KeyUse keyUse) { + decodedCEK.put(keyUse, key); + return this; + } + + + public Key getCEKKey(KeyUse keyUse, boolean generateIfNotPresent) { + Key key = decodedCEK.get(keyUse); + if (key == null) { + if (encryptionProvider != null) { + + if (cekBytes == null && generateIfNotPresent) { + generateCekBytes(); + } + + if (cekBytes != null) { + encryptionProvider.deserializeCEK(this); + } + } else { + throw new IllegalStateException("encryptionProvider needs to be set"); + } + } + + return decodedCEK.get(keyUse); + } + + + private void generateCekBytes() { + int cekLength = encryptionProvider.getExpectedCEKLength(); + cekBytes = JWEUtils.generateSecret(cekLength); + } + + + public void setEncryptionProvider(JWEEncryptionProvider encryptionProvider) { + this.encryptionProvider = encryptionProvider; + } + + + public enum KeyUse { + ENCRYPTION, + SIGNATURE + } +} diff --git a/core/src/main/java/org/keycloak/jose/jwe/JWERegistry.java b/core/src/main/java/org/keycloak/jose/jwe/JWERegistry.java new file mode 100644 index 0000000000..80aaea5a87 --- /dev/null +++ b/core/src/main/java/org/keycloak/jose/jwe/JWERegistry.java @@ -0,0 +1,66 @@ +/* + * Copyright 2017 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; + +import java.util.HashMap; +import java.util.Map; + +import org.keycloak.jose.jwe.alg.AesKeyWrapAlgorithmProvider; +import org.keycloak.jose.jwe.alg.DirectAlgorithmProvider; +import org.keycloak.jose.jwe.alg.JWEAlgorithmProvider; +import org.keycloak.jose.jwe.enc.AesCbcHmacShaEncryptionProvider; +import org.keycloak.jose.jwe.enc.JWEEncryptionProvider; + +/** + * + * @author Marek Posolda + */ +class JWERegistry { + + // https://tools.ietf.org/html/rfc7518#page-12 + // Registry not pluggable for now. Just supported algorithms included + private static final Map ENC_PROVIDERS = new HashMap<>(); + + // https://tools.ietf.org/html/rfc7518#page-22 + // Registry not pluggable for now. Just supported algorithms included + private static final Map ALG_PROVIDERS = new HashMap<>(); + + + static { + // Provider 'dir' just directly uses encryption keys for encrypt/decrypt content. + ALG_PROVIDERS.put(JWEConstants.DIR, new DirectAlgorithmProvider()); + ALG_PROVIDERS.put(JWEConstants.A128KW, new AesKeyWrapAlgorithmProvider()); + + + ENC_PROVIDERS.put(JWEConstants.A128CBC_HS256, new AesCbcHmacShaEncryptionProvider.Aes128CbcHmacSha256Provider()); + ENC_PROVIDERS.put(JWEConstants.A192CBC_HS384, new AesCbcHmacShaEncryptionProvider.Aes192CbcHmacSha384Provider()); + ENC_PROVIDERS.put(JWEConstants.A256CBC_HS512, new AesCbcHmacShaEncryptionProvider.Aes256CbcHmacSha512Provider()); + } + + + static JWEAlgorithmProvider getAlgProvider(String alg) { + return ALG_PROVIDERS.get(alg); + } + + + static JWEEncryptionProvider getEncProvider(String enc) { + return ENC_PROVIDERS.get(enc); + } + + +} diff --git a/core/src/main/java/org/keycloak/jose/jwe/JWEUtils.java b/core/src/main/java/org/keycloak/jose/jwe/JWEUtils.java new file mode 100644 index 0000000000..d88ec56ac9 --- /dev/null +++ b/core/src/main/java/org/keycloak/jose/jwe/JWEUtils.java @@ -0,0 +1,35 @@ +/* + * Copyright 2017 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; + +import java.security.SecureRandom; + +/** + * @author Marek Posolda + */ +public class JWEUtils { + + private JWEUtils() { + } + + public static byte[] generateSecret(int bytes) { + byte[] buf = new byte[bytes]; + new SecureRandom().nextBytes(buf); + return buf; + } +} diff --git a/core/src/main/java/org/keycloak/jose/jwe/alg/AesKeyWrapAlgorithmProvider.java b/core/src/main/java/org/keycloak/jose/jwe/alg/AesKeyWrapAlgorithmProvider.java new file mode 100644 index 0000000000..0a5a2c5ba4 --- /dev/null +++ b/core/src/main/java/org/keycloak/jose/jwe/alg/AesKeyWrapAlgorithmProvider.java @@ -0,0 +1,52 @@ +/* + * Copyright 2017 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.io.IOException; +import java.security.GeneralSecurityException; +import java.security.Key; + +import org.bouncycastle.crypto.InvalidCipherTextException; +import org.bouncycastle.crypto.Wrapper; +import org.bouncycastle.crypto.engines.AESWrapEngine; +import org.bouncycastle.crypto.params.KeyParameter; +import org.keycloak.jose.jwe.JWEKeyStorage; +import org.keycloak.jose.jwe.enc.JWEEncryptionProvider; + +/** + * @author Marek Posolda + */ +public class AesKeyWrapAlgorithmProvider implements JWEAlgorithmProvider { + + @Override + public byte[] decodeCek(byte[] encodedCek, Key encryptionKey) throws Exception { + Wrapper encrypter = new AESWrapEngine(); + encrypter.init(false, new KeyParameter(encryptionKey.getEncoded())); + return encrypter.unwrap(encodedCek, 0, encodedCek.length); + } + + @Override + public byte[] encodeCek(JWEEncryptionProvider encryptionProvider, JWEKeyStorage keyStorage, Key encryptionKey) throws Exception { + Wrapper encrypter = new AESWrapEngine(); + encrypter.init(true, new KeyParameter(encryptionKey.getEncoded())); + byte[] cekBytes = keyStorage.getCekBytes(); + return encrypter.wrap(cekBytes, 0, cekBytes.length); + } + + +} diff --git a/core/src/main/java/org/keycloak/jose/jwe/alg/DirectAlgorithmProvider.java b/core/src/main/java/org/keycloak/jose/jwe/alg/DirectAlgorithmProvider.java new file mode 100644 index 0000000000..b1dd699821 --- /dev/null +++ b/core/src/main/java/org/keycloak/jose/jwe/alg/DirectAlgorithmProvider.java @@ -0,0 +1,41 @@ +/* + * Copyright 2017 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.io.IOException; +import java.security.GeneralSecurityException; +import java.security.Key; + +import org.keycloak.jose.jwe.JWEKeyStorage; +import org.keycloak.jose.jwe.enc.JWEEncryptionProvider; + +/** + * @author Marek Posolda + */ +public class DirectAlgorithmProvider implements JWEAlgorithmProvider { + + @Override + public byte[] decodeCek(byte[] encodedCek, Key encryptionKey) { + return new byte[0]; + } + + @Override + public byte[] encodeCek(JWEEncryptionProvider encryptionProvider, JWEKeyStorage keyStorage, Key encryptionKey) { + return new byte[0]; + } +} diff --git a/core/src/main/java/org/keycloak/jose/jwe/alg/JWEAlgorithmProvider.java b/core/src/main/java/org/keycloak/jose/jwe/alg/JWEAlgorithmProvider.java new file mode 100644 index 0000000000..caede1319d --- /dev/null +++ b/core/src/main/java/org/keycloak/jose/jwe/alg/JWEAlgorithmProvider.java @@ -0,0 +1,36 @@ +/* + * Copyright 2017 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.io.IOException; +import java.security.GeneralSecurityException; +import java.security.Key; + +import org.keycloak.jose.jwe.JWEKeyStorage; +import org.keycloak.jose.jwe.enc.JWEEncryptionProvider; + +/** + * @author Marek Posolda + */ +public interface JWEAlgorithmProvider { + + byte[] decodeCek(byte[] encodedCek, Key encryptionKey) throws Exception; + + byte[] encodeCek(JWEEncryptionProvider encryptionProvider, JWEKeyStorage keyStorage, Key encryptionKey) throws Exception; + +} diff --git a/core/src/main/java/org/keycloak/jose/jwe/enc/AesCbcHmacShaEncryptionProvider.java b/core/src/main/java/org/keycloak/jose/jwe/enc/AesCbcHmacShaEncryptionProvider.java new file mode 100644 index 0000000000..dcec260137 --- /dev/null +++ b/core/src/main/java/org/keycloak/jose/jwe/enc/AesCbcHmacShaEncryptionProvider.java @@ -0,0 +1,272 @@ +/* + * Copyright 2017 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.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.security.GeneralSecurityException; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.spec.AlgorithmParameterSpec; +import java.util.Arrays; + +import javax.crypto.Cipher; +import javax.crypto.Mac; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +import org.keycloak.jose.jwe.JWE; +import org.keycloak.jose.jwe.JWEKeyStorage; +import org.keycloak.jose.jwe.JWEUtils; + +/** + * @author Marek Posolda + */ +public abstract class AesCbcHmacShaEncryptionProvider implements JWEEncryptionProvider { + + + @Override + public void encodeJwe(JWE jwe) throws IOException, GeneralSecurityException { + + byte[] contentBytes = jwe.getContent(); + + byte[] initializationVector = JWEUtils.generateSecret(16); + + Key aesKey = jwe.getKeyStorage().getCEKKey(JWEKeyStorage.KeyUse.ENCRYPTION, false); + if (aesKey == null) { + throw new IllegalArgumentException("AES CEK key not present"); + } + + Key hmacShaKey = jwe.getKeyStorage().getCEKKey(JWEKeyStorage.KeyUse.SIGNATURE, false); + if (hmacShaKey == null) { + throw new IllegalArgumentException("HMAC 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); + } + + byte[] cipherBytes = encryptBytes(contentBytes, initializationVector, aesKey); + + byte[] aad = jwe.getBase64Header().getBytes("UTF-8"); + byte[] authenticationTag = computeAuthenticationTag(aad, initializationVector, cipherBytes, hmacShaKey); + + jwe.setEncryptedContentInfo(initializationVector, cipherBytes, authenticationTag); + } + + + @Override + public void verifyAndDecodeJwe(JWE jwe) throws IOException, GeneralSecurityException { + Key aesKey = jwe.getKeyStorage().getCEKKey(JWEKeyStorage.KeyUse.ENCRYPTION, false); + if (aesKey == null) { + throw new IllegalArgumentException("AES CEK key not present"); + } + + Key hmacShaKey = jwe.getKeyStorage().getCEKKey(JWEKeyStorage.KeyUse.SIGNATURE, false); + if (hmacShaKey == null) { + throw new IllegalArgumentException("HMAC 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); + } + + byte[] aad = jwe.getBase64Header().getBytes("UTF-8"); + byte[] authenticationTag = computeAuthenticationTag(aad, jwe.getInitializationVector(), jwe.getEncryptedContent(), hmacShaKey); + + byte[] expectedAuthTag = jwe.getAuthenticationTag(); + boolean digitsEqual = MessageDigest.isEqual(expectedAuthTag, authenticationTag); + + if (!digitsEqual) { + throw new IllegalArgumentException("Signature validations failed"); + } + + byte[] contentBytes = decryptBytes(jwe.getEncryptedContent(), jwe.getInitializationVector(), aesKey); + + jwe.content(contentBytes); + } + + + protected abstract int getExpectedAesKeyLength(); + + protected abstract String getHmacShaAlgorithm(); + + protected abstract int getAuthenticationTagLength(); + + + private byte[] encryptBytes(byte[] contentBytes, byte[] ivBytes, Key aesKey) throws GeneralSecurityException { + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding", "BC"); + AlgorithmParameterSpec ivParamSpec = new IvParameterSpec(ivBytes); + cipher.init(Cipher.ENCRYPT_MODE, aesKey, ivParamSpec); + return cipher.doFinal(contentBytes); + } + + + private byte[] decryptBytes(byte[] encryptedBytes, byte[] ivBytes, Key aesKey) throws GeneralSecurityException { + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding", "BC"); + AlgorithmParameterSpec ivParamSpec = new IvParameterSpec(ivBytes); + cipher.init(Cipher.DECRYPT_MODE, aesKey, ivParamSpec); + return cipher.doFinal(encryptedBytes); + } + + + private byte[] computeAuthenticationTag(byte[] aadBytes, byte[] ivBytes, byte[] cipherBytes, Key hmacKeySpec) throws NoSuchAlgorithmException, InvalidKeyException { + // Compute "al" + ByteBuffer b = ByteBuffer.allocate(4); + b.order(ByteOrder.BIG_ENDIAN); // optional, the initial order of a byte buffer is always BIG_ENDIAN. + int aadLengthInBits = aadBytes.length * 8; + b.putInt(aadLengthInBits); + byte[] result1 = b.array(); + byte[] al = new byte[8]; + System.arraycopy(result1, 0, al, 4, 4); + + byte[] concatenatedHmacInput = new byte[aadBytes.length + ivBytes.length + cipherBytes.length + al.length]; + System.arraycopy(aadBytes, 0, concatenatedHmacInput, 0, aadBytes.length); + System.arraycopy(ivBytes, 0, concatenatedHmacInput, aadBytes.length, ivBytes.length ); + System.arraycopy(cipherBytes, 0, concatenatedHmacInput, aadBytes.length + ivBytes.length , cipherBytes.length); + System.arraycopy(al, 0, concatenatedHmacInput, aadBytes.length + ivBytes.length + cipherBytes.length, al.length); + + String hmacShaAlg = getHmacShaAlgorithm(); + Mac macImpl = Mac.getInstance(hmacShaAlg); + macImpl.init(hmacKeySpec); + macImpl.update(concatenatedHmacInput); + byte[] macEncoded = macImpl.doFinal(); + + int authTagLength = getAuthenticationTagLength(); + return Arrays.copyOf(macEncoded, authTagLength); + } + + + @Override + public void deserializeCEK(JWEKeyStorage keyStorage) { + byte[] cekBytes = keyStorage.getCekBytes(); + + int cekLength = getExpectedCEKLength(); + byte[] cekMacKey = Arrays.copyOf(cekBytes, cekLength / 2); + byte[] cekAesKey = Arrays.copyOfRange(cekBytes, cekLength / 2, cekLength); + + SecretKeySpec aesKey = new SecretKeySpec(cekAesKey, "AES"); + SecretKeySpec hmacKey = new SecretKeySpec(cekMacKey, "HMACSHA2"); + + keyStorage.setCEKKey(aesKey, JWEKeyStorage.KeyUse.ENCRYPTION); + keyStorage.setCEKKey(hmacKey, JWEKeyStorage.KeyUse.SIGNATURE); + } + + + @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"); + } + + Key hmacShaKey = keyStorage.getCEKKey(JWEKeyStorage.KeyUse.SIGNATURE, false); + if (hmacShaKey == null) { + throw new IllegalArgumentException("HMAC CEK key not present"); + } + + byte[] hmacBytes = hmacShaKey.getEncoded(); + byte[] aesBytes = aesKey.getEncoded(); + + byte[] result = new byte[hmacBytes.length + aesBytes.length]; + System.arraycopy(hmacBytes, 0, result, 0, hmacBytes.length); + System.arraycopy(aesBytes, 0, result, hmacBytes.length, aesBytes.length); + + return result; + } + + + + public static class Aes128CbcHmacSha256Provider extends AesCbcHmacShaEncryptionProvider { + + @Override + protected int getExpectedAesKeyLength() { + return 16; + } + + @Override + protected String getHmacShaAlgorithm() { + return "HMACSHA256"; + } + + @Override + protected int getAuthenticationTagLength() { + return 16; + } + + @Override + public int getExpectedCEKLength() { + return 32; + } + } + + + public static class Aes192CbcHmacSha384Provider extends AesCbcHmacShaEncryptionProvider { + + @Override + protected int getExpectedAesKeyLength() { + return 24; + } + + @Override + protected String getHmacShaAlgorithm() { + return "HMACSHA384"; + } + + @Override + protected int getAuthenticationTagLength() { + return 24; + } + + @Override + public int getExpectedCEKLength() { + return 48; + } + } + + + public static class Aes256CbcHmacSha512Provider extends AesCbcHmacShaEncryptionProvider { + + @Override + protected int getExpectedAesKeyLength() { + return 32; + } + + @Override + protected String getHmacShaAlgorithm() { + return "HMACSHA512"; + } + + @Override + protected int getAuthenticationTagLength() { + return 32; + } + + @Override + public int getExpectedCEKLength() { + return 64; + } + } + + +} diff --git a/core/src/main/java/org/keycloak/jose/jwe/enc/JWEEncryptionProvider.java b/core/src/main/java/org/keycloak/jose/jwe/enc/JWEEncryptionProvider.java new file mode 100644 index 0000000000..f9e590c8d2 --- /dev/null +++ b/core/src/main/java/org/keycloak/jose/jwe/enc/JWEEncryptionProvider.java @@ -0,0 +1,75 @@ +/* + * Copyright 2017 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.io.IOException; +import java.security.GeneralSecurityException; +import java.security.Key; + +import org.keycloak.jose.jwe.JWE; +import org.keycloak.jose.jwe.JWEKeyStorage; + +/** + * @author Marek Posolda + */ +public interface JWEEncryptionProvider { + + /** + * This method usually has 3 outputs: + * - generated initialization vector + * - encrypted content + * - authenticationTag for MAC validation + * + * It is supposed to call {@link JWE#setEncryptedContentInfo(byte[], byte[], byte[])} after it's finished + * + * @param jwe + * @throws IOException + * @throws GeneralSecurityException + */ + void encodeJwe(JWE jwe) throws Exception; + + + /** + * This method is supposed to verify checksums and decrypt content. Then it needs to call {@link JWE#content(byte[])} after it's finished + * + * @param jwe + * @throws IOException + * @throws GeneralSecurityException + */ + void verifyAndDecodeJwe(JWE jwe) throws Exception; + + + /** + * This method requires that decoded CEK keys are present in the keyStorage.decodedCEK map before it's called + * + * @param keyStorage + * @return + */ + byte[] serializeCEK(JWEKeyStorage keyStorage); + + /** + * This method is supposed to deserialize keys. It requires that {@link JWEKeyStorage#getCekBytes()} is set. After keys are deserialized, + * this method needs to call {@link JWEKeyStorage#setCEKKey(Key, JWEKeyStorage.KeyUse)} according to all uses, which this encryption algorithm requires. + * + * @param keyStorage + */ + void deserializeCEK(JWEKeyStorage keyStorage); + + int getExpectedCEKLength(); + +} diff --git a/core/src/main/java/org/keycloak/jose/jws/AlgorithmType.java b/core/src/main/java/org/keycloak/jose/jws/AlgorithmType.java index 6c8d93f122..236f84c1df 100755 --- a/core/src/main/java/org/keycloak/jose/jws/AlgorithmType.java +++ b/core/src/main/java/org/keycloak/jose/jws/AlgorithmType.java @@ -25,6 +25,7 @@ public enum AlgorithmType { RSA, HMAC, + AES, ECDSA } diff --git a/core/src/main/java/org/keycloak/representations/CodeJWT.java b/core/src/main/java/org/keycloak/representations/CodeJWT.java new file mode 100644 index 0000000000..df43deebf4 --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/CodeJWT.java @@ -0,0 +1,39 @@ +/* + * Copyright 2017 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.representations; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * @author Marek Posolda + */ +public class CodeJWT extends JsonWebToken { + + @JsonProperty("uss") + protected String userSessionId; + + public String getUserSessionId() { + return userSessionId; + } + + public CodeJWT userSessionId(String userSessionId) { + this.userSessionId = userSessionId; + return this; + } + +} diff --git a/core/src/main/java/org/keycloak/util/TokenUtil.java b/core/src/main/java/org/keycloak/util/TokenUtil.java index 5226c6b1b3..742983a71f 100644 --- a/core/src/main/java/org/keycloak/util/TokenUtil.java +++ b/core/src/main/java/org/keycloak/util/TokenUtil.java @@ -18,11 +18,18 @@ package org.keycloak.util; import org.keycloak.OAuth2Constants; +import org.keycloak.jose.jwe.JWE; +import org.keycloak.jose.jwe.JWEConstants; +import org.keycloak.jose.jwe.JWEException; +import org.keycloak.jose.jwe.JWEHeader; +import org.keycloak.jose.jwe.JWEKeyStorage; import org.keycloak.jose.jws.JWSInput; import org.keycloak.jose.jws.JWSInputException; +import org.keycloak.representations.JsonWebToken; import org.keycloak.representations.RefreshToken; import java.io.IOException; +import java.security.Key; /** * @author Marek Posolda @@ -115,4 +122,52 @@ public class TokenUtil { return token.getType().equals(TOKEN_TYPE_OFFLINE); } + + public static String jweDirectEncode(Key aesKey, Key hmacKey, JsonWebToken jwt) throws JWEException { + int keyLength = aesKey.getEncoded().length; + String encAlgorithm; + switch (keyLength) { + case 16: encAlgorithm = JWEConstants.A128CBC_HS256; + break; + case 24: encAlgorithm = JWEConstants.A192CBC_HS384; + break; + case 32: encAlgorithm = JWEConstants.A256CBC_HS512; + break; + default: throw new IllegalArgumentException("Bad size for Encryption key: " + aesKey + ". Valid sizes are 16, 24, 32."); + } + + try { + byte[] contentBytes = JsonSerialization.writeValueAsBytes(jwt); + + JWEHeader jweHeader = new JWEHeader(JWEConstants.DIR, encAlgorithm, null); + JWE jwe = new JWE() + .header(jweHeader) + .content(contentBytes); + + jwe.getKeyStorage() + .setCEKKey(aesKey, JWEKeyStorage.KeyUse.ENCRYPTION) + .setCEKKey(hmacKey, JWEKeyStorage.KeyUse.SIGNATURE); + + return jwe.encodeJwe(); + } catch (IOException ioe) { + throw new JWEException(ioe); + } + } + + + public static T jweDirectVerifyAndDecode(Key aesKey, Key hmacKey, String jweStr, Class expectedClass) throws JWEException { + JWE jwe = new JWE(); + jwe.getKeyStorage() + .setCEKKey(aesKey, JWEKeyStorage.KeyUse.ENCRYPTION) + .setCEKKey(hmacKey, JWEKeyStorage.KeyUse.SIGNATURE); + + jwe.verifyAndDecodeJwe(jweStr); + + try { + return JsonSerialization.readValue(jwe.getContent(), expectedClass); + } catch (IOException ioe) { + throw new JWEException(ioe); + } + } + } diff --git a/core/src/test/java/org/keycloak/jose/JWETest.java b/core/src/test/java/org/keycloak/jose/JWETest.java new file mode 100644 index 0000000000..f97f3c5b9b --- /dev/null +++ b/core/src/test/java/org/keycloak/jose/JWETest.java @@ -0,0 +1,213 @@ +/* + * Copyright 2017 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; + +import java.io.UnsupportedEncodingException; +import java.security.Key; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +import org.junit.Assert; +import org.junit.Test; +import org.keycloak.common.util.Base64Url; +import org.keycloak.jose.jwe.JWE; +import org.keycloak.jose.jwe.JWEConstants; +import org.keycloak.jose.jwe.JWEException; +import org.keycloak.jose.jwe.JWEHeader; +import org.keycloak.jose.jwe.JWEKeyStorage; + +/** + * @author Marek Posolda + */ +public class JWETest { + + private static final String PAYLOAD = "Hello world! How are you man? I hope you are fine. This is some quite a long text, which is much longer than just simple 'Hello World'"; + + private static final byte[] HMAC_SHA256_KEY = new byte[] { 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 13, 14, 15, 16 }; + private static final byte[] AES_128_KEY = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 }; + + private static final byte[] HMAC_SHA512_KEY = new byte[] { 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 13, 14, 15, 16, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 }; + private static final byte[] AES_256_KEY = new byte[] { 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 13, 14, 15, 16, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 }; + + @Test + public void testDirect_Aes128CbcHmacSha256() throws Exception { + SecretKey aesKey = new SecretKeySpec(AES_128_KEY, "AES"); + SecretKey hmacKey = new SecretKeySpec(HMAC_SHA256_KEY, "HMACSHA2"); + + testDirectEncryptAndDecrypt(aesKey, hmacKey, JWEConstants.A128CBC_HS256, PAYLOAD, true); + } + + + // Works just on OpenJDK 8. Other JDKs (IBM, Oracle) have restrictions on maximum key size of AES to be 128 + // @Test + public void testDirect_Aes256CbcHmacSha512() throws Exception { + final SecretKey aesKey = new SecretKeySpec(AES_256_KEY, "AES"); + final SecretKey hmacKey = new SecretKeySpec(HMAC_SHA512_KEY, "HMACSHA2"); + + testDirectEncryptAndDecrypt(aesKey, hmacKey, JWEConstants.A256CBC_HS512, PAYLOAD, true); + } + + + private void testDirectEncryptAndDecrypt(Key aesKey, Key hmacKey, String encAlgorithm, String payload, boolean sysout) throws Exception { + JWEHeader jweHeader = new JWEHeader(JWEConstants.DIR, encAlgorithm, null); + JWE jwe = new JWE() + .header(jweHeader) + .content(payload.getBytes("UTF-8")); + + jwe.getKeyStorage() + .setCEKKey(aesKey, JWEKeyStorage.KeyUse.ENCRYPTION) + .setCEKKey(hmacKey, JWEKeyStorage.KeyUse.SIGNATURE); + + String encodedContent = jwe.encodeJwe(); + + if (sysout) { + System.out.println("Encoded content: " + encodedContent); + System.out.println("Encoded content length: " + encodedContent.length()); + } + + jwe = new JWE(); + jwe.getKeyStorage() + .setCEKKey(aesKey, JWEKeyStorage.KeyUse.ENCRYPTION) + .setCEKKey(hmacKey, JWEKeyStorage.KeyUse.SIGNATURE); + + jwe.verifyAndDecodeJwe(encodedContent); + + String decodedContent = new String(jwe.getContent(), "UTF-8"); + + Assert.assertEquals(payload, decodedContent); + } + + + // @Test + public void testPerfDirect() throws Exception { + int iterations = 50000; + + long start = System.currentTimeMillis(); + for (int i=0 ; iMarek Posolda + */ +public class InfinispanCodeToTokenStoreProvider implements CodeToTokenStoreProvider { + + private final Supplier> codeCache; + private final KeycloakSession session; + + public InfinispanCodeToTokenStoreProvider(KeycloakSession session, Supplier> actionKeyCache) { + this.session = session; + this.codeCache = actionKeyCache; + } + + @Override + public boolean putIfAbsent(UUID codeId) { + ActionTokenValueEntity tokenValue = new ActionTokenValueEntity(null); + + int lifespanInSeconds = session.getContext().getRealm().getAccessCodeLifespan(); + + BasicCache cache = codeCache.get(); + ActionTokenValueEntity existing = cache.putIfAbsent(codeId, tokenValue, lifespanInSeconds, TimeUnit.SECONDS); + return existing == null; + } + + @Override + public void close() { + + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanCodeToTokenStoreProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanCodeToTokenStoreProviderFactory.java new file mode 100644 index 0000000000..3fa49f9ab3 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanCodeToTokenStoreProviderFactory.java @@ -0,0 +1,98 @@ +/* + * Copyright 2017 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.models.sessions.infinispan; + +import java.util.UUID; +import java.util.function.Supplier; + +import org.infinispan.Cache; +import org.infinispan.client.hotrod.Flag; +import org.infinispan.client.hotrod.RemoteCache; +import org.infinispan.commons.api.BasicCache; +import org.jboss.logging.Logger; +import org.keycloak.Config; +import org.keycloak.connections.infinispan.InfinispanConnectionProvider; +import org.keycloak.models.CodeToTokenStoreProvider; +import org.keycloak.models.CodeToTokenStoreProviderFactory; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.sessions.infinispan.entities.ActionTokenValueEntity; +import org.keycloak.models.sessions.infinispan.util.InfinispanUtil; + +/** + * @author Marek Posolda + */ +public class InfinispanCodeToTokenStoreProviderFactory implements CodeToTokenStoreProviderFactory { + + private static final Logger LOG = Logger.getLogger(InfinispanCodeToTokenStoreProviderFactory.class); + + // Reuse "actionTokens" infinispan cache for now + private volatile Supplier> codeCache; + + @Override + public CodeToTokenStoreProvider create(KeycloakSession session) { + lazyInit(session); + return new InfinispanCodeToTokenStoreProvider(session, codeCache); + } + + private void lazyInit(KeycloakSession session) { + if (codeCache == null) { + synchronized (this) { + if (codeCache == null) { + InfinispanConnectionProvider connections = session.getProvider(InfinispanConnectionProvider.class); + Cache cache = connections.getCache(InfinispanConnectionProvider.ACTION_TOKEN_CACHE); + + RemoteCache remoteCache = InfinispanUtil.getRemoteCache(cache); + + if (remoteCache != null) { + LOG.debugf("Having remote stores. Using remote cache '%s' for single-use cache of code", remoteCache.getName()); + this.codeCache = () -> { + // Doing this way as flag is per invocation + return remoteCache.withFlags(Flag.FORCE_RETURN_VALUE); + }; + } else { + LOG.debugf("Not having remote stores. Using normal cache '%s' for single-use cache of code", cache.getName()); + this.codeCache = () -> { + return cache; + }; + } + } + } + } + } + + @Override + public void init(Config.Scope config) { + + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + + } + + @Override + public void close() { + + } + + @Override + public String getId() { + return "infinispan"; + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanKeycloakTransaction.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanKeycloakTransaction.java index 959223c7a2..a86a6605e7 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanKeycloakTransaction.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanKeycloakTransaction.java @@ -88,6 +88,11 @@ public class InfinispanKeycloakTransaction implements KeycloakTransaction { public void execute() { decorateCache(cache).put(key, value); } + + @Override + public String toString() { + return String.format("CacheTaskWithValue: Operation 'put' for key %s", key); + } }); } } @@ -104,6 +109,11 @@ public class InfinispanKeycloakTransaction implements KeycloakTransaction { public void execute() { decorateCache(cache).put(key, value, lifespan, lifespanUnit); } + + @Override + public String toString() { + return String.format("CacheTaskWithValue: Operation 'put' for key %s, lifespan %d TimeUnit %s", key, lifespan, lifespanUnit.toString()); + } }); } } @@ -123,6 +133,11 @@ public class InfinispanKeycloakTransaction implements KeycloakTransaction { throw new IllegalStateException("There is already existing value in cache for key " + key); } } + + @Override + public String toString() { + return String.format("CacheTaskWithValue: Operation 'putIfAbsent' for key %s", key); + } }); } } @@ -142,6 +157,12 @@ public class InfinispanKeycloakTransaction implements KeycloakTransaction { public void execute() { decorateCache(cache).replace(key, value); } + + @Override + public String toString() { + return String.format("CacheTaskWithValue: Operation 'replace' for key %s", key); + } + }); } } @@ -162,7 +183,21 @@ public class InfinispanKeycloakTransaction implements KeycloakTransaction { log.tracev("Adding cache operation: {0} on {1}", CacheOperation.REMOVE, key); Object taskKey = getTaskKey(cache, key); - tasks.put(taskKey, () -> decorateCache(cache).remove(key)); + + // TODO:performance Eventual performance optimization could be to skip "cache.remove" if item was added in this transaction (EG. authenticationSession valid for single request due to automatic SSO login) + tasks.put(taskKey, new CacheTask() { + + @Override + public void execute() { + decorateCache(cache).remove(key); + } + + @Override + public String toString() { + return String.format("CacheTask: Operation 'remove' for key %s", key); + } + + }); } // This is for possibility to lookup for session by id, which was created in this transaction diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/AuthenticatedClientSessionEntity.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/AuthenticatedClientSessionEntity.java index f67d736195..901a3138e3 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/AuthenticatedClientSessionEntity.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/AuthenticatedClientSessionEntity.java @@ -39,7 +39,7 @@ public class AuthenticatedClientSessionEntity implements Serializable { private String authMethod; private String redirectUri; - private int timestamp; + private volatile int timestamp; private String action; private Set roles; diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/KeycloakRemoteStoreConfiguration.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/KeycloakRemoteStoreConfiguration.java index fa9b7db3d0..3f7d258186 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/KeycloakRemoteStoreConfiguration.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/KeycloakRemoteStoreConfiguration.java @@ -48,16 +48,16 @@ public class KeycloakRemoteStoreConfiguration extends RemoteStoreConfiguration { public String useConfigTemplateFromCache() { - return useConfigTemplateFromCache==null ? null : useConfigTemplateFromCache.get(); + return useConfigTemplateFromCache.get(); } public String remoteServers() { - return remoteServers==null ? null : remoteServers.get(); + return remoteServers.get(); } public Boolean sessionCache() { - return sessionCache==null ? false : sessionCache.get(); + return sessionCache.get()==null ? false : sessionCache.get(); } } diff --git a/model/infinispan/src/main/resources/META-INF/services/org.keycloak.models.CodeToTokenStoreProviderFactory b/model/infinispan/src/main/resources/META-INF/services/org.keycloak.models.CodeToTokenStoreProviderFactory new file mode 100644 index 0000000000..e42a1ad1d6 --- /dev/null +++ b/model/infinispan/src/main/resources/META-INF/services/org.keycloak.models.CodeToTokenStoreProviderFactory @@ -0,0 +1,18 @@ +# +# Copyright 2017 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. +# + +org.keycloak.models.sessions.infinispan.InfinispanCodeToTokenStoreProviderFactory \ No newline at end of file diff --git a/server-spi-private/src/main/java/org/keycloak/keys/AesKeyProvider.java b/server-spi-private/src/main/java/org/keycloak/keys/AesKeyProvider.java new file mode 100644 index 0000000000..4f9fcc2b54 --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/keys/AesKeyProvider.java @@ -0,0 +1,34 @@ +/* + * Copyright 2017 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.keys; + +import org.keycloak.jose.jws.AlgorithmType; + +/** + * @author Marek Posolda + */ +public interface AesKeyProvider extends SecretKeyProvider { + + default AlgorithmType getType() { + return AlgorithmType.AES; + } + + default String getJavaAlgorithmName() { + return "AES"; + } +} diff --git a/server-spi-private/src/main/java/org/keycloak/keys/AesKeyProviderFactory.java b/server-spi-private/src/main/java/org/keycloak/keys/AesKeyProviderFactory.java new file mode 100644 index 0000000000..4c359636b6 --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/keys/AesKeyProviderFactory.java @@ -0,0 +1,34 @@ +/* + * Copyright 2017 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.keys; + +import java.util.Collections; +import java.util.Map; + +import org.keycloak.jose.jws.AlgorithmType; + +/** + * @author Marek Posolda + */ +public interface AesKeyProviderFactory extends KeyProviderFactory { + + @Override + default Map getTypeMetadata() { + return Collections.singletonMap("algorithmType", AlgorithmType.AES); + } +} diff --git a/server-spi-private/src/main/java/org/keycloak/keys/HmacKeyProvider.java b/server-spi-private/src/main/java/org/keycloak/keys/HmacKeyProvider.java index 242877455d..a525598fc6 100644 --- a/server-spi-private/src/main/java/org/keycloak/keys/HmacKeyProvider.java +++ b/server-spi-private/src/main/java/org/keycloak/keys/HmacKeyProvider.java @@ -19,34 +19,17 @@ package org.keycloak.keys; import org.keycloak.jose.jws.AlgorithmType; -import javax.crypto.SecretKey; -import java.security.PrivateKey; -import java.security.PublicKey; -import java.security.cert.X509Certificate; -import java.util.List; - /** * @author Stian Thorgersen */ -public interface HmacKeyProvider extends KeyProvider { +public interface HmacKeyProvider extends SecretKeyProvider { default AlgorithmType getType() { return AlgorithmType.HMAC; } - /** - * Return the active secret key, or null if no active key is available. - * - * @return - */ - SecretKey getSecretKey(); - - /** - * Return the secret key for the specified kid, or null if the kid is unknown. - * - * @param kid - * @return - */ - SecretKey getSecretKey(String kid); + default String getJavaAlgorithmName() { + return "HmacSHA256"; + } } diff --git a/server-spi-private/src/main/java/org/keycloak/keys/HmacKeyProviderFactory.java b/server-spi-private/src/main/java/org/keycloak/keys/HmacKeyProviderFactory.java index ba7007b45e..2e91c4c9d9 100644 --- a/server-spi-private/src/main/java/org/keycloak/keys/HmacKeyProviderFactory.java +++ b/server-spi-private/src/main/java/org/keycloak/keys/HmacKeyProviderFactory.java @@ -25,7 +25,7 @@ import java.util.Map; /** * @author Stian Thorgersen */ -public interface HmacKeyProviderFactory extends KeyProviderFactory { +public interface HmacKeyProviderFactory extends KeyProviderFactory { @Override default Map getTypeMetadata() { diff --git a/server-spi-private/src/main/java/org/keycloak/keys/SecretKeyProvider.java b/server-spi-private/src/main/java/org/keycloak/keys/SecretKeyProvider.java new file mode 100644 index 0000000000..a2b25a1c23 --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/keys/SecretKeyProvider.java @@ -0,0 +1,50 @@ +/* + * Copyright 2017 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.keys; + +import javax.crypto.SecretKey; + +/** + * Base for secret key providers (HMAC, AES) + * + * @author Marek Posolda + */ +public interface SecretKeyProvider extends KeyProvider { + + /** + * Return the active secret key, or null if no active key is available. + * + * @return + */ + SecretKey getSecretKey(); + + /** + * Return the secret key for the specified kid, or null if the kid is unknown. + * + * @param kid + * @return + */ + SecretKey getSecretKey(String kid); + + + /** + * Return name of Java (JCA) algorithm of the key. For example: HmacSHA256 + * @return + */ + String getJavaAlgorithmName(); +} diff --git a/server-spi-private/src/main/java/org/keycloak/migration/MigrationModelManager.java b/server-spi-private/src/main/java/org/keycloak/migration/MigrationModelManager.java index 4e3f67607f..5839aa68d2 100755 --- a/server-spi-private/src/main/java/org/keycloak/migration/MigrationModelManager.java +++ b/server-spi-private/src/main/java/org/keycloak/migration/MigrationModelManager.java @@ -36,6 +36,7 @@ import org.keycloak.migration.migrators.MigrateTo3_0_0; import org.keycloak.migration.migrators.MigrateTo3_1_0; import org.keycloak.migration.migrators.MigrateTo3_2_0; import org.keycloak.migration.migrators.MigrateTo3_3_0; +import org.keycloak.migration.migrators.MigrateTo3_4_0; import org.keycloak.migration.migrators.Migration; import org.keycloak.models.KeycloakSession; @@ -64,7 +65,8 @@ public class MigrationModelManager { new MigrateTo3_0_0(), new MigrateTo3_1_0(), new MigrateTo3_2_0(), - new MigrateTo3_3_0() + new MigrateTo3_3_0(), + new MigrateTo3_4_0() }; public static void migrate(KeycloakSession session) { diff --git a/server-spi-private/src/main/java/org/keycloak/migration/migrators/MigrateTo3_4_0.java b/server-spi-private/src/main/java/org/keycloak/migration/migrators/MigrateTo3_4_0.java new file mode 100644 index 0000000000..8c4f930100 --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/migration/migrators/MigrateTo3_4_0.java @@ -0,0 +1,42 @@ +/* + * Copyright 2017 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.migration.migrators; + +import org.keycloak.migration.ModelVersion; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.utils.DefaultKeyProviders; + +/** + * @author Marek Posolda + */ +public class MigrateTo3_4_0 implements Migration { + + public static final ModelVersion VERSION = new ModelVersion("3.4.0"); + + @Override + public void migrate(KeycloakSession session) { + session.realms().getRealms().stream().forEach( + r -> DefaultKeyProviders.createAesProvider(r) + ); + } + + @Override + public ModelVersion getVersion() { + return VERSION; + } +} diff --git a/server-spi-private/src/main/java/org/keycloak/models/ActionTokenStoreProvider.java b/server-spi-private/src/main/java/org/keycloak/models/ActionTokenStoreProvider.java index 4e4a8dbb5f..ba32eaac5f 100644 --- a/server-spi-private/src/main/java/org/keycloak/models/ActionTokenStoreProvider.java +++ b/server-spi-private/src/main/java/org/keycloak/models/ActionTokenStoreProvider.java @@ -22,6 +22,10 @@ import java.util.Map; /** * Internal action token store provider. + * + * It's used for store the details about used action tokens. There is separate provider for OAuth2 codes - {@link CodeToTokenStoreProvider}, + * which may reuse some components (eg. same infinispan cache) + * * @author hmlnarik */ public interface ActionTokenStoreProvider extends Provider { diff --git a/server-spi-private/src/main/java/org/keycloak/models/CodeToTokenStoreProvider.java b/server-spi-private/src/main/java/org/keycloak/models/CodeToTokenStoreProvider.java new file mode 100644 index 0000000000..01b1ada1f7 --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/models/CodeToTokenStoreProvider.java @@ -0,0 +1,34 @@ +/* + * Copyright 2017 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.models; + +import java.util.UUID; + +import org.keycloak.provider.Provider; + +/** + * Provides single-use cache for OAuth2 code parameter. Used to ensure that particular value of code parameter is used once. + * + * For now, it is separate provider as it's a bit different use-case than {@link ActionTokenStoreProvider}, however it may reuse some components (eg. same infinispan cache) + * + * @author Marek Posolda + */ +public interface CodeToTokenStoreProvider extends Provider { + + boolean putIfAbsent(UUID codeId); +} diff --git a/server-spi-private/src/main/java/org/keycloak/models/CodeToTokenStoreProviderFactory.java b/server-spi-private/src/main/java/org/keycloak/models/CodeToTokenStoreProviderFactory.java new file mode 100644 index 0000000000..85b240100d --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/models/CodeToTokenStoreProviderFactory.java @@ -0,0 +1,26 @@ +/* + * Copyright 2017 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.models; + +import org.keycloak.provider.ProviderFactory; + +/** + * @author Marek Posolda + */ +public interface CodeToTokenStoreProviderFactory extends ProviderFactory { +} diff --git a/server-spi-private/src/main/java/org/keycloak/models/CodeToTokenStoreSpi.java b/server-spi-private/src/main/java/org/keycloak/models/CodeToTokenStoreSpi.java new file mode 100644 index 0000000000..e71ade6303 --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/models/CodeToTokenStoreSpi.java @@ -0,0 +1,50 @@ +/* + * Copyright 2017 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.models; + +import org.keycloak.provider.Provider; +import org.keycloak.provider.ProviderFactory; +import org.keycloak.provider.Spi; + +/** + * @author Marek Posolda + */ +public class CodeToTokenStoreSpi implements Spi { + + public static final String NAME = "codeToTokenStore"; + + @Override + public boolean isInternal() { + return true; + } + + @Override + public String getName() { + return NAME; + } + + @Override + public Class getProviderClass() { + return CodeToTokenStoreProvider.class; + } + + @Override + public Class getProviderFactoryClass() { + return CodeToTokenStoreProviderFactory.class; + } +} diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/DefaultKeyProviders.java b/server-spi-private/src/main/java/org/keycloak/models/utils/DefaultKeyProviders.java index e03929201a..fbc01ed27d 100644 --- a/server-spi-private/src/main/java/org/keycloak/models/utils/DefaultKeyProviders.java +++ b/server-spi-private/src/main/java/org/keycloak/models/utils/DefaultKeyProviders.java @@ -41,6 +41,7 @@ public class DefaultKeyProviders { realm.addComponentModel(generated); createSecretProvider(realm); + createAesProvider(realm); } public static void createSecretProvider(RealmModel realm) { @@ -57,6 +58,20 @@ public class DefaultKeyProviders { realm.addComponentModel(generated); } + public static void createAesProvider(RealmModel realm) { + ComponentModel generated = new ComponentModel(); + generated.setName("aes-generated"); + generated.setParentId(realm.getId()); + generated.setProviderId("aes-generated"); + generated.setProviderType(KeyProvider.class.getName()); + + MultivaluedHashMap config = new MultivaluedHashMap<>(); + config.putSingle("priority", "100"); + generated.setConfig(config); + + realm.addComponentModel(generated); + } + public static void createProviders(RealmModel realm, String privateKeyPem, String certificatePem) { ComponentModel rsa = new ComponentModel(); rsa.setName("rsa"); @@ -75,6 +90,7 @@ public class DefaultKeyProviders { realm.addComponentModel(rsa); createSecretProvider(realm); + createAesProvider(realm); } } diff --git a/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi index 543ef256c1..14134656b7 100755 --- a/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi +++ b/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi @@ -20,6 +20,7 @@ org.keycloak.storage.UserStorageProviderSpi org.keycloak.storage.federated.UserFederatedStorageProviderSpi org.keycloak.models.RealmSpi org.keycloak.models.ActionTokenStoreSpi +org.keycloak.models.CodeToTokenStoreSpi org.keycloak.models.UserSessionSpi org.keycloak.models.UserSpi org.keycloak.models.session.UserSessionPersisterSpi diff --git a/server-spi/src/main/java/org/keycloak/keys/HmacKeyMetadata.java b/server-spi/src/main/java/org/keycloak/keys/SecretKeyMetadata.java similarity index 93% rename from server-spi/src/main/java/org/keycloak/keys/HmacKeyMetadata.java rename to server-spi/src/main/java/org/keycloak/keys/SecretKeyMetadata.java index 0942819918..2f89171261 100644 --- a/server-spi/src/main/java/org/keycloak/keys/HmacKeyMetadata.java +++ b/server-spi/src/main/java/org/keycloak/keys/SecretKeyMetadata.java @@ -20,6 +20,6 @@ package org.keycloak.keys; /** * @author Stian Thorgersen */ -public class HmacKeyMetadata extends KeyMetadata { +public class SecretKeyMetadata extends KeyMetadata { } diff --git a/server-spi/src/main/java/org/keycloak/models/KeyManager.java b/server-spi/src/main/java/org/keycloak/models/KeyManager.java index 391646895a..bc47dcbb49 100644 --- a/server-spi/src/main/java/org/keycloak/models/KeyManager.java +++ b/server-spi/src/main/java/org/keycloak/models/KeyManager.java @@ -17,7 +17,7 @@ package org.keycloak.models; -import org.keycloak.keys.HmacKeyMetadata; +import org.keycloak.keys.SecretKeyMetadata; import org.keycloak.keys.RsaKeyMetadata; import javax.crypto.SecretKey; @@ -44,7 +44,13 @@ public interface KeyManager { SecretKey getHmacSecretKey(RealmModel realm, String kid); - List getHmacKeys(RealmModel realm, boolean includeDisabled); + List getHmacKeys(RealmModel realm, boolean includeDisabled); + + ActiveAesKey getActiveAesKey(RealmModel realm); + + SecretKey getAesSecretKey(RealmModel realm, String kid); + + List getAesKeys(RealmModel realm, boolean includeDisabled); class ActiveRsaKey { private final String kid; @@ -94,4 +100,23 @@ public interface KeyManager { } } + class ActiveAesKey { + private final String kid; + private final SecretKey secretKey; + + public ActiveAesKey(String kid, SecretKey secretKey) { + this.kid = kid; + this.secretKey = secretKey; + } + + public String getKid() { + return kid; + } + + public SecretKey getSecretKey() { + return secretKey; + } + } + + } diff --git a/server-spi/src/main/java/org/keycloak/sessions/CommonClientSessionModel.java b/server-spi/src/main/java/org/keycloak/sessions/CommonClientSessionModel.java index 5f913caf41..a87309c7c2 100644 --- a/server-spi/src/main/java/org/keycloak/sessions/CommonClientSessionModel.java +++ b/server-spi/src/main/java/org/keycloak/sessions/CommonClientSessionModel.java @@ -56,7 +56,6 @@ public interface CommonClientSessionModel { public static enum Action { OAUTH_GRANT, - CODE_TO_TOKEN, AUTHENTICATE, LOGGED_OUT, LOGGING_OUT, diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java index af7d2f7a3c..e212c924fe 100755 --- a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java +++ b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java @@ -223,7 +223,7 @@ public class AuthenticationProcessor { public String generateCode() { ClientSessionCode accessCode = new ClientSessionCode(session, getRealm(), getAuthenticationSession()); authenticationSession.setTimestamp(Time.currentTime()); - return accessCode.getCode(); + return accessCode.getOrGenerateCode(); } public EventBuilder newEvent() { diff --git a/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java b/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java index 3afb34ce8e..3ce9768859 100755 --- a/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java +++ b/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java @@ -150,7 +150,7 @@ public class RequiredActionContextResult implements RequiredActionContext { public String generateCode() { ClientSessionCode accessCode = new ClientSessionCode<>(session, getRealm(), getAuthenticationSession()); authenticationSession.setTimestamp(Time.currentTime()); - return accessCode.getCode(); + return accessCode.getOrGenerateCode(); } diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/IdentityProviderAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/IdentityProviderAuthenticator.java index b255acebfe..7910e0f93c 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/IdentityProviderAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/IdentityProviderAuthenticator.java @@ -63,7 +63,7 @@ public class IdentityProviderAuthenticator implements Authenticator { List identityProviders = context.getRealm().getIdentityProviders(); for (IdentityProviderModel identityProvider : identityProviders) { if (identityProvider.isEnabled() && providerId.equals(identityProvider.getAlias())) { - String accessCode = new ClientSessionCode<>(context.getSession(), context.getRealm(), context.getAuthenticationSession()).getCode(); + String accessCode = new ClientSessionCode<>(context.getSession(), context.getRealm(), context.getAuthenticationSession()).getOrGenerateCode(); String clientId = context.getAuthenticationSession().getClient().getClientId(); Response response = Response.seeOther( Urls.identityProviderAuthnRequest(context.getUriInfo().getBaseUri(), providerId, context.getRealm().getName(), accessCode, clientId)) diff --git a/services/src/main/java/org/keycloak/keys/Attributes.java b/services/src/main/java/org/keycloak/keys/Attributes.java index edde62cdfa..8476e5540b 100644 --- a/services/src/main/java/org/keycloak/keys/Attributes.java +++ b/services/src/main/java/org/keycloak/keys/Attributes.java @@ -51,6 +51,8 @@ public interface Attributes { String SECRET_KEY = "secret"; String SECRET_SIZE_KEY = "secretSize"; - ProviderConfigProperty SECRET_SIZE_PROPERTY = new ProviderConfigProperty(SECRET_SIZE_KEY, "Secret size", "Size in bytes for the generated secret", LIST_TYPE, "32", "32", "64", "128", "256", "512"); + ProviderConfigProperty SECRET_SIZE_PROPERTY = new ProviderConfigProperty(SECRET_SIZE_KEY, "Secret size", "Size in bytes for the generated secret", LIST_TYPE, + String.valueOf(GeneratedHmacKeyProviderFactory.DEFAULT_HMAC_KEY_SIZE), + "16", "24", "32", "64", "128", "256", "512"); } diff --git a/services/src/main/java/org/keycloak/keys/DefaultKeyManager.java b/services/src/main/java/org/keycloak/keys/DefaultKeyManager.java index 4f90f2c2ce..6a33293457 100644 --- a/services/src/main/java/org/keycloak/keys/DefaultKeyManager.java +++ b/services/src/main/java/org/keycloak/keys/DefaultKeyManager.java @@ -82,6 +82,23 @@ public class DefaultKeyManager implements KeyManager { throw new RuntimeException("Failed to get keys"); } + @Override + public ActiveAesKey getActiveAesKey(RealmModel realm) { + for (KeyProvider p : getProviders(realm)) { + if (p.getType().equals(AlgorithmType.AES)) { + AesKeyProvider h = (AesKeyProvider) p; + if (h.getKid() != null && h.getSecretKey() != null) { + if (logger.isTraceEnabled()) { + logger.tracev("Active AES Key realm={0} kid={1}", realm.getName(), p.getKid()); + } + String kid = p.getKid(); + return new ActiveAesKey(kid, h.getSecretKey()); + } + } + } + throw new RuntimeException("Failed to get keys"); + } + @Override public PublicKey getRsaPublicKey(RealmModel realm, String kid) { if (kid == null) { @@ -135,7 +152,7 @@ public class DefaultKeyManager implements KeyManager { @Override public SecretKey getHmacSecretKey(RealmModel realm, String kid) { if (kid == null) { - logger.warnv("KID is null, can't find public key", realm.getName(), kid); + logger.warnv("KID is null, can't find secret key", realm.getName(), kid); return null; } @@ -157,6 +174,31 @@ public class DefaultKeyManager implements KeyManager { return null; } + @Override + public SecretKey getAesSecretKey(RealmModel realm, String kid) { + if (kid == null) { + logger.warnv("KID is null, can't find aes key", realm.getName(), kid); + return null; + } + + for (KeyProvider p : getProviders(realm)) { + if (p.getType().equals(AlgorithmType.AES)) { + AesKeyProvider h = (AesKeyProvider) p; + SecretKey s = h.getSecretKey(kid); + if (s != null) { + if (logger.isTraceEnabled()) { + logger.tracev("Found AES key realm={0} kid={1}", realm.getName(), kid); + } + return s; + } + } + } + if (logger.isTraceEnabled()) { + logger.tracev("Failed to find AES key realm={0} kid={1}", realm.getName(), kid); + } + return null; + } + @Override public List getRsaKeys(RealmModel realm, boolean includeDisabled) { List keys = new LinkedList<>(); @@ -174,14 +216,30 @@ public class DefaultKeyManager implements KeyManager { } @Override - public List getHmacKeys(RealmModel realm, boolean includeDisabled) { - List keys = new LinkedList<>(); + public List getHmacKeys(RealmModel realm, boolean includeDisabled) { + List keys = new LinkedList<>(); for (KeyProvider p : getProviders(realm)) { if (p instanceof HmacKeyProvider) { if (includeDisabled) { keys.addAll(p.getKeyMetadata()); } else { - List metadata = p.getKeyMetadata(); + List metadata = p.getKeyMetadata(); + metadata.stream().filter(k -> k.getStatus() != KeyMetadata.Status.DISABLED).forEach(k -> keys.add(k)); + } + } + } + return keys; + } + + @Override + public List getAesKeys(RealmModel realm, boolean includeDisabled) { + List keys = new LinkedList<>(); + for (KeyProvider p : getProviders(realm)) { + if (p instanceof AesKeyProvider) { + if (includeDisabled) { + keys.addAll(p.getKeyMetadata()); + } else { + List metadata = p.getKeyMetadata(); metadata.stream().filter(k -> k.getStatus() != KeyMetadata.Status.DISABLED).forEach(k -> keys.add(k)); } } @@ -199,6 +257,7 @@ public class DefaultKeyManager implements KeyManager { boolean activeRsa = false; boolean activeHmac = false; + boolean activeAes = false; for (ComponentModel c : components) { try { @@ -217,7 +276,13 @@ public class DefaultKeyManager implements KeyManager { if (r.getKid() != null && r.getSecretKey() != null) { activeHmac = true; } + } else if (provider.getType().equals(AlgorithmType.AES)) { + AesKeyProvider r = (AesKeyProvider) provider; + if (r.getKid() != null && r.getSecretKey() != null) { + activeAes = true; + } } + } catch (Throwable t) { logger.errorv(t, "Failed to load provider {0}", c.getId()); } @@ -231,6 +296,10 @@ public class DefaultKeyManager implements KeyManager { providers.add(new FailsafeHmacKeyProvider()); } + if (!activeAes) { + providers.add(new FailsafeAesKeyProvider()); + } + providersMap.put(realm.getId(), providers); } return providers; diff --git a/services/src/main/java/org/keycloak/keys/FailsafeAesKeyProvider.java b/services/src/main/java/org/keycloak/keys/FailsafeAesKeyProvider.java new file mode 100644 index 0000000000..d81a5965b1 --- /dev/null +++ b/services/src/main/java/org/keycloak/keys/FailsafeAesKeyProvider.java @@ -0,0 +1,33 @@ +/* + * Copyright 2017 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.keys; + +import org.jboss.logging.Logger; + +/** + * @author Marek Posolda + */ +public class FailsafeAesKeyProvider extends FailsafeSecretKeyProvider implements AesKeyProvider { + + private static final Logger logger = Logger.getLogger(FailsafeAesKeyProvider.class); + + @Override + protected Logger logger() { + return logger; + } +} diff --git a/services/src/main/java/org/keycloak/keys/FailsafeHmacKeyProvider.java b/services/src/main/java/org/keycloak/keys/FailsafeHmacKeyProvider.java index 37e837b339..676e048074 100644 --- a/services/src/main/java/org/keycloak/keys/FailsafeHmacKeyProvider.java +++ b/services/src/main/java/org/keycloak/keys/FailsafeHmacKeyProvider.java @@ -29,61 +29,12 @@ import java.util.List; /** * @author Stian Thorgersen */ -public class FailsafeHmacKeyProvider implements HmacKeyProvider { +public class FailsafeHmacKeyProvider extends FailsafeSecretKeyProvider implements HmacKeyProvider { private static final Logger logger = Logger.getLogger(FailsafeHmacKeyProvider.class); - private static String KID; - - private static SecretKey KEY; - - private static long EXPIRES; - - private SecretKey key; - - private String kid; - - public FailsafeHmacKeyProvider() { - logger.errorv("No active keys found, using failsafe provider, please login to admin console to add keys. Clustering is not supported."); - - synchronized (FailsafeHmacKeyProvider.class) { - if (EXPIRES < Time.currentTime()) { - KEY = KeyUtils.loadSecretKey(KeycloakModelUtils.generateSecret(32)); - KID = KeycloakModelUtils.generateId(); - EXPIRES = Time.currentTime() + 60 * 10; - - if (EXPIRES > 0) { - logger.warnv("Keys expired, re-generated kid={0}", KID); - } - } - - kid = KID; - key = KEY; - } - } - @Override - public String getKid() { - return kid; + protected Logger logger() { + return logger; } - - @Override - public SecretKey getSecretKey() { - return key; - } - - @Override - public SecretKey getSecretKey(String kid) { - return kid.equals(this.kid) ? key : null; - } - - @Override - public List getKeyMetadata() { - return Collections.emptyList(); - } - - @Override - public void close() { - } - } diff --git a/services/src/main/java/org/keycloak/keys/FailsafeSecretKeyProvider.java b/services/src/main/java/org/keycloak/keys/FailsafeSecretKeyProvider.java new file mode 100644 index 0000000000..32be263712 --- /dev/null +++ b/services/src/main/java/org/keycloak/keys/FailsafeSecretKeyProvider.java @@ -0,0 +1,90 @@ +/* + * Copyright 2017 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.keys; + +import java.util.Collections; +import java.util.List; + +import javax.crypto.SecretKey; + +import org.jboss.logging.Logger; +import org.keycloak.common.util.KeyUtils; +import org.keycloak.common.util.Time; +import org.keycloak.models.utils.KeycloakModelUtils; + +/** + * @author Stian Thorgersen + */ +public abstract class FailsafeSecretKeyProvider implements SecretKeyProvider { + + + private static String KID; + + private static SecretKey KEY; + + private static long EXPIRES; + + private SecretKey key; + + private String kid; + + public FailsafeSecretKeyProvider() { + logger().errorv("No active keys found, using failsafe provider, please login to admin console to add keys. Clustering is not supported."); + + synchronized (FailsafeHmacKeyProvider.class) { + if (EXPIRES < Time.currentTime()) { + KEY = KeyUtils.loadSecretKey(KeycloakModelUtils.generateSecret(32), getJavaAlgorithmName()); + KID = KeycloakModelUtils.generateId(); + EXPIRES = Time.currentTime() + 60 * 10; + + if (EXPIRES > 0) { + logger().warnv("Keys expired, re-generated kid={0}", KID); + } + } + + kid = KID; + key = KEY; + } + } + + @Override + public String getKid() { + return kid; + } + + @Override + public SecretKey getSecretKey() { + return key; + } + + @Override + public SecretKey getSecretKey(String kid) { + return kid.equals(this.kid) ? key : null; + } + + @Override + public List getKeyMetadata() { + return Collections.emptyList(); + } + + @Override + public void close() { + } + + protected abstract Logger logger(); +} diff --git a/services/src/main/java/org/keycloak/keys/GeneratedAesKeyProvider.java b/services/src/main/java/org/keycloak/keys/GeneratedAesKeyProvider.java new file mode 100644 index 0000000000..cb07323cf6 --- /dev/null +++ b/services/src/main/java/org/keycloak/keys/GeneratedAesKeyProvider.java @@ -0,0 +1,31 @@ +/* + * Copyright 2017 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.keys; + +import org.keycloak.component.ComponentModel; + +/** + * @author Marek Posolda + */ +public class GeneratedAesKeyProvider extends GeneratedSecretKeyProvider implements AesKeyProvider { + + public GeneratedAesKeyProvider(ComponentModel model) { + super(model); + } + +} diff --git a/services/src/main/java/org/keycloak/keys/GeneratedAesKeyProviderFactory.java b/services/src/main/java/org/keycloak/keys/GeneratedAesKeyProviderFactory.java new file mode 100644 index 0000000000..b9f5a06ba0 --- /dev/null +++ b/services/src/main/java/org/keycloak/keys/GeneratedAesKeyProviderFactory.java @@ -0,0 +1,83 @@ +/* + * Copyright 2017 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.keys; + +import java.util.List; + +import org.jboss.logging.Logger; +import org.keycloak.component.ComponentModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.provider.ProviderConfigProperty; + +import static org.keycloak.provider.ProviderConfigProperty.LIST_TYPE; + +/** + * @author Marek Posolda + */ +public class GeneratedAesKeyProviderFactory extends GeneratedSecretKeyProviderFactory implements AesKeyProviderFactory { + + private static final Logger logger = Logger.getLogger(GeneratedAesKeyProviderFactory.class); + + public static final String ID = "aes-generated"; + + private static final String HELP_TEXT = "Generates AES secret key"; + + private static final ProviderConfigProperty AES_KEY_SIZE_PROPERTY; + + private static final int DEFAULT_AES_KEY_SIZE = 16; + + static { + AES_KEY_SIZE_PROPERTY = new ProviderConfigProperty(Attributes.SECRET_SIZE_KEY, "AES Key size", + "Size in bytes for the generated AES Key. Size 16 is for AES-128, Size 24 for AES-192 and Size 32 for AES-256. WARN: Bigger keys then 128 bits are not allowed on some JDK implementations", + LIST_TYPE, String.valueOf(DEFAULT_AES_KEY_SIZE), "16", "24", "32"); + } + + private static final List CONFIG_PROPERTIES = SecretKeyProviderUtils.configurationBuilder() + .property(AES_KEY_SIZE_PROPERTY) + .build(); + + @Override + public AesKeyProvider create(KeycloakSession session, ComponentModel model) { + return new GeneratedAesKeyProvider(model); + } + + @Override + public String getHelpText() { + return HELP_TEXT; + } + + @Override + public List getConfigProperties() { + return CONFIG_PROPERTIES; + } + + @Override + public String getId() { + return ID; + } + + @Override + protected Logger logger() { + return logger; + } + + @Override + protected int getDefaultKeySize() { + return DEFAULT_AES_KEY_SIZE; + } +} diff --git a/services/src/main/java/org/keycloak/keys/GeneratedHmacKeyProvider.java b/services/src/main/java/org/keycloak/keys/GeneratedHmacKeyProvider.java index a989ac3a11..dfc8fb27b1 100644 --- a/services/src/main/java/org/keycloak/keys/GeneratedHmacKeyProvider.java +++ b/services/src/main/java/org/keycloak/keys/GeneratedHmacKeyProvider.java @@ -17,87 +17,16 @@ package org.keycloak.keys; -import org.keycloak.common.util.Base64Url; -import org.keycloak.common.util.KeyUtils; import org.keycloak.component.ComponentModel; -import org.keycloak.jose.jws.AlgorithmType; -import javax.crypto.SecretKey; -import java.util.Collections; -import java.util.List; /** * @author Stian Thorgersen */ -public class GeneratedHmacKeyProvider implements HmacKeyProvider { - - private final boolean enabled; - - private final boolean active; - - private final ComponentModel model; - private final String kid; - private final SecretKey secretKey; +public class GeneratedHmacKeyProvider extends GeneratedSecretKeyProvider implements HmacKeyProvider { public GeneratedHmacKeyProvider(ComponentModel model) { - this.enabled = model.get(Attributes.ENABLED_KEY, true); - this.active = model.get(Attributes.ACTIVE_KEY, true); - this.kid = model.get(Attributes.KID_KEY); - this.model = model; - - if (model.hasNote(SecretKey.class.getName())) { - secretKey = model.getNote(SecretKey.class.getName()); - } else { - secretKey = KeyUtils.loadSecretKey(Base64Url.decode(model.get(Attributes.SECRET_KEY))); - model.setNote(SecretKey.class.getName(), secretKey); - } - } - - @Override - public SecretKey getSecretKey() { - return isActive() ? secretKey : null; - } - - @Override - public SecretKey getSecretKey(String kid) { - return isEnabled() && kid.equals(this.kid) ? secretKey : null; - } - - @Override - public String getKid() { - return isActive() ? kid : null; - } - - @Override - public List getKeyMetadata() { - if (kid != null && secretKey != null) { - HmacKeyMetadata k = new HmacKeyMetadata(); - k.setProviderId(model.getId()); - k.setProviderPriority(model.get(Attributes.PRIORITY_KEY, 0l)); - k.setKid(kid); - if (isActive()) { - k.setStatus(KeyMetadata.Status.ACTIVE); - } else if (isEnabled()) { - k.setStatus(KeyMetadata.Status.PASSIVE); - } else { - k.setStatus(KeyMetadata.Status.DISABLED); - } - return Collections.singletonList(k); - } else { - return Collections.emptyList(); - } - } - - @Override - public void close() { - } - - private boolean isEnabled() { - return secretKey != null && enabled; - } - - private boolean isActive() { - return isEnabled() && active; + super(model); } } diff --git a/services/src/main/java/org/keycloak/keys/GeneratedHmacKeyProviderFactory.java b/services/src/main/java/org/keycloak/keys/GeneratedHmacKeyProviderFactory.java index 3a725170bf..ab4902ade7 100644 --- a/services/src/main/java/org/keycloak/keys/GeneratedHmacKeyProviderFactory.java +++ b/services/src/main/java/org/keycloak/keys/GeneratedHmacKeyProviderFactory.java @@ -34,7 +34,7 @@ import java.util.List; /** * @author Stian Thorgersen */ -public class GeneratedHmacKeyProviderFactory extends AbstractHmacKeyProviderFactory { +public class GeneratedHmacKeyProviderFactory extends GeneratedSecretKeyProviderFactory implements HmacKeyProviderFactory { private static final Logger logger = Logger.getLogger(GeneratedHmacKeyProviderFactory.class); @@ -42,12 +42,14 @@ public class GeneratedHmacKeyProviderFactory extends AbstractHmacKeyProviderFact private static final String HELP_TEXT = "Generates HMAC secret key"; - private static final List CONFIG_PROPERTIES = AbstractHmacKeyProviderFactory.configurationBuilder() + public static final int DEFAULT_HMAC_KEY_SIZE = 32; + + private static final List CONFIG_PROPERTIES = SecretKeyProviderUtils.configurationBuilder() .property(Attributes.SECRET_SIZE_PROPERTY) .build(); @Override - public KeyProvider create(KeycloakSession session, ComponentModel model) { + public HmacKeyProvider create(KeycloakSession session, ComponentModel model) { return new GeneratedHmacKeyProvider(model); } @@ -61,51 +63,18 @@ public class GeneratedHmacKeyProviderFactory extends AbstractHmacKeyProviderFact return CONFIG_PROPERTIES; } - @Override - public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model) throws ComponentValidationException { - ConfigurationValidationHelper.check(model).checkList(Attributes.SECRET_SIZE_PROPERTY, false); - - int size = model.get(Attributes.SECRET_SIZE_KEY, 32); - - if (!(model.contains(Attributes.SECRET_KEY))) { - generateSecret(model, size); - logger.debugv("Generated secret for {0}", realm.getName()); - } else { - int currentSize = Base64Url.decode(model.get(Attributes.SECRET_KEY)).length; - if (currentSize != size) { - generateSecret(model, size); - logger.debugv("Secret size changed, generating new secret for {0}", realm.getName()); - } - } - } - - private void generateSecret(ComponentModel model, int size) { - try { - byte[] secret = KeycloakModelUtils.generateSecret(size); - model.put(Attributes.SECRET_KEY, Base64Url.encode(secret)); - - String kid = KeycloakModelUtils.generateId(); - model.put(Attributes.KID_KEY, kid); - } catch (Throwable t) { - throw new ComponentValidationException("Failed to generate secret", t); - } - } - - @Override - public void init(Config.Scope config) { - } - - @Override - public void postInit(KeycloakSessionFactory factory) { - } - - @Override - public void close() { - } - @Override public String getId() { return ID; } + @Override + protected Logger logger() { + return logger; + } + + @Override + protected int getDefaultKeySize() { + return DEFAULT_HMAC_KEY_SIZE; + } } diff --git a/services/src/main/java/org/keycloak/keys/GeneratedSecretKeyProvider.java b/services/src/main/java/org/keycloak/keys/GeneratedSecretKeyProvider.java new file mode 100644 index 0000000000..76b7ea8290 --- /dev/null +++ b/services/src/main/java/org/keycloak/keys/GeneratedSecretKeyProvider.java @@ -0,0 +1,102 @@ +/* + * Copyright 2017 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.keys; + +import java.util.Collections; +import java.util.List; + +import javax.crypto.SecretKey; + +import org.keycloak.common.util.Base64Url; +import org.keycloak.common.util.KeyUtils; +import org.keycloak.component.ComponentModel; + +/** + * @author Stian Thorgersen + */ +public abstract class GeneratedSecretKeyProvider implements SecretKeyProvider { + + private final boolean enabled; + + private final boolean active; + + private final ComponentModel model; + private final String kid; + private final SecretKey secretKey; + + public GeneratedSecretKeyProvider(ComponentModel model) { + this.enabled = model.get(Attributes.ENABLED_KEY, true); + this.active = model.get(Attributes.ACTIVE_KEY, true); + this.kid = model.get(Attributes.KID_KEY); + this.model = model; + + if (model.hasNote(SecretKey.class.getName())) { + secretKey = model.getNote(SecretKey.class.getName()); + } else { + secretKey = KeyUtils.loadSecretKey(Base64Url.decode(model.get(Attributes.SECRET_KEY)), getJavaAlgorithmName()); + model.setNote(SecretKey.class.getName(), secretKey); + } + } + + @Override + public SecretKey getSecretKey() { + return isActive() ? secretKey : null; + } + + @Override + public SecretKey getSecretKey(String kid) { + return isEnabled() && kid.equals(this.kid) ? secretKey : null; + } + + @Override + public String getKid() { + return isActive() ? kid : null; + } + + @Override + public List getKeyMetadata() { + if (kid != null && secretKey != null) { + SecretKeyMetadata k = new SecretKeyMetadata(); + k.setProviderId(model.getId()); + k.setProviderPriority(model.get(Attributes.PRIORITY_KEY, 0l)); + k.setKid(kid); + if (isActive()) { + k.setStatus(KeyMetadata.Status.ACTIVE); + } else if (isEnabled()) { + k.setStatus(KeyMetadata.Status.PASSIVE); + } else { + k.setStatus(KeyMetadata.Status.DISABLED); + } + return Collections.singletonList(k); + } else { + return Collections.emptyList(); + } + } + + @Override + public void close() { + } + + private boolean isEnabled() { + return secretKey != null && enabled; + } + + private boolean isActive() { + return isEnabled() && active; + } +} diff --git a/services/src/main/java/org/keycloak/keys/GeneratedSecretKeyProviderFactory.java b/services/src/main/java/org/keycloak/keys/GeneratedSecretKeyProviderFactory.java new file mode 100644 index 0000000000..3211cfc62e --- /dev/null +++ b/services/src/main/java/org/keycloak/keys/GeneratedSecretKeyProviderFactory.java @@ -0,0 +1,82 @@ +/* + * Copyright 2017 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.keys; + +import org.jboss.logging.Logger; +import org.keycloak.Config; +import org.keycloak.common.util.Base64Url; +import org.keycloak.component.ComponentModel; +import org.keycloak.component.ComponentValidationException; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.RealmModel; +import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.provider.ConfigurationValidationHelper; + +/** + * @author Stian Thorgersen + */ +public abstract class GeneratedSecretKeyProviderFactory implements KeyProviderFactory { + + @Override + public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model) throws ComponentValidationException { + ConfigurationValidationHelper validation = SecretKeyProviderUtils.validateConfiguration(model); + validation.checkList(Attributes.SECRET_SIZE_PROPERTY, false); + + int size = model.get(Attributes.SECRET_SIZE_KEY, getDefaultKeySize()); + + if (!(model.contains(Attributes.SECRET_KEY))) { + generateSecret(model, size); + logger().debugv("Generated secret for {0}", realm.getName()); + } else { + int currentSize = Base64Url.decode(model.get(Attributes.SECRET_KEY)).length; + if (currentSize != size) { + generateSecret(model, size); + logger().debugv("Secret size changed, generating new secret for {0}", realm.getName()); + } + } + } + + private void generateSecret(ComponentModel model, int size) { + try { + byte[] secret = KeycloakModelUtils.generateSecret(size); + model.put(Attributes.SECRET_KEY, Base64Url.encode(secret)); + + String kid = KeycloakModelUtils.generateId(); + model.put(Attributes.KID_KEY, kid); + } catch (Throwable t) { + throw new ComponentValidationException("Failed to generate secret", t); + } + } + + protected abstract Logger logger(); + + @Override + public void init(Config.Scope config) { + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + } + + @Override + public void close() { + } + + protected abstract int getDefaultKeySize(); +} diff --git a/services/src/main/java/org/keycloak/keys/AbstractHmacKeyProviderFactory.java b/services/src/main/java/org/keycloak/keys/SecretKeyProviderUtils.java similarity index 80% rename from services/src/main/java/org/keycloak/keys/AbstractHmacKeyProviderFactory.java rename to services/src/main/java/org/keycloak/keys/SecretKeyProviderUtils.java index f8032b3baa..1c30f5896e 100644 --- a/services/src/main/java/org/keycloak/keys/AbstractHmacKeyProviderFactory.java +++ b/services/src/main/java/org/keycloak/keys/SecretKeyProviderUtils.java @@ -27,18 +27,17 @@ import org.keycloak.provider.ProviderConfigurationBuilder; /** * @author Stian Thorgersen */ -public abstract class AbstractHmacKeyProviderFactory implements HmacKeyProviderFactory { +public abstract class SecretKeyProviderUtils { - public final static ProviderConfigurationBuilder configurationBuilder() { + public static ProviderConfigurationBuilder configurationBuilder() { return ProviderConfigurationBuilder.create() .property(Attributes.PRIORITY_PROPERTY) .property(Attributes.ENABLED_PROPERTY) .property(Attributes.ACTIVE_PROPERTY); } - @Override - public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model) throws ComponentValidationException { - ConfigurationValidationHelper.check(model) + public static ConfigurationValidationHelper validateConfiguration(ComponentModel model) throws ComponentValidationException { + return ConfigurationValidationHelper.check(model) .checkLong(Attributes.PRIORITY_PROPERTY, false) .checkBoolean(Attributes.ENABLED_PROPERTY, false) .checkBoolean(Attributes.ACTIVE_PROPERTY, false); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java index 13d24a7926..91405d34fd 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java @@ -29,7 +29,6 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserSessionModel; import org.keycloak.protocol.LoginProtocol; -import org.keycloak.protocol.RestartLoginCookie; import org.keycloak.protocol.oidc.utils.OIDCRedirectUriBuilder; import org.keycloak.protocol.oidc.utils.OIDCResponseMode; import org.keycloak.protocol.oidc.utils.OIDCResponseType; @@ -39,7 +38,6 @@ import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationSessionManager; import org.keycloak.services.managers.ClientSessionCode; import org.keycloak.services.managers.ResourceAdminManager; -import org.keycloak.sessions.CommonClientSessionModel; import org.keycloak.sessions.AuthenticationSessionModel; import org.keycloak.util.TokenUtil; @@ -185,9 +183,10 @@ public class OIDCLoginProtocol implements LoginProtocol { redirectUri.addParam(OAuth2Constants.STATE, state); // Standard or hybrid flow + String code = null; if (responseType.hasResponseType(OIDCResponseType.CODE)) { - accessCode.setAction(CommonClientSessionModel.Action.CODE_TO_TOKEN.name()); - redirectUri.addParam(OAuth2Constants.CODE, accessCode.getCode()); + code = accessCode.getOrGenerateCode(); + redirectUri.addParam(OAuth2Constants.CODE, code); } // Implicit or hybrid flow @@ -205,7 +204,7 @@ public class OIDCLoginProtocol implements LoginProtocol { } if (responseType.hasResponseType(OIDCResponseType.CODE)) { - responseBuilder.generateCodeHash(accessCode.getCode()); + responseBuilder.generateCodeHash(code); } } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java index a28f283819..42b42d95f4 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java @@ -245,35 +245,27 @@ public class TokenEndpoint { throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Missing parameter: " + OAuth2Constants.CODE, Response.Status.BAD_REQUEST); } - String[] parts = code.split("\\."); - if (parts.length == 4) { - event.detail(Details.CODE_ID, parts[2]); - } - - ClientSessionCode.ParseResult parseResult = ClientSessionCode.parseResult(code, session, realm, AuthenticatedClientSessionModel.class); + ClientSessionCode.ParseResult parseResult = ClientSessionCode.parseResult(code, session, realm, event, AuthenticatedClientSessionModel.class); if (parseResult.isAuthSessionNotFound() || parseResult.isIllegalHash()) { - event.error(Errors.INVALID_CODE); + AuthenticatedClientSessionModel clientSession = parseResult.getClientSession(); // Attempt to use same code twice should invalidate existing clientSession - AuthenticatedClientSessionModel clientSession = parseResult.getClientSession(); if (clientSession != null) { clientSession.setUserSession(null); } + event.error(Errors.INVALID_CODE); + throw new ErrorResponseException(OAuthErrorException.INVALID_GRANT, "Code not valid", Response.Status.BAD_REQUEST); } AuthenticatedClientSessionModel clientSession = parseResult.getClientSession(); - if (!parseResult.getCode().isValid(AuthenticatedClientSessionModel.Action.CODE_TO_TOKEN.name(), ClientSessionCode.ActionType.CLIENT)) { - event.error(Errors.INVALID_CODE); + if (parseResult.isExpiredToken()) { + event.error(Errors.EXPIRED_CODE); throw new ErrorResponseException(OAuthErrorException.INVALID_GRANT, "Code is expired", Response.Status.BAD_REQUEST); } - // TODO: This shouldn't be needed to write into the AuthenticatedClientSessionModel itself - parseResult.getCode().setAction(null); - - // TODO: Maybe rather create userSession even at this stage? UserSessionModel userSession = clientSession.getUserSession(); if (userSession == null) { @@ -281,20 +273,20 @@ public class TokenEndpoint { throw new ErrorResponseException(OAuthErrorException.INVALID_GRANT, "User session not found", Response.Status.BAD_REQUEST); } + UserModel user = userSession.getUser(); if (user == null) { event.error(Errors.USER_NOT_FOUND); throw new ErrorResponseException(OAuthErrorException.INVALID_GRANT, "User not found", Response.Status.BAD_REQUEST); } + + event.user(userSession.getUser()); + if (!user.isEnabled()) { event.error(Errors.USER_DISABLED); throw new ErrorResponseException(OAuthErrorException.INVALID_GRANT, "User disabled", Response.Status.BAD_REQUEST); } - event.user(userSession.getUser()); - - event.session(userSession.getId()); - String redirectUri = clientSession.getNote(OIDCLoginProtocol.REDIRECT_URI_PARAM); String formParam = formParams.getFirst(OAuth2Constants.REDIRECT_URI); if (redirectUri != null && !redirectUri.equals(formParam)) { diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java index 4f6f4eca80..19c188a008 100755 --- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java +++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java @@ -849,7 +849,7 @@ public class AuthenticationManager { return session.getProvider(LoginFormsProvider.class) .setExecution(execution) - .setClientSessionCode(accessCode.getCode()) + .setClientSessionCode(accessCode.getOrGenerateCode()) .setAccessRequest(realmRoles, resourceRoles, protocolMappers) .createOAuthGrant(); } else { diff --git a/services/src/main/java/org/keycloak/services/managers/ClientSessionCode.java b/services/src/main/java/org/keycloak/services/managers/ClientSessionCode.java index 59158e6c31..503973ee73 100755 --- a/services/src/main/java/org/keycloak/services/managers/ClientSessionCode.java +++ b/services/src/main/java/org/keycloak/services/managers/ClientSessionCode.java @@ -17,19 +17,16 @@ package org.keycloak.services.managers; -import org.jboss.logging.Logger; -import org.keycloak.common.util.Base64Url; import org.keycloak.common.util.Time; +import org.keycloak.events.EventBuilder; import org.keycloak.models.ClientModel; import org.keycloak.models.ClientTemplateModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; -import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.sessions.CommonClientSessionModel; -import java.security.MessageDigest; import java.util.HashSet; import java.util.Set; @@ -39,10 +36,6 @@ import java.util.Set; */ public class ClientSessionCode { - private static final String ACTIVE_CODE = "active_code"; - - private static final Logger logger = Logger.getLogger(ClientSessionCode.class); - private KeycloakSession session; private final RealmModel realm; private final CLIENT_SESSION commonLoginSession; @@ -63,6 +56,7 @@ public class ClientSessionCode ClientSessionCode code; boolean authSessionNotFound; boolean illegalHash; + boolean expiredToken; CLIENT_SESSION clientSession; public ClientSessionCode getCode() { @@ -77,29 +71,39 @@ public class ClientSessionCode return illegalHash; } + public boolean isExpiredToken() { + return expiredToken; + } + public CLIENT_SESSION getClientSession() { return clientSession; } } - public static ParseResult parseResult(String code, KeycloakSession session, RealmModel realm, Class sessionClass) { + public static ParseResult parseResult(String code, KeycloakSession session, RealmModel realm, EventBuilder event, Class sessionClass) { ParseResult result = new ParseResult<>(); if (code == null) { result.illegalHash = true; return result; } try { - result.clientSession = getClientSession(code, session, realm, sessionClass); + CodeGenerateUtil.ClientSessionParser clientSessionParser = CodeGenerateUtil.getParser(sessionClass); + result.clientSession = getClientSession(code, session, realm, event, clientSessionParser); if (result.clientSession == null) { result.authSessionNotFound = true; return result; } - if (!verifyCode(code, result.clientSession)) { + if (!clientSessionParser.verifyCode(session, code, result.clientSession)) { result.illegalHash = true; return result; } + if (clientSessionParser.isExpired(session, code, result.clientSession)) { + result.expiredToken = true; + return result; + } + result.code = new ClientSessionCode(session, realm, result.clientSession); return result; } catch (RuntimeException e) { @@ -108,13 +112,19 @@ public class ClientSessionCode } } - public static CLIENT_SESSION getClientSession(String code, KeycloakSession session, RealmModel realm, Class sessionClass) { - CommonClientSessionModel clientSessionn = CodeGenerateUtil.getParser(sessionClass).parseSession(code, session, realm);; - CLIENT_SESSION clientSession = sessionClass.cast(clientSessionn); - return clientSession; + public static CLIENT_SESSION getClientSession(String code, KeycloakSession session, RealmModel realm, EventBuilder event, Class sessionClass) { + CodeGenerateUtil.ClientSessionParser clientSessionParser = CodeGenerateUtil.getParser(sessionClass); + return getClientSession(code, session, realm, event, clientSessionParser); } + + private static CLIENT_SESSION getClientSession(String code, KeycloakSession session, RealmModel realm, EventBuilder event, + CodeGenerateUtil.ClientSessionParser clientSessionParser) { + return clientSessionParser.parseSession(code, session, realm, event); + } + + public CLIENT_SESSION getClientSession() { return commonLoginSession; } @@ -203,52 +213,9 @@ public class ClientSessionCode commonLoginSession.setTimestamp(Time.currentTime()); } - public String getCode() { + public String getOrGenerateCode() { CodeGenerateUtil.ClientSessionParser parser = CodeGenerateUtil.getParser(commonLoginSession.getClass()); - String nextCode = parser.getNote(commonLoginSession, ACTIVE_CODE); - if (nextCode == null) { - nextCode = generateCode(commonLoginSession); - } else { - logger.debug("Code already generated for session, using same code"); - } - return nextCode; + return parser.retrieveCode(session, commonLoginSession); } - private static String generateCode(CommonClientSessionModel authSession) { - try { - String actionId = Base64Url.encode(KeycloakModelUtils.generateSecret()); - - StringBuilder sb = new StringBuilder(); - sb.append(actionId); - sb.append('.'); - sb.append(authSession.getId()); - - CodeGenerateUtil.ClientSessionParser parser = CodeGenerateUtil.getParser(authSession.getClass()); - - String code = parser.generateCode(authSession, actionId); - parser.setNote(authSession, ACTIVE_CODE, code); - - return code; - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - public static boolean verifyCode(String code, CommonClientSessionModel authSession) { - try { - CodeGenerateUtil.ClientSessionParser parser = CodeGenerateUtil.getParser(authSession.getClass()); - - String activeCode = parser.getNote(authSession, ACTIVE_CODE); - if (activeCode == null) { - logger.debug("Active code not found in client session"); - return false; - } - - parser.removeNote(authSession, ACTIVE_CODE); - - return MessageDigest.isEqual(code.getBytes(), activeCode.getBytes()); - } catch (Exception e) { - throw new RuntimeException(e); - } - } } diff --git a/services/src/main/java/org/keycloak/services/managers/CodeGenerateUtil.java b/services/src/main/java/org/keycloak/services/managers/CodeGenerateUtil.java index 3d0c9ca9fd..70e3f353cf 100644 --- a/services/src/main/java/org/keycloak/services/managers/CodeGenerateUtil.java +++ b/services/src/main/java/org/keycloak/services/managers/CodeGenerateUtil.java @@ -17,16 +17,30 @@ package org.keycloak.services.managers; +import java.security.MessageDigest; import java.util.HashMap; import java.util.Map; +import java.util.UUID; +import java.util.function.Supplier; + +import javax.crypto.SecretKey; import org.jboss.logging.Logger; +import org.keycloak.common.util.Base64Url; +import org.keycloak.common.util.Time; +import org.keycloak.events.Details; +import org.keycloak.events.EventBuilder; +import org.keycloak.jose.jwe.JWEException; import org.keycloak.models.AuthenticatedClientSessionModel; +import org.keycloak.models.CodeToTokenStoreProvider; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserSessionModel; +import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.representations.CodeJWT; import org.keycloak.sessions.CommonClientSessionModel; import org.keycloak.sessions.AuthenticationSessionModel; +import org.keycloak.util.TokenUtil; /** * @@ -36,11 +50,18 @@ class CodeGenerateUtil { private static final Logger logger = Logger.getLogger(CodeGenerateUtil.class); - private static final Map, ClientSessionParser> PARSERS = new HashMap<>(); + private static final String ACTIVE_CODE = "active_code"; + + private static final Map, Supplier> PARSERS = new HashMap<>(); static { - PARSERS.put(AuthenticationSessionModel.class, new AuthenticationSessionModelParser()); - PARSERS.put(AuthenticatedClientSessionModel.class, new AuthenticatedClientSessionModelParser()); + PARSERS.put(AuthenticationSessionModel.class, () -> { + return new AuthenticationSessionModelParser(); + }); + + PARSERS.put(AuthenticatedClientSessionModel.class, () -> { + return new AuthenticatedClientSessionModelParser(); + }); } @@ -48,7 +69,7 @@ class CodeGenerateUtil { static ClientSessionParser getParser(Class clientSessionClass) { for (Class c : PARSERS.keySet()) { if (c.isAssignableFrom(clientSessionClass)) { - return PARSERS.get(c); + return PARSERS.get(c).get(); } } return null; @@ -57,17 +78,15 @@ class CodeGenerateUtil { interface ClientSessionParser { - CS parseSession(String code, KeycloakSession session, RealmModel realm); + CS parseSession(String code, KeycloakSession session, RealmModel realm, EventBuilder event); - String generateCode(CS clientSession, String actionId); + String retrieveCode(KeycloakSession session, CS clientSession); void removeExpiredSession(KeycloakSession session, CS clientSession); - String getNote(CS clientSession, String name); + boolean verifyCode(KeycloakSession session, String code, CS clientSession); - void removeNote(CS clientSession, String name); - - void setNote(CS clientSession, String name, String value); + boolean isExpired(KeycloakSession session, String code, CS clientSession); } @@ -78,95 +97,149 @@ class CodeGenerateUtil { private static class AuthenticationSessionModelParser implements ClientSessionParser { @Override - public AuthenticationSessionModel parseSession(String code, KeycloakSession session, RealmModel realm) { + public AuthenticationSessionModel parseSession(String code, KeycloakSession session, RealmModel realm, EventBuilder event) { // Read authSessionID from cookie. Code is ignored for now return new AuthenticationSessionManager(session).getCurrentAuthenticationSession(realm); } @Override - public String generateCode(AuthenticationSessionModel clientSession, String actionId) { - return actionId; + public String retrieveCode(KeycloakSession session, AuthenticationSessionModel authSession) { + String nextCode = authSession.getAuthNote(ACTIVE_CODE); + if (nextCode == null) { + String actionId = Base64Url.encode(KeycloakModelUtils.generateSecret()); + authSession.setAuthNote(ACTIVE_CODE, actionId); + nextCode = actionId; + } else { + logger.debug("Code already generated for authentication session, using same code"); + } + + return nextCode; } + @Override public void removeExpiredSession(KeycloakSession session, AuthenticationSessionModel clientSession) { new AuthenticationSessionManager(session).removeAuthenticationSession(clientSession.getRealm(), clientSession, true); } - @Override - public String getNote(AuthenticationSessionModel clientSession, String name) { - return clientSession.getAuthNote(name); - } @Override - public void removeNote(AuthenticationSessionModel clientSession, String name) { - clientSession.removeAuthNote(name); + public boolean verifyCode(KeycloakSession session, String code, AuthenticationSessionModel authSession) { + String activeCode = authSession.getAuthNote(ACTIVE_CODE); + if (activeCode == null) { + logger.debug("Active code not found in authentication session"); + return false; + } + + authSession.removeAuthNote(ACTIVE_CODE); + + return MessageDigest.isEqual(code.getBytes(), activeCode.getBytes()); } + @Override - public void setNote(AuthenticationSessionModel clientSession, String name, String value) { - clientSession.setAuthNote(name, value); + public boolean isExpired(KeycloakSession session, String code, AuthenticationSessionModel clientSession) { + return false; } } private static class AuthenticatedClientSessionModelParser implements ClientSessionParser { + private CodeJWT codeJWT; + @Override - public AuthenticatedClientSessionModel parseSession(String code, KeycloakSession session, RealmModel realm) { + public AuthenticatedClientSessionModel parseSession(String code, KeycloakSession session, RealmModel realm, EventBuilder event) { + SecretKey aesKey = session.keys().getActiveAesKey(realm).getSecretKey(); + SecretKey hmacKey = session.keys().getActiveHmacKey(realm).getSecretKey(); + try { - String[] parts = code.split("\\."); - String userSessionId = parts[2]; - String clientUUID = parts[3]; - - UserSessionModel userSession = new UserSessionCrossDCManager(session).getUserSessionWithClientAndCodeToTokenAction(realm, userSessionId, clientUUID); - if (userSession == null) { - // TODO:mposolda Temporary workaround needed to track if code is invalid or was already used. Will be good to remove once used OAuth codes are tracked through one-time cache - userSession = session.sessions().getUserSession(realm, userSessionId); - if (userSession == null) { - return null; - } - } - - return userSession.getAuthenticatedClientSessions().get(clientUUID); - } catch (ArrayIndexOutOfBoundsException e) { + codeJWT = TokenUtil.jweDirectVerifyAndDecode(aesKey, hmacKey, code, CodeJWT.class); + } catch (JWEException jweException) { + logger.error("Exception during JWE Verification or decode", jweException); return null; } + + event.detail(Details.CODE_ID, codeJWT.getUserSessionId()); + event.session(codeJWT.getUserSessionId()); + + UserSessionModel userSession = new UserSessionCrossDCManager(session).getUserSessionWithClient(realm, codeJWT.getUserSessionId(), codeJWT.getIssuedFor()); + if (userSession == null) { + // TODO:mposolda Temporary workaround needed to track if code is invalid or was already used. Will be good to remove once used OAuth codes are tracked through one-time cache + userSession = session.sessions().getUserSession(realm, codeJWT.getUserSessionId()); + if (userSession == null) { + return null; + } + } + + return userSession.getAuthenticatedClientSessions().get(codeJWT.getIssuedFor()); + + } + + + @Override + public String retrieveCode(KeycloakSession session, AuthenticatedClientSessionModel clientSession) { + String actionId = KeycloakModelUtils.generateId(); + + CodeJWT codeJWT = new CodeJWT(); + codeJWT.id(actionId); + codeJWT.issuedFor(clientSession.getClient().getId()); + codeJWT.userSessionId(clientSession.getUserSession().getId()); + + RealmModel realm = clientSession.getRealm(); + + int issuedAt = Time.currentTime(); + codeJWT.issuedAt(issuedAt); + codeJWT.expiration(issuedAt + realm.getAccessCodeLifespan()); + + SecretKey aesKey = session.keys().getActiveAesKey(realm).getSecretKey(); + SecretKey hmacKey = session.keys().getActiveHmacKey(realm).getSecretKey(); + + if (logger.isTraceEnabled()) { + logger.tracef("Using AES key of length '%d' bytes and HMAC key of length '%d' bytes . Client: '%s', User Session: '%s'", aesKey.getEncoded().length, + hmacKey.getEncoded().length, clientSession.getClient().getClientId(), clientSession.getUserSession().getId()); + } + + try { + return TokenUtil.jweDirectEncode(aesKey, hmacKey, codeJWT); + } catch (JWEException jweEx) { + throw new RuntimeException(jweEx); + } } - @Override - public String generateCode(AuthenticatedClientSessionModel clientSession, String actionId) { - String userSessionId = clientSession.getUserSession().getId(); - String clientUUID = clientSession.getClient().getId(); - StringBuilder sb = new StringBuilder(); - sb.append("uss."); - sb.append(actionId); - sb.append('.'); - sb.append(userSessionId); - sb.append('.'); - sb.append(clientUUID); - return sb.toString(); + @Override + public boolean verifyCode(KeycloakSession session, String code, AuthenticatedClientSessionModel clientSession) { + if (codeJWT == null) { + throw new IllegalStateException("Illegal use. codeJWT not yet set"); + } + + UUID codeId = UUID.fromString(codeJWT.getId()); + CodeToTokenStoreProvider singleUseCache = session.getProvider(CodeToTokenStoreProvider.class); + + if (singleUseCache.putIfAbsent(codeId)) { + + if (logger.isTraceEnabled()) { + logger.tracef("Added code '%s' to single-use cache. User session: %s, client: %s", codeJWT.getId(), codeJWT.getUserSessionId(), codeJWT.getIssuedFor()); + } + + return true; + } else { + logger.warnf("Code '%s' already used for userSession '%s' and client '%s'.", codeJWT.getId(), codeJWT.getUserSessionId(), codeJWT.getIssuedFor()); + return false; + } } + @Override public void removeExpiredSession(KeycloakSession session, AuthenticatedClientSessionModel clientSession) { throw new IllegalStateException("Not yet implemented"); } - @Override - public String getNote(AuthenticatedClientSessionModel clientSession, String name) { - return clientSession.getNote(name); - } @Override - public void removeNote(AuthenticatedClientSessionModel clientSession, String name) { - clientSession.removeNote(name); - } - - @Override - public void setNote(AuthenticatedClientSessionModel clientSession, String name, String value) { - clientSession.setNote(name, value); + public boolean isExpired(KeycloakSession session, String code, AuthenticatedClientSessionModel clientSession) { + return !codeJWT.isActive(); } } diff --git a/services/src/main/java/org/keycloak/services/managers/UserSessionCrossDCManager.java b/services/src/main/java/org/keycloak/services/managers/UserSessionCrossDCManager.java index 11795e5456..de2516fefc 100644 --- a/services/src/main/java/org/keycloak/services/managers/UserSessionCrossDCManager.java +++ b/services/src/main/java/org/keycloak/services/managers/UserSessionCrossDCManager.java @@ -46,19 +46,14 @@ public class UserSessionCrossDCManager { } - // get userSession if it has "authenticatedClientSession" of specified client attached to it and there is "CODE_TO_TOKEN" action. Otherwise download it from remoteCache + // get userSession if it has "authenticatedClientSession" of specified client attached to it. Otherwise download it from remoteCache // TODO Probably remove this method once AuthenticatedClientSession.getAction is removed and information is moved to OAuth code JWT instead - public UserSessionModel getUserSessionWithClientAndCodeToTokenAction(RealmModel realm, String id, String clientUUID) { + public UserSessionModel getUserSessionWithClient(RealmModel realm, String id, String clientUUID) { return kcSession.sessions().getUserSessionWithPredicate(realm, id, false, (UserSessionModel userSession) -> { Map authSessions = userSession.getAuthenticatedClientSessions(); - if (!authSessions.containsKey(clientUUID)) { - return false; - } - - AuthenticatedClientSessionModel authSession = authSessions.get(clientUUID); - return CommonClientSessionModel.Action.CODE_TO_TOKEN.toString().equals(authSession.getAction()); + return authSessions.containsKey(clientUUID); }); } diff --git a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java index 78fadf596f..20e23de99b 100755 --- a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java +++ b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java @@ -297,7 +297,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal ClientSessionCode clientSessionCode = new ClientSessionCode<>(session, realmModel, authSession); clientSessionCode.setAction(AuthenticationSessionModel.Action.AUTHENTICATE.name()); - clientSessionCode.getCode(); + clientSessionCode.getOrGenerateCode(); authSession.setProtocol(client.getProtocol()); authSession.setRedirectUri(redirectUri); authSession.setClientNote(OIDCLoginProtocol.STATE_PARAM, UUID.randomUUID().toString()); @@ -1046,7 +1046,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal if (clientSessionCode != null) { authSession = clientSessionCode.getClientSession(); - String relayState = clientSessionCode.getCode(); + String relayState = clientSessionCode.getOrGenerateCode(); encodedState = IdentityBrokerState.decoded(relayState, authSession.getClient().getClientId()); } diff --git a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java index f6dd8a43ab..c4ba764843 100755 --- a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java +++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java @@ -740,8 +740,8 @@ public class LoginActionsService { authSession.setTimestamp(Time.currentTime()); String clientId = authSession.getClient().getClientId(); - URI redirect = firstBrokerLogin ? Urls.identityProviderAfterFirstBrokerLogin(uriInfo.getBaseUri(), realm.getName(), accessCode.getCode(), clientId) : - Urls.identityProviderAfterPostBrokerLogin(uriInfo.getBaseUri(), realm.getName(), accessCode.getCode(), clientId) ; + URI redirect = firstBrokerLogin ? Urls.identityProviderAfterFirstBrokerLogin(uriInfo.getBaseUri(), realm.getName(), accessCode.getOrGenerateCode(), clientId) : + Urls.identityProviderAfterPostBrokerLogin(uriInfo.getBaseUri(), realm.getName(), accessCode.getOrGenerateCode(), clientId) ; logger.debugf("Redirecting to '%s' ", redirect); return Response.status(302).location(redirect).build(); diff --git a/services/src/main/java/org/keycloak/services/resources/SessionCodeChecks.java b/services/src/main/java/org/keycloak/services/resources/SessionCodeChecks.java index b5011fb2cc..3f68dd3272 100644 --- a/services/src/main/java/org/keycloak/services/resources/SessionCodeChecks.java +++ b/services/src/main/java/org/keycloak/services/resources/SessionCodeChecks.java @@ -133,7 +133,7 @@ public class SessionCodeChecks { } // object retrieve - AuthenticationSessionModel authSession = ClientSessionCode.getClientSession(code, session, realm, AuthenticationSessionModel.class); + AuthenticationSessionModel authSession = ClientSessionCode.getClientSession(code, session, realm, event, AuthenticationSessionModel.class); if (authSession != null) { return authSession; } @@ -240,7 +240,7 @@ public class SessionCodeChecks { return false; } } else { - ClientSessionCode.ParseResult result = ClientSessionCode.parseResult(code, session, realm, AuthenticationSessionModel.class); + ClientSessionCode.ParseResult result = ClientSessionCode.parseResult(code, session, realm, event, AuthenticationSessionModel.class); clientCode = result.getCode(); if (clientCode == null) { diff --git a/services/src/main/java/org/keycloak/services/resources/admin/KeyResource.java b/services/src/main/java/org/keycloak/services/resources/admin/KeyResource.java index d990fd109d..87bb486d4b 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/KeyResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/KeyResource.java @@ -20,7 +20,7 @@ package org.keycloak.services.resources.admin; import org.jboss.resteasy.annotations.cache.NoCache; import org.keycloak.common.util.PemUtils; import org.keycloak.jose.jws.AlgorithmType; -import org.keycloak.keys.HmacKeyMetadata; +import org.keycloak.keys.SecretKeyMetadata; import org.keycloak.keys.RsaKeyMetadata; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeyManager; @@ -65,6 +65,7 @@ public class KeyResource { Map active = new HashMap<>(); active.put(AlgorithmType.RSA.name(), keystore.getActiveRsaKey(realm).getKid()); active.put(AlgorithmType.HMAC.name(), keystore.getActiveHmacKey(realm).getKid()); + active.put(AlgorithmType.AES.name(), keystore.getActiveAesKey(realm).getKid()); keys.setActive(active); List l = new LinkedList<>(); @@ -79,7 +80,7 @@ public class KeyResource { r.setCertificate(PemUtils.encodeCertificate(m.getCertificate())); l.add(r); } - for (HmacKeyMetadata m : session.keys().getHmacKeys(realm, true)) { + for (SecretKeyMetadata m : session.keys().getHmacKeys(realm, true)) { KeysMetadataRepresentation.KeyMetadataRepresentation r = new KeysMetadataRepresentation.KeyMetadataRepresentation(); r.setProviderId(m.getProviderId()); r.setProviderPriority(m.getProviderPriority()); @@ -88,6 +89,15 @@ public class KeyResource { r.setType(AlgorithmType.HMAC.name()); l.add(r); } + for (SecretKeyMetadata m : session.keys().getAesKeys(realm, true)) { + KeysMetadataRepresentation.KeyMetadataRepresentation r = new KeysMetadataRepresentation.KeyMetadataRepresentation(); + r.setProviderId(m.getProviderId()); + r.setProviderPriority(m.getProviderPriority()); + r.setKid(m.getKid()); + r.setStatus(m.getStatus() != null ? m.getStatus().name() : null); + r.setType(AlgorithmType.AES.name()); + l.add(r); + } keys.setKeys(l); diff --git a/services/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java b/services/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java index 01bb2ecc10..1f456cfc31 100755 --- a/services/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java +++ b/services/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java @@ -77,7 +77,7 @@ public class TwitterIdentityProvider extends AbstractIdentityProvider userSessionId = new AtomicReference<>(); + LoginTask loginTask = null; + + try (CloseableHttpClient httpClient = HttpClientBuilder.create().setRedirectStrategy(new LaxRedirectStrategy()).build()) { + loginTask = new LoginTask(httpClient, userSessionId, 100, 1, true, Arrays.asList( + createHttpClientContextForUser(httpClient, "test-user@localhost", "password") + )); + run(DEFAULT_THREADS, DEFAULT_CLIENTS_COUNT, loginTask); + int clientSessionsCount = testingClient.testing().getClientSessionsCountInUserSession("test", userSessionId.get()); + Assert.assertEquals(2, clientSessionsCount); + } finally { + long end = System.currentTimeMillis() - start; + log.infof("Statistics: %s", loginTask == null ? "??" : loginTask.getHistogram()); + log.info("concurrentLoginSingleUserSingleClient took " + (end/1000) + "s"); + log.info("*********************************************"); + } + } + @Test public void concurrentLoginMultipleUsers() throws Throwable { log.info("*********************************************"); @@ -140,7 +163,7 @@ public class ConcurrentLoginTest extends AbstractConcurrencyTest { LoginTask loginTask = null; try (CloseableHttpClient httpClient = HttpClientBuilder.create().setRedirectStrategy(new LaxRedirectStrategy()).build()) { - loginTask = new LoginTask(httpClient, userSessionId, 100, 1, Arrays.asList( + loginTask = new LoginTask(httpClient, userSessionId, 100, 1, false, Arrays.asList( createHttpClientContextForUser(httpClient, "test-user@localhost", "password"), createHttpClientContextForUser(httpClient, "john-doh@localhost", "password"), createHttpClientContextForUser(httpClient, "roleRichUser", "password") @@ -157,6 +180,60 @@ public class ConcurrentLoginTest extends AbstractConcurrencyTest { } } + + @Test + public void concurrentCodeReuseShouldFail() throws Throwable { + log.info("*********************************************"); + long start = System.currentTimeMillis(); + + + for (int i=0 ; i<10 ; i++) { + OAuthClient oauth1 = new OAuthClient(); + oauth1.init(adminClient, driver); + oauth1.clientId("client0"); + + OAuthClient.AuthorizationEndpointResponse resp = oauth1.doLogin("test-user@localhost", "password"); + String code = resp.getCode(); + Assert.assertNotNull(code); + String codeURL = driver.getCurrentUrl(); + + + AtomicInteger codeToTokenSuccessCount = new AtomicInteger(0); + AtomicInteger codeToTokenErrorsCount = new AtomicInteger(0); + + KeycloakRunnable codeToTokenTask = new KeycloakRunnable() { + + @Override + public void run(int threadIndex, Keycloak keycloak, RealmResource realm) throws Throwable { + log.infof("Trying to execute codeURL: %s, threadIndex: %i", codeURL, threadIndex); + + OAuthClient.AccessTokenResponse resp = oauth1.doAccessTokenRequest(code, "password"); + if (resp.getAccessToken() != null && resp.getError() == null) { + codeToTokenSuccessCount.incrementAndGet(); + } else if (resp.getAccessToken() == null && resp.getError() != null) { + codeToTokenErrorsCount.incrementAndGet(); + } + } + + }; + + run(DEFAULT_THREADS, DEFAULT_THREADS, codeToTokenTask); + + oauth1.openLogout(); + + Assert.assertEquals(1, codeToTokenSuccessCount.get()); + Assert.assertEquals(DEFAULT_THREADS - 1, codeToTokenErrorsCount.get()); + + log.infof("Iteration %i passed successfully", i); + } + + long end = System.currentTimeMillis() - start; + log.info("concurrentCodeReuseShouldFail took " + (end/1000) + "s"); + log.info("*********************************************"); + + } + + protected String getPageContent(String url, CloseableHttpClient httpClient, HttpClientContext context) throws IOException { HttpGet request = new HttpGet(url); @@ -237,6 +314,7 @@ public class ConcurrentLoginTest extends AbstractConcurrencyTest { return m; } + public class LoginTask implements KeycloakRunnable { private final AtomicInteger clientIndex = new AtomicInteger(); @@ -256,9 +334,10 @@ public class ConcurrentLoginTest extends AbstractConcurrencyTest { private final int retryCount; private final AtomicInteger[] retryHistogram; private final AtomicInteger totalInvocations = new AtomicInteger(); + private final boolean sameClient; private final List clientContexts; - public LoginTask(CloseableHttpClient httpClient, AtomicReference userSessionId, int retryDelayMs, int retryCount, List clientContexts) { + public LoginTask(CloseableHttpClient httpClient, AtomicReference userSessionId, int retryDelayMs, int retryCount, boolean sameClient, List clientContexts) { this.httpClient = httpClient; this.userSessionId = userSessionId; this.retryDelayMs = retryDelayMs; @@ -267,12 +346,13 @@ public class ConcurrentLoginTest extends AbstractConcurrencyTest { for (int i = 0; i < retryHistogram.length; i ++) { retryHistogram[i] = new AtomicInteger(); } + this.sameClient = sameClient; this.clientContexts = clientContexts; } @Override public void run(int threadIndex, Keycloak keycloak, RealmResource realm) throws Throwable { - int i = clientIndex.getAndIncrement(); + int i = sameClient ? 0 : clientIndex.getAndIncrement(); OAuthClient oauth1 = oauthClient.get(); oauth1.clientId("client" + i); log.infof("%d [%s]: Accessing login page for %s", threadIndex, Thread.currentThread().getName(), oauth1.getClientId()); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/ConcurrentLoginCrossDCTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/ConcurrentLoginCrossDCTest.java index b710943d8e..4cd02567d6 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/ConcurrentLoginCrossDCTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/ConcurrentLoginCrossDCTest.java @@ -75,7 +75,7 @@ public class ConcurrentLoginCrossDCTest extends ConcurrentLoginTest { LoginTask loginTask = null; try (CloseableHttpClient httpClient = HttpClientBuilder.create().setRedirectStrategy(new LaxRedirectStrategy()).build()) { - loginTask = new LoginTask(httpClient, userSessionId, LOGIN_TASK_DELAY_MS, LOGIN_TASK_RETRIES, Arrays.asList( + loginTask = new LoginTask(httpClient, userSessionId, LOGIN_TASK_DELAY_MS, LOGIN_TASK_RETRIES, false, Arrays.asList( createHttpClientContextForUser(httpClient, "test-user@localhost", "password") )); HttpUriRequest request = handleLogin(getPageContent(oauth.getLoginFormUrl(), httpClient, HttpClientContext.create()), "test-user@localhost", "password"); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/GeneratedHmacKeyProviderTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/GeneratedHmacKeyProviderTest.java index 961026d4c8..efddeeab70 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/GeneratedHmacKeyProviderTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/GeneratedHmacKeyProviderTest.java @@ -168,7 +168,7 @@ public class GeneratedHmacKeyProviderTest extends AbstractKeycloakTest { rep.getConfig().putSingle("secretSize", "1234"); Response response = adminClient.realm("test").components().add(rep); - assertErrror(response, "'Secret size' should be 32, 64, 128, 256 or 512"); + assertErrror(response, "'Secret size' should be 16, 24, 32, 64, 128, 256 or 512"); } protected void assertErrror(Response response, String error) { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java index 6a9aa655e5..601231f868 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java @@ -307,7 +307,6 @@ public class AccessTokenTest extends AbstractKeycloakTest { events.expectCodeToToken(codeId, sessionId) .removeDetail(Details.TOKEN_ID) .user((String) null) - .session((String) null) .removeDetail(Details.REFRESH_TOKEN_ID) .removeDetail(Details.REFRESH_TOKEN_TYPE) .error(Errors.INVALID_CODE).assertEvent(); @@ -334,8 +333,8 @@ public class AccessTokenTest extends AbstractKeycloakTest { setTimeOffset(0); - AssertEvents.ExpectedEvent expectedEvent = events.expectCodeToToken(codeId, null); - expectedEvent.error("invalid_code") + AssertEvents.ExpectedEvent expectedEvent = events.expectCodeToToken(codeId, codeId); + expectedEvent.error("expired_code") .removeDetail(Details.TOKEN_ID) .removeDetail(Details.REFRESH_TOKEN_ID) .removeDetail(Details.REFRESH_TOKEN_TYPE) @@ -380,7 +379,7 @@ public class AccessTokenTest extends AbstractKeycloakTest { response = oauth.doAccessTokenRequest(code, "password"); Assert.assertEquals(400, response.getStatusCode()); - AssertEvents.ExpectedEvent expectedEvent = events.expectCodeToToken(codeId, null); + AssertEvents.ExpectedEvent expectedEvent = events.expectCodeToToken(codeId, codeId); expectedEvent.error("invalid_code") .removeDetail(Details.TOKEN_ID) .removeDetail(Details.REFRESH_TOKEN_ID) diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AuthorizationCodeTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AuthorizationCodeTest.java index 757799046c..7ab951225f 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AuthorizationCodeTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AuthorizationCodeTest.java @@ -22,6 +22,7 @@ import org.junit.Rule; import org.junit.Test; import org.keycloak.OAuth2Constants; import org.keycloak.OAuthErrorException; +import org.keycloak.connections.infinispan.InfinispanConnectionProvider; import org.keycloak.events.Details; import org.keycloak.events.Errors; import org.keycloak.models.Constants; @@ -73,7 +74,6 @@ public class AuthorizationCodeTest extends AbstractKeycloakTest { Assert.assertNull(response.getError()); String codeId = events.expectLogin().assertEvent().getDetails().get(Details.CODE_ID); - assertCode(codeId, response.getCode()); } @Test @@ -89,7 +89,6 @@ public class AuthorizationCodeTest extends AbstractKeycloakTest { String code = driver.findElement(By.id(OAuth2Constants.CODE)).getAttribute("value"); String codeId = events.expectLogin().detail(Details.REDIRECT_URI, "http://localhost:8180/auth/realms/test/protocol/openid-connect/oauth/oob").assertEvent().getDetails().get(Details.CODE_ID); - assertCode(codeId, code); ClientManager.realm(adminClient.realm("test")).clientId("test-app").removeRedirectUris(Constants.INSTALLED_APP_URN); } @@ -104,7 +103,6 @@ public class AuthorizationCodeTest extends AbstractKeycloakTest { Assert.assertNotNull(response.getCode()); String codeId = events.expectLogin().assertEvent().getDetails().get(Details.CODE_ID); - assertCode(codeId, response.getCode()); } @Test @@ -119,7 +117,6 @@ public class AuthorizationCodeTest extends AbstractKeycloakTest { Assert.assertNull(response.getError()); String codeId = events.expectLogin().assertEvent().getDetails().get(Details.CODE_ID); - assertCode(codeId, response.getCode()); } @Test @@ -151,11 +148,6 @@ public class AuthorizationCodeTest extends AbstractKeycloakTest { assertEquals("OpenIdConnect.AuthenticationProperties=2302984sdlk", state); String codeId = events.expectLogin().assertEvent().getDetails().get(Details.CODE_ID); - assertCode(codeId, code); - } - - private void assertCode(String expectedCodeId, String actualCode) { - assertEquals(expectedCodeId, actualCode.split("\\.")[2]); } } diff --git a/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java b/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java index 846267f7d3..e6163315fb 100755 --- a/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java +++ b/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java @@ -33,6 +33,7 @@ import org.keycloak.models.UserSessionModel; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.models.UserManager; +import org.keycloak.sessions.CommonClientSessionModel; import org.keycloak.testsuite.rule.KeycloakRule; import java.util.Arrays; @@ -169,14 +170,14 @@ public class UserSessionProviderTest { int time = clientSession.getTimestamp(); assertEquals(null, clientSession.getAction()); - clientSession.setAction(AuthenticatedClientSessionModel.Action.CODE_TO_TOKEN.name()); + clientSession.setAction(AuthenticatedClientSessionModel.Action.LOGGED_OUT.name()); clientSession.setTimestamp(time + 10); kc.stopSession(session, true); session = kc.startSession(); AuthenticatedClientSessionModel updated = session.sessions().getUserSession(realm, userSessionId).getAuthenticatedClientSessions().get(clientUUID); - assertEquals(AuthenticatedClientSessionModel.Action.CODE_TO_TOKEN.name(), updated.getAction()); + assertEquals(AuthenticatedClientSessionModel.Action.LOGGED_OUT.name(), updated.getAction()); assertEquals(time + 10, updated.getTimestamp()); } @@ -190,11 +191,11 @@ public class UserSessionProviderTest { UserSessionModel userSession = session.sessions().getUserSession(realm, userSessionId); AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(clientUUID); - clientSession.setAction(AuthenticatedClientSessionModel.Action.CODE_TO_TOKEN.name()); + clientSession.setAction(AuthenticatedClientSessionModel.Action.LOGGED_OUT.name()); clientSession.setNote("foo", "bar"); AuthenticatedClientSessionModel updated = session.sessions().getUserSession(realm, userSessionId).getAuthenticatedClientSessions().get(clientUUID); - assertEquals(AuthenticatedClientSessionModel.Action.CODE_TO_TOKEN.name(), updated.getAction()); + assertEquals(AuthenticatedClientSessionModel.Action.LOGGED_OUT.name(), updated.getAction()); assertEquals("bar", updated.getNote("foo")); }