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 extends Provider> getProviderClass() {
+ return CodeToTokenStoreProvider.class;
+ }
+
+ @Override
+ public Class extends ProviderFactory> 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"));
}