From 3b6e1f4e93fe71c6f3da29bb059b6ae708ad6906 Mon Sep 17 00:00:00 2001 From: mposolda Date: Tue, 26 Sep 2017 09:31:15 +0200 Subject: [PATCH] KEYCLOAK-5007 Used single-use cache for tracke OAuth code. OAuth code changed to be encrypted and signed JWT --- .../org/keycloak/common/util/KeyUtils.java | 4 +- .../keycloak/common/util/KeyUtilsTest.java | 2 +- .../main/java/org/keycloak/jose/jwe/JWE.java | 12 +- .../org/keycloak/jose/jwe/JWEException.java | 35 ++++ .../jwe/alg/AesKeyWrapAlgorithmProvider.java | 14 +- .../jose/jwe/alg/DirectAlgorithmProvider.java | 4 +- .../jose/jwe/alg/JWEAlgorithmProvider.java | 4 +- .../jose/jwe/enc/JWEEncryptionProvider.java | 4 +- .../org/keycloak/jose/jws/AlgorithmType.java | 1 + .../org/keycloak/representations/CodeJWT.java | 39 ++++ .../java/org/keycloak/util/TokenUtil.java | 55 ++++++ .../test/java/org/keycloak/jose/JWETest.java | 82 ++++---- .../AuthenticatedClientSessionAdapter.java | 11 +- .../InfinispanCodeToTokenStoreProvider.java | 57 ++++++ ...nispanCodeToTokenStoreProviderFactory.java | 98 +++++++++ .../InfinispanKeycloakTransaction.java | 37 +++- .../AuthenticatedClientSessionEntity.java | 2 +- ...oak.models.CodeToTokenStoreProviderFactory | 18 ++ .../org/keycloak/keys/AesKeyProvider.java | 34 ++++ .../keycloak/keys/AesKeyProviderFactory.java | 34 ++++ .../org/keycloak/keys/HmacKeyProvider.java | 25 +-- .../keycloak/keys/HmacKeyProviderFactory.java | 2 +- .../org/keycloak/keys/SecretKeyProvider.java | 50 +++++ .../migration/MigrationModelManager.java | 4 +- .../migration/migrators/MigrateTo3_4_0.java | 42 ++++ .../models/ActionTokenStoreProvider.java | 4 + .../models/CodeToTokenStoreProvider.java | 34 ++++ .../CodeToTokenStoreProviderFactory.java | 26 +++ .../keycloak/models/CodeToTokenStoreSpi.java | 50 +++++ .../models/utils/DefaultKeyProviders.java | 16 ++ .../services/org.keycloak.provider.Spi | 1 + ...eyMetadata.java => SecretKeyMetadata.java} | 2 +- .../java/org/keycloak/models/KeyManager.java | 29 ++- .../sessions/CommonClientSessionModel.java | 1 - .../AuthenticationProcessor.java | 2 +- .../RequiredActionContextResult.java | 2 +- .../IdentityProviderAuthenticator.java | 2 +- .../java/org/keycloak/keys/Attributes.java | 4 +- .../org/keycloak/keys/DefaultKeyManager.java | 77 +++++++- .../keycloak/keys/FailsafeAesKeyProvider.java | 33 ++++ .../keys/FailsafeHmacKeyProvider.java | 55 +----- .../keys/FailsafeSecretKeyProvider.java | 90 +++++++++ .../keys/GeneratedAesKeyProvider.java | 31 +++ .../keys/GeneratedAesKeyProviderFactory.java | 83 ++++++++ .../keys/GeneratedHmacKeyProvider.java | 75 +------ .../keys/GeneratedHmacKeyProviderFactory.java | 53 ++--- .../keys/GeneratedSecretKeyProvider.java | 102 ++++++++++ .../GeneratedSecretKeyProviderFactory.java | 82 ++++++++ ...ctory.java => SecretKeyProviderUtils.java} | 9 +- .../protocol/oidc/OIDCLoginProtocol.java | 9 +- .../oidc/endpoints/TokenEndpoint.java | 28 +-- .../managers/AuthenticationManager.java | 2 +- .../services/managers/ClientSessionCode.java | 87 +++----- .../services/managers/CodeGenerateUtil.java | 187 ++++++++++++------ .../managers/UserSessionCrossDCManager.java | 11 +- .../resources/IdentityBrokerService.java | 4 +- .../resources/LoginActionsService.java | 4 +- .../services/resources/SessionCodeChecks.java | 4 +- .../services/resources/admin/KeyResource.java | 14 +- .../twitter/TwitterIdentityProvider.java | 11 +- .../org.keycloak.keys.KeyProviderFactory | 1 + .../rest/resource/TestCacheResource.java | 9 + .../resources/TestingCacheResource.java | 4 + .../concurrency/ConcurrentLoginTest.java | 88 ++++++++- .../crossdc/ConcurrentLoginCrossDCTest.java | 2 +- .../keys/GeneratedHmacKeyProviderTest.java | 2 +- .../testsuite/oauth/AccessTokenTest.java | 7 +- .../oauth/AuthorizationCodeTest.java | 10 +- .../model/UserSessionProviderTest.java | 9 +- 69 files changed, 1568 insertions(+), 458 deletions(-) create mode 100644 core/src/main/java/org/keycloak/jose/jwe/JWEException.java create mode 100644 core/src/main/java/org/keycloak/representations/CodeJWT.java create mode 100644 model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanCodeToTokenStoreProvider.java create mode 100644 model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanCodeToTokenStoreProviderFactory.java create mode 100644 model/infinispan/src/main/resources/META-INF/services/org.keycloak.models.CodeToTokenStoreProviderFactory create mode 100644 server-spi-private/src/main/java/org/keycloak/keys/AesKeyProvider.java create mode 100644 server-spi-private/src/main/java/org/keycloak/keys/AesKeyProviderFactory.java create mode 100644 server-spi-private/src/main/java/org/keycloak/keys/SecretKeyProvider.java create mode 100644 server-spi-private/src/main/java/org/keycloak/migration/migrators/MigrateTo3_4_0.java create mode 100644 server-spi-private/src/main/java/org/keycloak/models/CodeToTokenStoreProvider.java create mode 100644 server-spi-private/src/main/java/org/keycloak/models/CodeToTokenStoreProviderFactory.java create mode 100644 server-spi-private/src/main/java/org/keycloak/models/CodeToTokenStoreSpi.java rename server-spi/src/main/java/org/keycloak/keys/{HmacKeyMetadata.java => SecretKeyMetadata.java} (93%) create mode 100644 services/src/main/java/org/keycloak/keys/FailsafeAesKeyProvider.java create mode 100644 services/src/main/java/org/keycloak/keys/FailsafeSecretKeyProvider.java create mode 100644 services/src/main/java/org/keycloak/keys/GeneratedAesKeyProvider.java create mode 100644 services/src/main/java/org/keycloak/keys/GeneratedAesKeyProviderFactory.java create mode 100644 services/src/main/java/org/keycloak/keys/GeneratedSecretKeyProvider.java create mode 100644 services/src/main/java/org/keycloak/keys/GeneratedSecretKeyProviderFactory.java rename services/src/main/java/org/keycloak/keys/{AbstractHmacKeyProviderFactory.java => SecretKeyProviderUtils.java} (80%) 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 37e2b2a3196d..932417e9d9c1 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 5e0abf560f86..29526ea5caa0 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 void loadSecretKey() throws Exception { 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 index 03bb43f99dda..617783edf482 100644 --- a/core/src/main/java/org/keycloak/jose/jwe/JWE.java +++ b/core/src/main/java/org/keycloak/jose/jwe/JWE.java @@ -111,7 +111,7 @@ public void setEncryptedContentInfo(byte[] initializationVector, byte[] encrypte } - public String encodeJwe() { + public String encodeJwe() throws JWEException { try { if (header == null) { throw new IllegalStateException("Header must be set"); @@ -139,8 +139,8 @@ public String encodeJwe() { encryptionProvider.encodeJwe(this); return getEncodedJweString(); - } catch (IOException | GeneralSecurityException e) { - throw new RuntimeException(e); + } catch (Exception e) { + throw new JWEException(e); } } @@ -157,7 +157,7 @@ private String getEncodedJweString() { } - public JWE verifyAndDecodeJwe(String jweStr) { + public JWE verifyAndDecodeJwe(String jweStr) throws JWEException { try { String[] parts = jweStr.split("\\."); if (parts.length != 5) { @@ -189,8 +189,8 @@ public JWE verifyAndDecodeJwe(String jweStr) { encryptionProvider.verifyAndDecodeJwe(this); return this; - } catch (IOException | GeneralSecurityException e) { - throw new RuntimeException(e); + } catch (Exception e) { + throw new JWEException(e); } } 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 000000000000..02768caba134 --- /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/alg/AesKeyWrapAlgorithmProvider.java b/core/src/main/java/org/keycloak/jose/jwe/alg/AesKeyWrapAlgorithmProvider.java index 2a6eedebb8d4..0a5a2c5ba459 100644 --- a/core/src/main/java/org/keycloak/jose/jwe/alg/AesKeyWrapAlgorithmProvider.java +++ b/core/src/main/java/org/keycloak/jose/jwe/alg/AesKeyWrapAlgorithmProvider.java @@ -34,18 +34,14 @@ public class AesKeyWrapAlgorithmProvider implements JWEAlgorithmProvider { @Override - public byte[] decodeCek(byte[] encodedCek, Key encryptionKey) throws IOException, GeneralSecurityException { - try { - Wrapper encrypter = new AESWrapEngine(); - encrypter.init(false, new KeyParameter(encryptionKey.getEncoded())); - return encrypter.unwrap(encodedCek, 0, encodedCek.length); - } catch (InvalidCipherTextException icte) { - throw new IllegalStateException(icte); - } + 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 IOException, GeneralSecurityException { + 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(); 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 index 98ab7b39a99d..b1dd69982167 100644 --- a/core/src/main/java/org/keycloak/jose/jwe/alg/DirectAlgorithmProvider.java +++ b/core/src/main/java/org/keycloak/jose/jwe/alg/DirectAlgorithmProvider.java @@ -30,12 +30,12 @@ public class DirectAlgorithmProvider implements JWEAlgorithmProvider { @Override - public byte[] decodeCek(byte[] encodedCek, Key encryptionKey) throws IOException, GeneralSecurityException { + public byte[] decodeCek(byte[] encodedCek, Key encryptionKey) { return new byte[0]; } @Override - public byte[] encodeCek(JWEEncryptionProvider encryptionProvider, JWEKeyStorage keyStorage, Key encryptionKey) throws IOException, GeneralSecurityException { + 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 index 057f487c6040..caede1319d97 100644 --- a/core/src/main/java/org/keycloak/jose/jwe/alg/JWEAlgorithmProvider.java +++ b/core/src/main/java/org/keycloak/jose/jwe/alg/JWEAlgorithmProvider.java @@ -29,8 +29,8 @@ */ public interface JWEAlgorithmProvider { - byte[] decodeCek(byte[] encodedCek, Key encryptionKey) throws IOException, GeneralSecurityException; + byte[] decodeCek(byte[] encodedCek, Key encryptionKey) throws Exception; - byte[] encodeCek(JWEEncryptionProvider encryptionProvider, JWEKeyStorage keyStorage, Key encryptionKey) throws IOException, GeneralSecurityException; + byte[] encodeCek(JWEEncryptionProvider encryptionProvider, JWEKeyStorage keyStorage, Key encryptionKey) throws Exception; } 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 index c49fe242e519..f9e590c8d210 100644 --- a/core/src/main/java/org/keycloak/jose/jwe/enc/JWEEncryptionProvider.java +++ b/core/src/main/java/org/keycloak/jose/jwe/enc/JWEEncryptionProvider.java @@ -41,7 +41,7 @@ public interface JWEEncryptionProvider { * @throws IOException * @throws GeneralSecurityException */ - void encodeJwe(JWE jwe) throws IOException, GeneralSecurityException; + void encodeJwe(JWE jwe) throws Exception; /** @@ -51,7 +51,7 @@ public interface JWEEncryptionProvider { * @throws IOException * @throws GeneralSecurityException */ - void verifyAndDecodeJwe(JWE jwe) throws IOException, GeneralSecurityException; + void verifyAndDecodeJwe(JWE jwe) throws Exception; /** 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 6c8d93f12217..236f84c1df88 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 000000000000..df43deebf4f0 --- /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 5226c6b1b361..742983a71fe8 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 static boolean isOfflineToken(String refreshToken) throws JWSInputExcepti 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 index 74b75f1b4d3e..f97f3c5b9baa 100644 --- a/core/src/test/java/org/keycloak/jose/JWETest.java +++ b/core/src/test/java/org/keycloak/jose/JWETest.java @@ -28,6 +28,7 @@ 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; @@ -36,7 +37,7 @@ */ public class JWETest { - private static final String PAYLOAD = "Hello world! How are you? This is some quite a long text, which is much longer than just simple 'Hello World'"; + 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 }; @@ -49,43 +50,25 @@ public void testDirect_Aes128CbcHmacSha256() throws Exception { SecretKey aesKey = new SecretKeySpec(AES_128_KEY, "AES"); SecretKey hmacKey = new SecretKeySpec(HMAC_SHA256_KEY, "HMACSHA2"); - JWEHeader jweHeader = new JWEHeader(JWEConstants.DIR, JWEConstants.A128CBC_HS256, 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(); - - 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); - + testDirectEncryptAndDecrypt(aesKey, hmacKey, JWEConstants.A128CBC_HS256, PAYLOAD, true); } - @Test + // 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"); - JWEHeader jweHeader = new JWEHeader(JWEConstants.DIR, JWEConstants.A256CBC_HS512, null); + 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")); + .content(payload.getBytes("UTF-8")); jwe.getKeyStorage() .setCEKKey(aesKey, JWEKeyStorage.KeyUse.ENCRYPTION) @@ -93,8 +76,10 @@ public void testDirect_Aes256CbcHmacSha512() throws Exception { String encodedContent = jwe.encodeJwe(); - System.out.println("Encoded content: " + encodedContent); - System.out.println("Encoded content length: " + encodedContent.length()); + if (sysout) { + System.out.println("Encoded content: " + encodedContent); + System.out.println("Encoded content length: " + encodedContent.length()); + } jwe = new JWE(); jwe.getKeyStorage() @@ -105,8 +90,32 @@ public void testDirect_Aes256CbcHmacSha512() throws Exception { String decodedContent = new String(jwe.getContent(), "UTF-8"); - Assert.assertEquals(PAYLOAD, decodedContent); + 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 000000000000..3fa49f9ab3e0 --- /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 959223c7a2d3..a86a6605e729 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 void put(Cache cache, K key, V value) { 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 void put(Cache cache, K key, V value, long lifespan, TimeUni 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 void execute() { 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 void replace(Cache cache, K key, V value) { 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 void remove(Cache cache, K key) { 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 f67d73619595..901a3138e3f2 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/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 000000000000..e42a1ad1d6a4 --- /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 000000000000..4f9fcc2b5455 --- /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 000000000000..4c359636b679 --- /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 242877455d8c..a525598fc6bc 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 @@ 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 ba7007b45e6b..2e91c4c9d996 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 @@ /** * @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 000000000000..a2b25a1c232f --- /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 4e3f67607fb0..5839aa68d254 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_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 000000000000..8c4f930100ed --- /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 4e4a8dbb5fe4..ba32eaac5f92 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 @@ /** * 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 000000000000..01b1ada1f763 --- /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 000000000000..85b240100d19 --- /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 000000000000..e71ade63034d --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/models/CodeToTokenStoreSpi.java @@ -0,0 +1,50 @@ +/* + * Copyright 2017 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.models; + +import org.keycloak.provider.Provider; +import org.keycloak.provider.ProviderFactory; +import org.keycloak.provider.Spi; + +/** + * @author Marek Posolda + */ +public class CodeToTokenStoreSpi implements Spi { + + public static final String NAME = "codeToTokenStore"; + + @Override + public boolean isInternal() { + return true; + } + + @Override + public String getName() { + return NAME; + } + + @Override + public Class getProviderClass() { + return CodeToTokenStoreProvider.class; + } + + @Override + public Class getProviderFactoryClass() { + return CodeToTokenStoreProviderFactory.class; + } +} diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/DefaultKeyProviders.java b/server-spi-private/src/main/java/org/keycloak/models/utils/DefaultKeyProviders.java index e03929201acc..fbc01ed27d70 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 static void createProviders(RealmModel realm) { realm.addComponentModel(generated); createSecretProvider(realm); + createAesProvider(realm); } public static void createSecretProvider(RealmModel realm) { @@ -57,6 +58,20 @@ public static void createSecretProvider(RealmModel realm) { 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 static void createProviders(RealmModel realm, String privateKeyPem, Strin 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 543ef256c167..14134656b764 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 094281991809..2f8917126116 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 @@ /** * @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 391646895a68..bc47dcbb4923 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 SecretKey getSecretKey() { } } + 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 5f913caf419c..a87309c7c2d5 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 af7d2f7a3c56..e212c924fe04 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 AuthenticationProcessor setForwardedSuccessMessage(FormMessage forwardedS 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 3afb34ce8e3f..3ce976885922 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 @@ private String getExecution() { 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 b255acebfeef..7910e0f93c66 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 @@ private void redirect(AuthenticationFlowContext context, String providerId) { 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 edde62cdfa11..8476e5540bfa 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 4f90f2c2ce74..6a332934573e 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 ActiveHmacKey getActiveHmacKey(RealmModel realm) { 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 Certificate getRsaCertificate(RealmModel realm, String kid) { @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 SecretKey getHmacSecretKey(RealmModel realm, String kid) { 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 List getRsaKeys(RealmModel realm, boolean includeDisabled } @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 @@ private List getProviders(RealmModel realm) { boolean activeRsa = false; boolean activeHmac = false; + boolean activeAes = false; for (ComponentModel c : components) { try { @@ -217,7 +276,13 @@ private List getProviders(RealmModel realm) { 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 @@ private List getProviders(RealmModel realm) { 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 000000000000..d81a5965b113 --- /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 37e837b3394f..676e04807430 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 @@ /** * @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 000000000000..32be2637126b --- /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 000000000000..cb07323cf604 --- /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 000000000000..b9f5a06ba015 --- /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 a989ac3a1192..dfc8fb27b132 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 3a725170bf03..ab4902ade751 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 @@ /** * @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); } @@ -62,50 +64,17 @@ public List getConfigProperties() { } @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) { + public String getId() { + return ID; } @Override - public void close() { + protected Logger logger() { + return logger; } @Override - public String getId() { - return ID; + 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 000000000000..76b7ea82902a --- /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 000000000000..3211cfc62ecb --- /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 f8032b3baac7..1c30f5896e7b 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 @@ /** * @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 13d24a7926e7..91405d34fd54 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.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.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 Response authenticated(UserSessionModel userSession, AuthenticatedClientS 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 Response authenticated(UserSessionModel userSession, AuthenticatedClientS } 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 a28f283819b5..42b42d95f409 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 Response codeToToken() { 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 Response codeToToken() { 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 4f6f4eca8061..19c188a00817 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 static Response actionRequired(final KeycloakSession session, final Authe 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 59158e6c317a..503973ee737b 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 @@ */ 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 static class ParseResult ClientSessionCode code; boolean authSessionNotFound; boolean illegalHash; + boolean expiredToken; CLIENT_SESSION clientSession; public ClientSessionCode getCode() { @@ -77,29 +71,39 @@ public boolean isIllegalHash() { 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 static ParseResult 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 void setAction(String action) { 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; - } - - 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); - } + return parser.retrieveCode(session, commonLoginSession); } - 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 3d0c9ca9fd1a..70e3f353cff7 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 @@ static ClientSessionParser getParser(C 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 @@ interface ClientSessionParser { 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]; + 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).getUserSessionWithClientAndCodeToTokenAction(realm, userSessionId, clientUUID); + 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) { - // 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 null; } - - return userSession.getAuthenticatedClientSessions().get(clientUUID); - } catch (ArrayIndexOutOfBoundsException e) { - return null; } - } - @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(); + return userSession.getAuthenticatedClientSessions().get(codeJWT.getIssuedFor()); + } + @Override - public void removeExpiredSession(KeycloakSession session, AuthenticatedClientSessionModel clientSession) { - throw new IllegalStateException("Not yet implemented"); + 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 getNote(AuthenticatedClientSessionModel clientSession, String name) { - return clientSession.getNote(name); + 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 removeNote(AuthenticatedClientSessionModel clientSession, String name) { - clientSession.removeNote(name); + public void removeExpiredSession(KeycloakSession session, AuthenticatedClientSessionModel clientSession) { + throw new IllegalStateException("Not yet implemented"); } + @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 11795e5456ee..de2516fefc45 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 UserSessionModel getUserSessionWithClient(RealmModel realm, String id, bo } - // 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 78fadf596f73..20e23de99b2f 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 Response clientInitiatedAccountLinking(@PathParam("provider_id") String p 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 @@ private AuthenticationRequest createAuthenticationRequest(String providerId, Cli 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 f6dd8a43abe0..c4ba764843b7 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 static Response redirectToAfterBrokerLoginEndpoint(KeycloakSession sessio 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 b5011fb2ccb3..3f68dd327208 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 AuthenticationSessionModel initialVerifyAuthSession() { } // 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 boolean initialVerify() { 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 d990fd109d8e..87bb486d4b06 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 @@ 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 KeysMetadataRepresentation getKeyMetadata() { 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 KeysMetadataRepresentation getKeyMetadata() { 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 KeysMetadataRepresentation getKeyMetadata() { 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 01bb2ecc1050..1f456cfc31b4 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 TwitterIdentityProvider(KeycloakSession session, OAuth2IdentityProviderCo @Override public Object callback(RealmModel realm, AuthenticationCallback callback, EventBuilder event) { - return new Endpoint(realm, callback); + return new Endpoint(realm, callback, event); } @Override @@ -161,6 +161,7 @@ protected Response exchangeSessionToken(UriInfo uriInfo, ClientModel authorizedC protected class Endpoint { protected RealmModel realm; protected AuthenticationCallback callback; + protected EventBuilder event; @Context protected KeycloakSession session; @@ -174,9 +175,12 @@ protected class Endpoint { @Context protected UriInfo uriInfo; - public Endpoint(RealmModel realm, AuthenticationCallback callback) { + + + public Endpoint(RealmModel realm, AuthenticationCallback callback, EventBuilder event) { this.realm = realm; this.callback = callback; + this.event = event; } @GET @@ -194,7 +198,7 @@ public Response authResponse(@QueryParam("state") String state, twitter.setOAuthConsumer(getConfig().getClientId(), getConfig().getClientSecret()); - AuthenticationSessionModel authSession = ClientSessionCode.getClientSession(state, session, realm, AuthenticationSessionModel.class); + AuthenticationSessionModel authSession = ClientSessionCode.getClientSession(state, session, realm, event, AuthenticationSessionModel.class); String twitterToken = authSession.getAuthNote(TWITTER_TOKEN); String twitterSecret = authSession.getAuthNote(TWITTER_TOKENSECRET); @@ -240,7 +244,6 @@ public Response authResponse(@QueryParam("state") String state, } private void sendErrorEvent() { - EventBuilder event = new EventBuilder(realm, session, clientConnection); event.event(EventType.LOGIN); event.error("twitter_login_failed"); } diff --git a/services/src/main/resources/META-INF/services/org.keycloak.keys.KeyProviderFactory b/services/src/main/resources/META-INF/services/org.keycloak.keys.KeyProviderFactory index a57070b97a3d..d46a92fe17a9 100644 --- a/services/src/main/resources/META-INF/services/org.keycloak.keys.KeyProviderFactory +++ b/services/src/main/resources/META-INF/services/org.keycloak.keys.KeyProviderFactory @@ -16,6 +16,7 @@ # org.keycloak.keys.GeneratedHmacKeyProviderFactory +org.keycloak.keys.GeneratedAesKeyProviderFactory org.keycloak.keys.GeneratedRsaKeyProviderFactory org.keycloak.keys.JavaKeystoreKeyProviderFactory org.keycloak.keys.ImportedRsaKeyProviderFactory \ No newline at end of file diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestCacheResource.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestCacheResource.java index 964e80da8b93..1954cb87df92 100644 --- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestCacheResource.java +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestCacheResource.java @@ -20,6 +20,7 @@ import java.util.HashMap; import java.util.Map; import java.util.Set; +import java.util.UUID; import java.util.stream.Collectors; import javax.ws.rs.Consumes; @@ -59,6 +60,14 @@ public boolean contains(@PathParam("id") String id) { return cache.containsKey(id); } + @GET + @Path("/contains-uuid/{id}") + @Produces(MediaType.APPLICATION_JSON) + public boolean containsUuid(@PathParam("id") String id) { + UUID uuid = UUID.fromString(id); + return cache.containsKey(uuid); + } + @GET @Path("/enumerate-keys") diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingCacheResource.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingCacheResource.java index 1c362ea3a108..c23d24122912 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingCacheResource.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingCacheResource.java @@ -39,6 +39,10 @@ public interface TestingCacheResource { @Produces(MediaType.APPLICATION_JSON) boolean contains(@PathParam("id") String id); + @GET + @Path("/contains-uuid/{id}") + @Produces(MediaType.APPLICATION_JSON) + boolean containsUuid(@PathParam("id") String id); @GET @Path("/enumerate-keys") diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/ConcurrentLoginTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/ConcurrentLoginTest.java index ff6f10f4590f..ee38faebeaae 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/ConcurrentLoginTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/ConcurrentLoginTest.java @@ -107,7 +107,7 @@ public void concurrentLoginSingleUser() throws Throwable { 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") )); run(DEFAULT_THREADS, DEFAULT_CLIENTS_COUNT, loginTask); @@ -131,6 +131,29 @@ protected HttpClientContext createHttpClientContextForUser(final CloseableHttpCl return context; } + @Test + public void concurrentLoginSingleUserSingleClient() throws Throwable { + log.info("*********************************************"); + long start = System.currentTimeMillis(); + + AtomicReference 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 void concurrentLoginMultipleUsers() throws Throwable { 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 void concurrentLoginMultipleUsers() throws Throwable { } } + + @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 @@ private static Map getQueryFromUrl(String url) throws URISyntaxE return m; } + public class LoginTask implements KeycloakRunnable { private final AtomicInteger clientIndex = new AtomicInteger(); @@ -256,9 +334,10 @@ protected OAuthClient initialValue() { 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 LoginTask(CloseableHttpClient httpClient, AtomicReference userSes 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 b710943d8e3e..4cd02567d6b4 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 void concurrentLoginWithRandomDcFailures() throws Throwable { 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 961026d4c867..efddeeab7046 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 void invalidKeysize() throws Exception { 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 6a9aa655e5ba..601231f868b3 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 void accessTokenUserSessionExpired() { 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 void accessTokenCodeExpired() { 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 void accessTokenCodeUsed() throws IOException { 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 757799046c52..7ab951225fc0 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.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 void authorizationRequest() throws IOException { Assert.assertNull(response.getError()); String codeId = events.expectLogin().assertEvent().getDetails().get(Details.CODE_ID); - assertCode(codeId, response.getCode()); } @Test @@ -89,7 +89,6 @@ public void authorizationRequestInstalledApp() throws IOException { 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 void authorizationValidRedirectUri() throws IOException { Assert.assertNotNull(response.getCode()); String codeId = events.expectLogin().assertEvent().getDetails().get(Details.CODE_ID); - assertCode(codeId, response.getCode()); } @Test @@ -119,7 +117,6 @@ public void authorizationRequestNoState() throws IOException { Assert.assertNull(response.getError()); String codeId = events.expectLogin().assertEvent().getDetails().get(Details.CODE_ID); - assertCode(codeId, response.getCode()); } @Test @@ -151,11 +148,6 @@ public void authorizationRequestFormPostResponseMode() throws IOException { 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 846267f7d35b..e6163315fb25 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.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 void testUpdateClientSession() { 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 void testUpdateClientSessionInSameTransaction() { 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")); }