KEYCLOAK-5007 Used single-use cache for tracke OAuth code. OAuth code changed to be encrypted and signed JWT
This commit is contained in:
parent
63673c4328
commit
3b6e1f4e93
69 changed files with 1574 additions and 464 deletions
|
@ -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) {
|
||||
|
|
|
@ -16,7 +16,7 @@ public class KeyUtilsTest {
|
|||
byte[] secretBytes = new byte[32];
|
||||
ThreadLocalRandom.current().nextBytes(secretBytes);
|
||||
SecretKeySpec expected = new SecretKeySpec(secretBytes, "HmacSHA256");
|
||||
SecretKey actual = KeyUtils.loadSecretKey(secretBytes);
|
||||
SecretKey actual = KeyUtils.loadSecretKey(secretBytes, "HmacSHA256");
|
||||
assertEquals(expected.getAlgorithm(), actual.getAlgorithm());
|
||||
assertArrayEquals(expected.getEncoded(), actual.getEncoded());
|
||||
}
|
||||
|
|
|
@ -111,7 +111,7 @@ public class JWE {
|
|||
}
|
||||
|
||||
|
||||
public String encodeJwe() {
|
||||
public String encodeJwe() throws JWEException {
|
||||
try {
|
||||
if (header == null) {
|
||||
throw new IllegalStateException("Header must be set");
|
||||
|
@ -139,8 +139,8 @@ public class JWE {
|
|||
encryptionProvider.encodeJwe(this);
|
||||
|
||||
return getEncodedJweString();
|
||||
} catch (IOException | GeneralSecurityException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (Exception e) {
|
||||
throw new JWEException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -157,7 +157,7 @@ public class JWE {
|
|||
}
|
||||
|
||||
|
||||
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 class JWE {
|
|||
encryptionProvider.verifyAndDecodeJwe(this);
|
||||
|
||||
return this;
|
||||
} catch (IOException | GeneralSecurityException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (Exception e) {
|
||||
throw new JWEException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
35
core/src/main/java/org/keycloak/jose/jwe/JWEException.java
Normal file
35
core/src/main/java/org/keycloak/jose/jwe/JWEException.java
Normal file
|
@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class JWEException extends Exception {
|
||||
|
||||
public JWEException(String s) {
|
||||
super(s);
|
||||
}
|
||||
|
||||
public JWEException() {
|
||||
}
|
||||
|
||||
public JWEException(Throwable throwable) {
|
||||
super(throwable);
|
||||
}
|
||||
}
|
|
@ -34,18 +34,14 @@ import org.keycloak.jose.jwe.enc.JWEEncryptionProvider;
|
|||
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();
|
||||
|
|
|
@ -30,12 +30,12 @@ import org.keycloak.jose.jwe.enc.JWEEncryptionProvider;
|
|||
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];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,8 +29,8 @@ import org.keycloak.jose.jwe.enc.JWEEncryptionProvider;
|
|||
*/
|
||||
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;
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
||||
/**
|
||||
|
|
|
@ -25,6 +25,7 @@ public enum AlgorithmType {
|
|||
|
||||
RSA,
|
||||
HMAC,
|
||||
AES,
|
||||
ECDSA
|
||||
|
||||
}
|
||||
|
|
39
core/src/main/java/org/keycloak/representations/CodeJWT.java
Normal file
39
core/src/main/java/org/keycloak/representations/CodeJWT.java
Normal file
|
@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
|
@ -115,4 +122,52 @@ public class TokenUtil {
|
|||
return token.getType().equals(TOKEN_TYPE_OFFLINE);
|
||||
}
|
||||
|
||||
|
||||
public static String jweDirectEncode(Key aesKey, Key hmacKey, JsonWebToken jwt) throws JWEException {
|
||||
int keyLength = aesKey.getEncoded().length;
|
||||
String encAlgorithm;
|
||||
switch (keyLength) {
|
||||
case 16: encAlgorithm = JWEConstants.A128CBC_HS256;
|
||||
break;
|
||||
case 24: encAlgorithm = JWEConstants.A192CBC_HS384;
|
||||
break;
|
||||
case 32: encAlgorithm = JWEConstants.A256CBC_HS512;
|
||||
break;
|
||||
default: throw new IllegalArgumentException("Bad size for Encryption key: " + aesKey + ". Valid sizes are 16, 24, 32.");
|
||||
}
|
||||
|
||||
try {
|
||||
byte[] contentBytes = JsonSerialization.writeValueAsBytes(jwt);
|
||||
|
||||
JWEHeader jweHeader = new JWEHeader(JWEConstants.DIR, encAlgorithm, null);
|
||||
JWE jwe = new JWE()
|
||||
.header(jweHeader)
|
||||
.content(contentBytes);
|
||||
|
||||
jwe.getKeyStorage()
|
||||
.setCEKKey(aesKey, JWEKeyStorage.KeyUse.ENCRYPTION)
|
||||
.setCEKKey(hmacKey, JWEKeyStorage.KeyUse.SIGNATURE);
|
||||
|
||||
return jwe.encodeJwe();
|
||||
} catch (IOException ioe) {
|
||||
throw new JWEException(ioe);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static <T extends JsonWebToken> T jweDirectVerifyAndDecode(Key aesKey, Key hmacKey, String jweStr, Class<T> 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@ import org.junit.Test;
|
|||
import org.keycloak.common.util.Base64Url;
|
||||
import org.keycloak.jose.jwe.JWE;
|
||||
import org.keycloak.jose.jwe.JWEConstants;
|
||||
import org.keycloak.jose.jwe.JWEException;
|
||||
import org.keycloak.jose.jwe.JWEHeader;
|
||||
import org.keycloak.jose.jwe.JWEKeyStorage;
|
||||
|
||||
|
@ -36,7 +37,7 @@ import org.keycloak.jose.jwe.JWEKeyStorage;
|
|||
*/
|
||||
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 class JWETest {
|
|||
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 class JWETest {
|
|||
|
||||
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 class JWETest {
|
|||
|
||||
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 ; i<iterations ; i++) {
|
||||
// took around 2950 ms with 50000 iterations
|
||||
SecretKey aesKey = new SecretKeySpec(AES_128_KEY, "AES");
|
||||
SecretKey hmacKey = new SecretKeySpec(HMAC_SHA256_KEY, "HMACSHA2");
|
||||
String encAlg = JWEConstants.A128CBC_HS256;
|
||||
|
||||
// Similar perf like AES128CBC_HS256
|
||||
//SecretKey aesKey = new SecretKeySpec(AES_256_KEY, "AES");
|
||||
//SecretKey hmacKey = new SecretKeySpec(HMAC_SHA512_KEY, "HMACSHA2");
|
||||
//String encAlg = JWEConstants.A256CBC_HS512;
|
||||
|
||||
String payload = PAYLOAD + i;
|
||||
testDirectEncryptAndDecrypt(aesKey, hmacKey, encAlg, payload, false);
|
||||
}
|
||||
|
||||
long took = System.currentTimeMillis() - start;
|
||||
System.out.println("Iterations: " + iterations + ", took: " + took);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -139,7 +148,7 @@ public class JWETest {
|
|||
|
||||
|
||||
@Test
|
||||
public void externalJweAes128CbcHmacSha256Test() throws UnsupportedEncodingException {
|
||||
public void externalJweAes128CbcHmacSha256Test() throws UnsupportedEncodingException, JWEException {
|
||||
String externalJwe = "eyJlbmMiOiJBMTI4Q0JDLUhTMjU2IiwiYWxnIjoiZGlyIn0..qysUrI1iVtiG4Z4jyr7XXg.apdNSQhR7WDMg6IHf5aLVI0gGp6JuOHYmIUtflns4WHmyxOOnh_GShLI6DWaK_SiywTV5gZvZYtl8H8Iv5fTfLkc4tiDDjbdtmsOP7tqyRxVh069gU5UvEAgmCXbIKALutgYXcYe2WM4E6BIHPTSt8jXdkktFcm7XHiD7mpakZyjXsG8p3XVkQJ72WbJI_t6.Ks6gHeko7BRTZ4CFs5ijRA";
|
||||
System.out.println("External encoded content length: " + externalJwe.length());
|
||||
|
||||
|
@ -159,8 +168,9 @@ public class JWETest {
|
|||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void externalJweAes256CbcHmacSha512Test() throws UnsupportedEncodingException {
|
||||
// Works just on OpenJDK 8. Other JDKs (IBM, Oracle) have restrictions on maximum key size of AES to be 128
|
||||
// @Test
|
||||
public void externalJweAes256CbcHmacSha512Test() throws UnsupportedEncodingException, JWEException {
|
||||
String externalJwe = "eyJlbmMiOiJBMjU2Q0JDLUhTNTEyIiwiYWxnIjoiZGlyIn0..xUPndQ5U69CYaWMKr4nyeg.AzSzba6OdNsvTIoNpub8d2TmYnkY7W8Sd-1S33DjJwJsSaNcfvfXBq5bqXAGVAnLHrLZJKWoEYsmOrYHz3Nao-kpLtUpc4XZI8yiYUqkHTjmxZnfD02R6hz31a5KBCnDTtUEv23VSxm8yUyQKoUTpVHbJ3b2VQvycg2XFUXPsA6oaSSEpz-uwe1Vmun2hUBB.Qal4rMYn1RrXQ9AQ9ONUjUXvlS2ow8np-T8QWMBR0ns";
|
||||
System.out.println("External encoded content length: " + externalJwe.length());
|
||||
|
||||
|
@ -181,7 +191,7 @@ public class JWETest {
|
|||
|
||||
|
||||
@Test
|
||||
public void externalJweAesKeyWrapTest() throws Exception {
|
||||
public void externalJweAes128KeyWrapTest() throws Exception {
|
||||
// See example "A.3" from JWE specification - https://tools.ietf.org/html/rfc7516#page-41
|
||||
String externalJwe = "eyJhbGciOiJBMTI4S1ciLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0.6KB707dM9YTIgHtLvtgWQ8mKwboJW3of9locizkDTHzBC2IlrT1oOQ.AxY8DCtDaGlsbGljb3RoZQ.KDlTtXchhZTGufMYmOYGS4HffxPSUrfmqCHXaI9wOGY.U0m_YmjN04DJvceFICbCVQ";
|
||||
|
||||
|
|
|
@ -24,12 +24,10 @@ import java.util.Set;
|
|||
|
||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.models.sessions.infinispan.changes.InfinispanChangelogBasedTransaction;
|
||||
import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
|
||||
import org.keycloak.models.sessions.infinispan.changes.SessionUpdateTask;
|
||||
import org.keycloak.models.sessions.infinispan.changes.UserSessionClientSessionUpdateTask;
|
||||
import org.keycloak.models.sessions.infinispan.changes.UserSessionUpdateTask;
|
||||
import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity;
|
||||
|
@ -40,7 +38,7 @@ import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
|
|||
*/
|
||||
public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSessionModel {
|
||||
|
||||
private final AuthenticatedClientSessionEntity entity;
|
||||
private AuthenticatedClientSessionEntity entity;
|
||||
private final ClientModel client;
|
||||
private final InfinispanUserSessionProvider provider;
|
||||
private final InfinispanChangelogBasedTransaction updateTx;
|
||||
|
@ -63,7 +61,6 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes
|
|||
@Override
|
||||
public void setUserSession(UserSessionModel userSession) {
|
||||
String clientUUID = client.getId();
|
||||
UserSessionEntity sessionEntity = this.userSession.getEntity();
|
||||
|
||||
// Dettach userSession
|
||||
if (userSession == null) {
|
||||
|
@ -83,7 +80,11 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes
|
|||
|
||||
@Override
|
||||
public void runUpdate(UserSessionEntity sessionEntity) {
|
||||
sessionEntity.getAuthenticatedClientSessions().put(clientUUID, entity);
|
||||
AuthenticatedClientSessionEntity current = sessionEntity.getAuthenticatedClientSessions().putIfAbsent(clientUUID, entity);
|
||||
if (current != null) {
|
||||
// It may happen when 2 concurrent HTTP requests trying SSO login against same client
|
||||
entity = current;
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* 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.concurrent.TimeUnit;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.infinispan.commons.api.BasicCache;
|
||||
import org.keycloak.models.CodeToTokenStoreProvider;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.sessions.infinispan.entities.ActionTokenValueEntity;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class InfinispanCodeToTokenStoreProvider implements CodeToTokenStoreProvider {
|
||||
|
||||
private final Supplier<BasicCache<UUID, ActionTokenValueEntity>> codeCache;
|
||||
private final KeycloakSession session;
|
||||
|
||||
public InfinispanCodeToTokenStoreProvider(KeycloakSession session, Supplier<BasicCache<UUID, ActionTokenValueEntity>> 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<UUID, ActionTokenValueEntity> cache = codeCache.get();
|
||||
ActionTokenValueEntity existing = cache.putIfAbsent(codeId, tokenValue, lifespanInSeconds, TimeUnit.SECONDS);
|
||||
return existing == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
}
|
|
@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class InfinispanCodeToTokenStoreProviderFactory implements CodeToTokenStoreProviderFactory {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(InfinispanCodeToTokenStoreProviderFactory.class);
|
||||
|
||||
// Reuse "actionTokens" infinispan cache for now
|
||||
private volatile Supplier<BasicCache<UUID, ActionTokenValueEntity>> 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";
|
||||
}
|
||||
}
|
|
@ -88,6 +88,11 @@ public class InfinispanKeycloakTransaction implements KeycloakTransaction {
|
|||
public void execute() {
|
||||
decorateCache(cache).put(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("CacheTaskWithValue: Operation 'put' for key %s", key);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -104,6 +109,11 @@ public class InfinispanKeycloakTransaction implements KeycloakTransaction {
|
|||
public void execute() {
|
||||
decorateCache(cache).put(key, value, lifespan, lifespanUnit);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("CacheTaskWithValue: Operation 'put' for key %s, lifespan %d TimeUnit %s", key, lifespan, lifespanUnit.toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -123,6 +133,11 @@ public class InfinispanKeycloakTransaction implements KeycloakTransaction {
|
|||
throw new IllegalStateException("There is already existing value in cache for key " + key);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("CacheTaskWithValue: Operation 'putIfAbsent' for key %s", key);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -142,6 +157,12 @@ public class InfinispanKeycloakTransaction implements KeycloakTransaction {
|
|||
public void execute() {
|
||||
decorateCache(cache).replace(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("CacheTaskWithValue: Operation 'replace' for key %s", key);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -162,7 +183,21 @@ public class InfinispanKeycloakTransaction implements KeycloakTransaction {
|
|||
log.tracev("Adding cache operation: {0} on {1}", CacheOperation.REMOVE, key);
|
||||
|
||||
Object taskKey = getTaskKey(cache, key);
|
||||
tasks.put(taskKey, () -> decorateCache(cache).remove(key));
|
||||
|
||||
// TODO:performance Eventual performance optimization could be to skip "cache.remove" if item was added in this transaction (EG. authenticationSession valid for single request due to automatic SSO login)
|
||||
tasks.put(taskKey, new CacheTask() {
|
||||
|
||||
@Override
|
||||
public void execute() {
|
||||
decorateCache(cache).remove(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("CacheTask: Operation 'remove' for key %s", key);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
// This is for possibility to lookup for session by id, which was created in this transaction
|
||||
|
|
|
@ -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<String> roles;
|
||||
|
|
|
@ -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
|
|
@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public interface AesKeyProvider extends SecretKeyProvider {
|
||||
|
||||
default AlgorithmType getType() {
|
||||
return AlgorithmType.AES;
|
||||
}
|
||||
|
||||
default String getJavaAlgorithmName() {
|
||||
return "AES";
|
||||
}
|
||||
}
|
|
@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public interface AesKeyProviderFactory extends KeyProviderFactory<AesKeyProvider> {
|
||||
|
||||
@Override
|
||||
default Map<String, Object> getTypeMetadata() {
|
||||
return Collections.singletonMap("algorithmType", AlgorithmType.AES);
|
||||
}
|
||||
}
|
|
@ -19,34 +19,17 @@ package org.keycloak.keys;
|
|||
|
||||
import org.keycloak.jose.jws.AlgorithmType;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public interface HmacKeyProvider extends KeyProvider<HmacKeyMetadata> {
|
||||
public interface HmacKeyProvider extends SecretKeyProvider {
|
||||
|
||||
default AlgorithmType getType() {
|
||||
return AlgorithmType.HMAC;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the active secret key, or <code>null</code> if no active key is available.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
SecretKey getSecretKey();
|
||||
|
||||
/**
|
||||
* Return the secret key for the specified kid, or <code>null</code> if the kid is unknown.
|
||||
*
|
||||
* @param kid
|
||||
* @return
|
||||
*/
|
||||
SecretKey getSecretKey(String kid);
|
||||
default String getJavaAlgorithmName() {
|
||||
return "HmacSHA256";
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ import java.util.Map;
|
|||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public interface HmacKeyProviderFactory extends KeyProviderFactory {
|
||||
public interface HmacKeyProviderFactory extends KeyProviderFactory<HmacKeyProvider> {
|
||||
|
||||
@Override
|
||||
default Map<String, Object> getTypeMetadata() {
|
||||
|
|
|
@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public interface SecretKeyProvider extends KeyProvider<SecretKeyMetadata> {
|
||||
|
||||
/**
|
||||
* Return the active secret key, or <code>null</code> if no active key is available.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
SecretKey getSecretKey();
|
||||
|
||||
/**
|
||||
* Return the secret key for the specified kid, or <code>null</code> 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();
|
||||
}
|
|
@ -36,6 +36,7 @@ import org.keycloak.migration.migrators.MigrateTo3_0_0;
|
|||
import org.keycloak.migration.migrators.MigrateTo3_1_0;
|
||||
import org.keycloak.migration.migrators.MigrateTo3_2_0;
|
||||
import org.keycloak.migration.migrators.MigrateTo3_3_0;
|
||||
import org.keycloak.migration.migrators.MigrateTo3_4_0;
|
||||
import org.keycloak.migration.migrators.Migration;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
||||
|
@ -64,7 +65,8 @@ public class MigrationModelManager {
|
|||
new MigrateTo3_0_0(),
|
||||
new MigrateTo3_1_0(),
|
||||
new MigrateTo3_2_0(),
|
||||
new MigrateTo3_3_0()
|
||||
new MigrateTo3_3_0(),
|
||||
new MigrateTo3_4_0()
|
||||
};
|
||||
|
||||
public static void migrate(KeycloakSession session) {
|
||||
|
|
|
@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -22,6 +22,10 @@ import java.util.Map;
|
|||
|
||||
/**
|
||||
* Internal action token store provider.
|
||||
*
|
||||
* It's used for store the details about used action tokens. There is separate provider for OAuth2 codes - {@link CodeToTokenStoreProvider},
|
||||
* which may reuse some components (eg. same infinispan cache)
|
||||
*
|
||||
* @author hmlnarik
|
||||
*/
|
||||
public interface ActionTokenStoreProvider extends Provider {
|
||||
|
|
|
@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public interface CodeToTokenStoreProvider extends Provider {
|
||||
|
||||
boolean putIfAbsent(UUID codeId);
|
||||
}
|
|
@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public interface CodeToTokenStoreProviderFactory extends ProviderFactory<CodeToTokenStoreProvider> {
|
||||
}
|
|
@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class CodeToTokenStoreSpi implements Spi {
|
||||
|
||||
public static final String NAME = "codeToTokenStore";
|
||||
|
||||
@Override
|
||||
public boolean isInternal() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends Provider> getProviderClass() {
|
||||
return CodeToTokenStoreProvider.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends ProviderFactory> getProviderFactoryClass() {
|
||||
return CodeToTokenStoreProviderFactory.class;
|
||||
}
|
||||
}
|
|
@ -41,6 +41,7 @@ public class DefaultKeyProviders {
|
|||
realm.addComponentModel(generated);
|
||||
|
||||
createSecretProvider(realm);
|
||||
createAesProvider(realm);
|
||||
}
|
||||
|
||||
public static void createSecretProvider(RealmModel realm) {
|
||||
|
@ -57,6 +58,20 @@ public class DefaultKeyProviders {
|
|||
realm.addComponentModel(generated);
|
||||
}
|
||||
|
||||
public static void createAesProvider(RealmModel realm) {
|
||||
ComponentModel generated = new ComponentModel();
|
||||
generated.setName("aes-generated");
|
||||
generated.setParentId(realm.getId());
|
||||
generated.setProviderId("aes-generated");
|
||||
generated.setProviderType(KeyProvider.class.getName());
|
||||
|
||||
MultivaluedHashMap<String, String> config = new MultivaluedHashMap<>();
|
||||
config.putSingle("priority", "100");
|
||||
generated.setConfig(config);
|
||||
|
||||
realm.addComponentModel(generated);
|
||||
}
|
||||
|
||||
public static void createProviders(RealmModel realm, String privateKeyPem, String certificatePem) {
|
||||
ComponentModel rsa = new ComponentModel();
|
||||
rsa.setName("rsa");
|
||||
|
@ -75,6 +90,7 @@ public class DefaultKeyProviders {
|
|||
realm.addComponentModel(rsa);
|
||||
|
||||
createSecretProvider(realm);
|
||||
createAesProvider(realm);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -20,6 +20,6 @@ package org.keycloak.keys;
|
|||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public class HmacKeyMetadata extends KeyMetadata {
|
||||
public class SecretKeyMetadata extends KeyMetadata {
|
||||
|
||||
}
|
|
@ -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<HmacKeyMetadata> getHmacKeys(RealmModel realm, boolean includeDisabled);
|
||||
List<SecretKeyMetadata> getHmacKeys(RealmModel realm, boolean includeDisabled);
|
||||
|
||||
ActiveAesKey getActiveAesKey(RealmModel realm);
|
||||
|
||||
SecretKey getAesSecretKey(RealmModel realm, String kid);
|
||||
|
||||
List<SecretKeyMetadata> getAesKeys(RealmModel realm, boolean includeDisabled);
|
||||
|
||||
class ActiveRsaKey {
|
||||
private final String kid;
|
||||
|
@ -94,4 +100,23 @@ public interface KeyManager {
|
|||
}
|
||||
}
|
||||
|
||||
class ActiveAesKey {
|
||||
private final String kid;
|
||||
private final SecretKey secretKey;
|
||||
|
||||
public ActiveAesKey(String kid, SecretKey secretKey) {
|
||||
this.kid = kid;
|
||||
this.secretKey = secretKey;
|
||||
}
|
||||
|
||||
public String getKid() {
|
||||
return kid;
|
||||
}
|
||||
|
||||
public SecretKey getSecretKey() {
|
||||
return secretKey;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -56,7 +56,6 @@ public interface CommonClientSessionModel {
|
|||
|
||||
public static enum Action {
|
||||
OAUTH_GRANT,
|
||||
CODE_TO_TOKEN,
|
||||
AUTHENTICATE,
|
||||
LOGGED_OUT,
|
||||
LOGGING_OUT,
|
||||
|
|
|
@ -223,7 +223,7 @@ public class AuthenticationProcessor {
|
|||
public String generateCode() {
|
||||
ClientSessionCode accessCode = new ClientSessionCode(session, getRealm(), getAuthenticationSession());
|
||||
authenticationSession.setTimestamp(Time.currentTime());
|
||||
return accessCode.getCode();
|
||||
return accessCode.getOrGenerateCode();
|
||||
}
|
||||
|
||||
public EventBuilder newEvent() {
|
||||
|
|
|
@ -150,7 +150,7 @@ public class RequiredActionContextResult implements RequiredActionContext {
|
|||
public String generateCode() {
|
||||
ClientSessionCode<AuthenticationSessionModel> accessCode = new ClientSessionCode<>(session, getRealm(), getAuthenticationSession());
|
||||
authenticationSession.setTimestamp(Time.currentTime());
|
||||
return accessCode.getCode();
|
||||
return accessCode.getOrGenerateCode();
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -63,7 +63,7 @@ public class IdentityProviderAuthenticator implements Authenticator {
|
|||
List<IdentityProviderModel> 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))
|
||||
|
|
|
@ -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");
|
||||
|
||||
}
|
||||
|
|
|
@ -82,6 +82,23 @@ public class DefaultKeyManager implements KeyManager {
|
|||
throw new RuntimeException("Failed to get keys");
|
||||
}
|
||||
|
||||
@Override
|
||||
public ActiveAesKey getActiveAesKey(RealmModel realm) {
|
||||
for (KeyProvider p : getProviders(realm)) {
|
||||
if (p.getType().equals(AlgorithmType.AES)) {
|
||||
AesKeyProvider h = (AesKeyProvider) p;
|
||||
if (h.getKid() != null && h.getSecretKey() != null) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.tracev("Active AES Key realm={0} kid={1}", realm.getName(), p.getKid());
|
||||
}
|
||||
String kid = p.getKid();
|
||||
return new ActiveAesKey(kid, h.getSecretKey());
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new RuntimeException("Failed to get keys");
|
||||
}
|
||||
|
||||
@Override
|
||||
public PublicKey getRsaPublicKey(RealmModel realm, String kid) {
|
||||
if (kid == null) {
|
||||
|
@ -135,7 +152,7 @@ public class DefaultKeyManager implements KeyManager {
|
|||
@Override
|
||||
public SecretKey getHmacSecretKey(RealmModel realm, String kid) {
|
||||
if (kid == null) {
|
||||
logger.warnv("KID is null, can't find public key", realm.getName(), kid);
|
||||
logger.warnv("KID is null, can't find secret key", realm.getName(), kid);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -157,6 +174,31 @@ public class DefaultKeyManager implements KeyManager {
|
|||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SecretKey getAesSecretKey(RealmModel realm, String kid) {
|
||||
if (kid == null) {
|
||||
logger.warnv("KID is null, can't find aes key", realm.getName(), kid);
|
||||
return null;
|
||||
}
|
||||
|
||||
for (KeyProvider p : getProviders(realm)) {
|
||||
if (p.getType().equals(AlgorithmType.AES)) {
|
||||
AesKeyProvider h = (AesKeyProvider) p;
|
||||
SecretKey s = h.getSecretKey(kid);
|
||||
if (s != null) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.tracev("Found AES key realm={0} kid={1}", realm.getName(), kid);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.tracev("Failed to find AES key realm={0} kid={1}", realm.getName(), kid);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<RsaKeyMetadata> getRsaKeys(RealmModel realm, boolean includeDisabled) {
|
||||
List<RsaKeyMetadata> keys = new LinkedList<>();
|
||||
|
@ -174,14 +216,30 @@ public class DefaultKeyManager implements KeyManager {
|
|||
}
|
||||
|
||||
@Override
|
||||
public List<HmacKeyMetadata> getHmacKeys(RealmModel realm, boolean includeDisabled) {
|
||||
List<HmacKeyMetadata> keys = new LinkedList<>();
|
||||
public List<SecretKeyMetadata> getHmacKeys(RealmModel realm, boolean includeDisabled) {
|
||||
List<SecretKeyMetadata> keys = new LinkedList<>();
|
||||
for (KeyProvider p : getProviders(realm)) {
|
||||
if (p instanceof HmacKeyProvider) {
|
||||
if (includeDisabled) {
|
||||
keys.addAll(p.getKeyMetadata());
|
||||
} else {
|
||||
List<HmacKeyMetadata> metadata = p.getKeyMetadata();
|
||||
List<SecretKeyMetadata> metadata = p.getKeyMetadata();
|
||||
metadata.stream().filter(k -> k.getStatus() != KeyMetadata.Status.DISABLED).forEach(k -> keys.add(k));
|
||||
}
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SecretKeyMetadata> getAesKeys(RealmModel realm, boolean includeDisabled) {
|
||||
List<SecretKeyMetadata> keys = new LinkedList<>();
|
||||
for (KeyProvider p : getProviders(realm)) {
|
||||
if (p instanceof AesKeyProvider) {
|
||||
if (includeDisabled) {
|
||||
keys.addAll(p.getKeyMetadata());
|
||||
} else {
|
||||
List<SecretKeyMetadata> metadata = p.getKeyMetadata();
|
||||
metadata.stream().filter(k -> k.getStatus() != KeyMetadata.Status.DISABLED).forEach(k -> keys.add(k));
|
||||
}
|
||||
}
|
||||
|
@ -199,6 +257,7 @@ public class DefaultKeyManager implements KeyManager {
|
|||
|
||||
boolean activeRsa = false;
|
||||
boolean activeHmac = false;
|
||||
boolean activeAes = false;
|
||||
|
||||
for (ComponentModel c : components) {
|
||||
try {
|
||||
|
@ -217,7 +276,13 @@ public class DefaultKeyManager implements KeyManager {
|
|||
if (r.getKid() != null && r.getSecretKey() != null) {
|
||||
activeHmac = true;
|
||||
}
|
||||
} else if (provider.getType().equals(AlgorithmType.AES)) {
|
||||
AesKeyProvider r = (AesKeyProvider) provider;
|
||||
if (r.getKid() != null && r.getSecretKey() != null) {
|
||||
activeAes = true;
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Throwable t) {
|
||||
logger.errorv(t, "Failed to load provider {0}", c.getId());
|
||||
}
|
||||
|
@ -231,6 +296,10 @@ public class DefaultKeyManager implements KeyManager {
|
|||
providers.add(new FailsafeHmacKeyProvider());
|
||||
}
|
||||
|
||||
if (!activeAes) {
|
||||
providers.add(new FailsafeAesKeyProvider());
|
||||
}
|
||||
|
||||
providersMap.put(realm.getId(), providers);
|
||||
}
|
||||
return providers;
|
||||
|
|
|
@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class FailsafeAesKeyProvider extends FailsafeSecretKeyProvider implements AesKeyProvider {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(FailsafeAesKeyProvider.class);
|
||||
|
||||
@Override
|
||||
protected Logger logger() {
|
||||
return logger;
|
||||
}
|
||||
}
|
|
@ -29,61 +29,12 @@ import java.util.List;
|
|||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
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<HmacKeyMetadata> getKeyMetadata() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
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<SecretKeyMetadata> getKeyMetadata() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
|
||||
protected abstract Logger logger();
|
||||
}
|
|
@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class GeneratedAesKeyProvider extends GeneratedSecretKeyProvider implements AesKeyProvider {
|
||||
|
||||
public GeneratedAesKeyProvider(ComponentModel model) {
|
||||
super(model);
|
||||
}
|
||||
|
||||
}
|
|
@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class GeneratedAesKeyProviderFactory extends GeneratedSecretKeyProviderFactory<AesKeyProvider> 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<ProviderConfigProperty> 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<ProviderConfigProperty> 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;
|
||||
}
|
||||
}
|
|
@ -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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
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<HmacKeyMetadata> 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ import java.util.List;
|
|||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public class GeneratedHmacKeyProviderFactory extends AbstractHmacKeyProviderFactory {
|
||||
public class GeneratedHmacKeyProviderFactory extends GeneratedSecretKeyProviderFactory<HmacKeyProvider> 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<ProviderConfigProperty> CONFIG_PROPERTIES = AbstractHmacKeyProviderFactory.configurationBuilder()
|
||||
public static final int DEFAULT_HMAC_KEY_SIZE = 32;
|
||||
|
||||
private static final List<ProviderConfigProperty> CONFIG_PROPERTIES = SecretKeyProviderUtils.configurationBuilder()
|
||||
.property(Attributes.SECRET_SIZE_PROPERTY)
|
||||
.build();
|
||||
|
||||
@Override
|
||||
public KeyProvider create(KeycloakSession session, ComponentModel model) {
|
||||
public HmacKeyProvider create(KeycloakSession session, ComponentModel model) {
|
||||
return new GeneratedHmacKeyProvider(model);
|
||||
}
|
||||
|
||||
|
@ -61,51 +63,18 @@ public class GeneratedHmacKeyProviderFactory extends AbstractHmacKeyProviderFact
|
|||
return CONFIG_PROPERTIES;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model) throws ComponentValidationException {
|
||||
ConfigurationValidationHelper.check(model).checkList(Attributes.SECRET_SIZE_PROPERTY, false);
|
||||
|
||||
int size = model.get(Attributes.SECRET_SIZE_KEY, 32);
|
||||
|
||||
if (!(model.contains(Attributes.SECRET_KEY))) {
|
||||
generateSecret(model, size);
|
||||
logger.debugv("Generated secret for {0}", realm.getName());
|
||||
} else {
|
||||
int currentSize = Base64Url.decode(model.get(Attributes.SECRET_KEY)).length;
|
||||
if (currentSize != size) {
|
||||
generateSecret(model, size);
|
||||
logger.debugv("Secret size changed, generating new secret for {0}", realm.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void generateSecret(ComponentModel model, int size) {
|
||||
try {
|
||||
byte[] secret = KeycloakModelUtils.generateSecret(size);
|
||||
model.put(Attributes.SECRET_KEY, Base64Url.encode(secret));
|
||||
|
||||
String kid = KeycloakModelUtils.generateId();
|
||||
model.put(Attributes.KID_KEY, kid);
|
||||
} catch (Throwable t) {
|
||||
throw new ComponentValidationException("Failed to generate secret", t);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Config.Scope config) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Logger logger() {
|
||||
return logger;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getDefaultKeySize() {
|
||||
return DEFAULT_HMAC_KEY_SIZE;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
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<SecretKeyMetadata> 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;
|
||||
}
|
||||
}
|
|
@ -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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public abstract class GeneratedSecretKeyProviderFactory<T extends KeyProvider> implements KeyProviderFactory<T> {
|
||||
|
||||
@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();
|
||||
}
|
|
@ -27,18 +27,17 @@ import org.keycloak.provider.ProviderConfigurationBuilder;
|
|||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
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);
|
|
@ -29,7 +29,6 @@ import org.keycloak.models.KeycloakSession;
|
|||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.protocol.LoginProtocol;
|
||||
import org.keycloak.protocol.RestartLoginCookie;
|
||||
import org.keycloak.protocol.oidc.utils.OIDCRedirectUriBuilder;
|
||||
import org.keycloak.protocol.oidc.utils.OIDCResponseMode;
|
||||
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
|
||||
|
@ -39,7 +38,6 @@ import org.keycloak.services.managers.AuthenticationManager;
|
|||
import org.keycloak.services.managers.AuthenticationSessionManager;
|
||||
import org.keycloak.services.managers.ClientSessionCode;
|
||||
import org.keycloak.services.managers.ResourceAdminManager;
|
||||
import org.keycloak.sessions.CommonClientSessionModel;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
import org.keycloak.util.TokenUtil;
|
||||
|
||||
|
@ -185,9 +183,10 @@ public class OIDCLoginProtocol implements LoginProtocol {
|
|||
redirectUri.addParam(OAuth2Constants.STATE, state);
|
||||
|
||||
// Standard or hybrid flow
|
||||
String code = null;
|
||||
if (responseType.hasResponseType(OIDCResponseType.CODE)) {
|
||||
accessCode.setAction(CommonClientSessionModel.Action.CODE_TO_TOKEN.name());
|
||||
redirectUri.addParam(OAuth2Constants.CODE, accessCode.getCode());
|
||||
code = accessCode.getOrGenerateCode();
|
||||
redirectUri.addParam(OAuth2Constants.CODE, code);
|
||||
}
|
||||
|
||||
// Implicit or hybrid flow
|
||||
|
@ -205,7 +204,7 @@ public class OIDCLoginProtocol implements LoginProtocol {
|
|||
}
|
||||
|
||||
if (responseType.hasResponseType(OIDCResponseType.CODE)) {
|
||||
responseBuilder.generateCodeHash(accessCode.getCode());
|
||||
responseBuilder.generateCodeHash(code);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -245,35 +245,27 @@ public class TokenEndpoint {
|
|||
throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Missing parameter: " + OAuth2Constants.CODE, Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
String[] parts = code.split("\\.");
|
||||
if (parts.length == 4) {
|
||||
event.detail(Details.CODE_ID, parts[2]);
|
||||
}
|
||||
|
||||
ClientSessionCode.ParseResult<AuthenticatedClientSessionModel> parseResult = ClientSessionCode.parseResult(code, session, realm, AuthenticatedClientSessionModel.class);
|
||||
ClientSessionCode.ParseResult<AuthenticatedClientSessionModel> parseResult = ClientSessionCode.parseResult(code, session, realm, event, AuthenticatedClientSessionModel.class);
|
||||
if (parseResult.isAuthSessionNotFound() || parseResult.isIllegalHash()) {
|
||||
event.error(Errors.INVALID_CODE);
|
||||
AuthenticatedClientSessionModel clientSession = parseResult.getClientSession();
|
||||
|
||||
// Attempt to use same code twice should invalidate existing clientSession
|
||||
AuthenticatedClientSessionModel clientSession = parseResult.getClientSession();
|
||||
if (clientSession != null) {
|
||||
clientSession.setUserSession(null);
|
||||
}
|
||||
|
||||
event.error(Errors.INVALID_CODE);
|
||||
|
||||
throw new ErrorResponseException(OAuthErrorException.INVALID_GRANT, "Code not valid", Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
AuthenticatedClientSessionModel clientSession = parseResult.getClientSession();
|
||||
|
||||
if (!parseResult.getCode().isValid(AuthenticatedClientSessionModel.Action.CODE_TO_TOKEN.name(), ClientSessionCode.ActionType.CLIENT)) {
|
||||
event.error(Errors.INVALID_CODE);
|
||||
if (parseResult.isExpiredToken()) {
|
||||
event.error(Errors.EXPIRED_CODE);
|
||||
throw new ErrorResponseException(OAuthErrorException.INVALID_GRANT, "Code is expired", Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
// TODO: This shouldn't be needed to write into the AuthenticatedClientSessionModel itself
|
||||
parseResult.getCode().setAction(null);
|
||||
|
||||
// TODO: Maybe rather create userSession even at this stage?
|
||||
UserSessionModel userSession = clientSession.getUserSession();
|
||||
|
||||
if (userSession == null) {
|
||||
|
@ -281,20 +273,20 @@ public class TokenEndpoint {
|
|||
throw new ErrorResponseException(OAuthErrorException.INVALID_GRANT, "User session not found", Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
|
||||
UserModel user = userSession.getUser();
|
||||
if (user == null) {
|
||||
event.error(Errors.USER_NOT_FOUND);
|
||||
throw new ErrorResponseException(OAuthErrorException.INVALID_GRANT, "User not found", Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
event.user(userSession.getUser());
|
||||
|
||||
if (!user.isEnabled()) {
|
||||
event.error(Errors.USER_DISABLED);
|
||||
throw new ErrorResponseException(OAuthErrorException.INVALID_GRANT, "User disabled", Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
event.user(userSession.getUser());
|
||||
|
||||
event.session(userSession.getId());
|
||||
|
||||
String redirectUri = clientSession.getNote(OIDCLoginProtocol.REDIRECT_URI_PARAM);
|
||||
String formParam = formParams.getFirst(OAuth2Constants.REDIRECT_URI);
|
||||
if (redirectUri != null && !redirectUri.equals(formParam)) {
|
||||
|
|
|
@ -849,7 +849,7 @@ public class AuthenticationManager {
|
|||
|
||||
return session.getProvider(LoginFormsProvider.class)
|
||||
.setExecution(execution)
|
||||
.setClientSessionCode(accessCode.getCode())
|
||||
.setClientSessionCode(accessCode.getOrGenerateCode())
|
||||
.setAccessRequest(realmRoles, resourceRoles, protocolMappers)
|
||||
.createOAuthGrant();
|
||||
} else {
|
||||
|
|
|
@ -17,19 +17,16 @@
|
|||
|
||||
package org.keycloak.services.managers;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.common.util.Base64Url;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.ClientTemplateModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.ProtocolMapperModel;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.sessions.CommonClientSessionModel;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
|
@ -39,10 +36,6 @@ import java.util.Set;
|
|||
*/
|
||||
public class ClientSessionCode<CLIENT_SESSION extends CommonClientSessionModel> {
|
||||
|
||||
private static final String ACTIVE_CODE = "active_code";
|
||||
|
||||
private static final Logger logger = Logger.getLogger(ClientSessionCode.class);
|
||||
|
||||
private KeycloakSession session;
|
||||
private final RealmModel realm;
|
||||
private final CLIENT_SESSION commonLoginSession;
|
||||
|
@ -63,6 +56,7 @@ public class ClientSessionCode<CLIENT_SESSION extends CommonClientSessionModel>
|
|||
ClientSessionCode<CLIENT_SESSION> code;
|
||||
boolean authSessionNotFound;
|
||||
boolean illegalHash;
|
||||
boolean expiredToken;
|
||||
CLIENT_SESSION clientSession;
|
||||
|
||||
public ClientSessionCode<CLIENT_SESSION> getCode() {
|
||||
|
@ -77,29 +71,39 @@ public class ClientSessionCode<CLIENT_SESSION extends CommonClientSessionModel>
|
|||
return illegalHash;
|
||||
}
|
||||
|
||||
public boolean isExpiredToken() {
|
||||
return expiredToken;
|
||||
}
|
||||
|
||||
public CLIENT_SESSION getClientSession() {
|
||||
return clientSession;
|
||||
}
|
||||
}
|
||||
|
||||
public static <CLIENT_SESSION extends CommonClientSessionModel> ParseResult<CLIENT_SESSION> parseResult(String code, KeycloakSession session, RealmModel realm, Class<CLIENT_SESSION> sessionClass) {
|
||||
public static <CLIENT_SESSION extends CommonClientSessionModel> ParseResult<CLIENT_SESSION> parseResult(String code, KeycloakSession session, RealmModel realm, EventBuilder event, Class<CLIENT_SESSION> sessionClass) {
|
||||
ParseResult<CLIENT_SESSION> result = new ParseResult<>();
|
||||
if (code == null) {
|
||||
result.illegalHash = true;
|
||||
return result;
|
||||
}
|
||||
try {
|
||||
result.clientSession = getClientSession(code, session, realm, sessionClass);
|
||||
CodeGenerateUtil.ClientSessionParser<CLIENT_SESSION> 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<CLIENT_SESSION>(session, realm, result.clientSession);
|
||||
return result;
|
||||
} catch (RuntimeException e) {
|
||||
|
@ -108,13 +112,19 @@ public class ClientSessionCode<CLIENT_SESSION extends CommonClientSessionModel>
|
|||
}
|
||||
}
|
||||
|
||||
public static <CLIENT_SESSION extends CommonClientSessionModel> CLIENT_SESSION getClientSession(String code, KeycloakSession session, RealmModel realm, Class<CLIENT_SESSION> sessionClass) {
|
||||
CommonClientSessionModel clientSessionn = CodeGenerateUtil.getParser(sessionClass).parseSession(code, session, realm);;
|
||||
CLIENT_SESSION clientSession = sessionClass.cast(clientSessionn);
|
||||
|
||||
return clientSession;
|
||||
public static <CLIENT_SESSION extends CommonClientSessionModel> CLIENT_SESSION getClientSession(String code, KeycloakSession session, RealmModel realm, EventBuilder event, Class<CLIENT_SESSION> sessionClass) {
|
||||
CodeGenerateUtil.ClientSessionParser<CLIENT_SESSION> clientSessionParser = CodeGenerateUtil.getParser(sessionClass);
|
||||
return getClientSession(code, session, realm, event, clientSessionParser);
|
||||
}
|
||||
|
||||
|
||||
private static <CLIENT_SESSION extends CommonClientSessionModel> CLIENT_SESSION getClientSession(String code, KeycloakSession session, RealmModel realm, EventBuilder event,
|
||||
CodeGenerateUtil.ClientSessionParser<CLIENT_SESSION> clientSessionParser) {
|
||||
return clientSessionParser.parseSession(code, session, realm, event);
|
||||
}
|
||||
|
||||
|
||||
public CLIENT_SESSION getClientSession() {
|
||||
return commonLoginSession;
|
||||
}
|
||||
|
@ -203,52 +213,9 @@ public class ClientSessionCode<CLIENT_SESSION extends CommonClientSessionModel>
|
|||
commonLoginSession.setTimestamp(Time.currentTime());
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
public String getOrGenerateCode() {
|
||||
CodeGenerateUtil.ClientSessionParser parser = CodeGenerateUtil.getParser(commonLoginSession.getClass());
|
||||
String nextCode = parser.getNote(commonLoginSession, ACTIVE_CODE);
|
||||
if (nextCode == null) {
|
||||
nextCode = generateCode(commonLoginSession);
|
||||
} else {
|
||||
logger.debug("Code already generated for session, using same code");
|
||||
}
|
||||
return nextCode;
|
||||
return parser.retrieveCode(session, commonLoginSession);
|
||||
}
|
||||
|
||||
private static String generateCode(CommonClientSessionModel authSession) {
|
||||
try {
|
||||
String actionId = Base64Url.encode(KeycloakModelUtils.generateSecret());
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(actionId);
|
||||
sb.append('.');
|
||||
sb.append(authSession.getId());
|
||||
|
||||
CodeGenerateUtil.ClientSessionParser parser = CodeGenerateUtil.getParser(authSession.getClass());
|
||||
|
||||
String code = parser.generateCode(authSession, actionId);
|
||||
parser.setNote(authSession, ACTIVE_CODE, code);
|
||||
|
||||
return code;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean verifyCode(String code, CommonClientSessionModel authSession) {
|
||||
try {
|
||||
CodeGenerateUtil.ClientSessionParser parser = CodeGenerateUtil.getParser(authSession.getClass());
|
||||
|
||||
String activeCode = parser.getNote(authSession, ACTIVE_CODE);
|
||||
if (activeCode == null) {
|
||||
logger.debug("Active code not found in client session");
|
||||
return false;
|
||||
}
|
||||
|
||||
parser.removeNote(authSession, ACTIVE_CODE);
|
||||
|
||||
return MessageDigest.isEqual(code.getBytes(), activeCode.getBytes());
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Class<? extends CommonClientSessionModel>, ClientSessionParser> PARSERS = new HashMap<>();
|
||||
private static final String ACTIVE_CODE = "active_code";
|
||||
|
||||
private static final Map<Class<? extends CommonClientSessionModel>, Supplier<ClientSessionParser>> 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 <CS extends CommonClientSessionModel> ClientSessionParser<CS> getParser(Class<CS> clientSessionClass) {
|
||||
for (Class<?> c : PARSERS.keySet()) {
|
||||
if (c.isAssignableFrom(clientSessionClass)) {
|
||||
return PARSERS.get(c);
|
||||
return PARSERS.get(c).get();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
@ -57,17 +78,15 @@ class CodeGenerateUtil {
|
|||
|
||||
interface ClientSessionParser<CS extends CommonClientSessionModel> {
|
||||
|
||||
CS parseSession(String code, KeycloakSession session, RealmModel realm);
|
||||
CS parseSession(String code, KeycloakSession session, RealmModel realm, EventBuilder event);
|
||||
|
||||
String generateCode(CS clientSession, String actionId);
|
||||
String retrieveCode(KeycloakSession session, CS clientSession);
|
||||
|
||||
void removeExpiredSession(KeycloakSession session, CS clientSession);
|
||||
|
||||
String getNote(CS clientSession, String name);
|
||||
boolean verifyCode(KeycloakSession session, String code, CS clientSession);
|
||||
|
||||
void removeNote(CS clientSession, String name);
|
||||
|
||||
void setNote(CS clientSession, String name, String value);
|
||||
boolean isExpired(KeycloakSession session, String code, CS clientSession);
|
||||
|
||||
}
|
||||
|
||||
|
@ -78,95 +97,149 @@ class CodeGenerateUtil {
|
|||
private static class AuthenticationSessionModelParser implements ClientSessionParser<AuthenticationSessionModel> {
|
||||
|
||||
@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<AuthenticatedClientSessionModel> {
|
||||
|
||||
private CodeJWT codeJWT;
|
||||
|
||||
@Override
|
||||
public AuthenticatedClientSessionModel parseSession(String code, KeycloakSession session, RealmModel realm) {
|
||||
public AuthenticatedClientSessionModel parseSession(String code, KeycloakSession session, RealmModel realm, EventBuilder event) {
|
||||
SecretKey aesKey = session.keys().getActiveAesKey(realm).getSecretKey();
|
||||
SecretKey hmacKey = session.keys().getActiveHmacKey(realm).getSecretKey();
|
||||
|
||||
try {
|
||||
String[] parts = code.split("\\.");
|
||||
String userSessionId = parts[2];
|
||||
String clientUUID = parts[3];
|
||||
|
||||
UserSessionModel userSession = new UserSessionCrossDCManager(session).getUserSessionWithClientAndCodeToTokenAction(realm, userSessionId, clientUUID);
|
||||
if (userSession == null) {
|
||||
// TODO:mposolda Temporary workaround needed to track if code is invalid or was already used. Will be good to remove once used OAuth codes are tracked through one-time cache
|
||||
userSession = session.sessions().getUserSession(realm, userSessionId);
|
||||
if (userSession == null) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return userSession.getAuthenticatedClientSessions().get(clientUUID);
|
||||
} catch (ArrayIndexOutOfBoundsException e) {
|
||||
codeJWT = TokenUtil.jweDirectVerifyAndDecode(aesKey, hmacKey, code, CodeJWT.class);
|
||||
} catch (JWEException jweException) {
|
||||
logger.error("Exception during JWE Verification or decode", jweException);
|
||||
return null;
|
||||
}
|
||||
|
||||
event.detail(Details.CODE_ID, codeJWT.getUserSessionId());
|
||||
event.session(codeJWT.getUserSessionId());
|
||||
|
||||
UserSessionModel userSession = new UserSessionCrossDCManager(session).getUserSessionWithClient(realm, codeJWT.getUserSessionId(), codeJWT.getIssuedFor());
|
||||
if (userSession == null) {
|
||||
// TODO:mposolda Temporary workaround needed to track if code is invalid or was already used. Will be good to remove once used OAuth codes are tracked through one-time cache
|
||||
userSession = session.sessions().getUserSession(realm, codeJWT.getUserSessionId());
|
||||
if (userSession == null) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return userSession.getAuthenticatedClientSessions().get(codeJWT.getIssuedFor());
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String retrieveCode(KeycloakSession session, AuthenticatedClientSessionModel clientSession) {
|
||||
String actionId = KeycloakModelUtils.generateId();
|
||||
|
||||
CodeJWT codeJWT = new CodeJWT();
|
||||
codeJWT.id(actionId);
|
||||
codeJWT.issuedFor(clientSession.getClient().getId());
|
||||
codeJWT.userSessionId(clientSession.getUserSession().getId());
|
||||
|
||||
RealmModel realm = clientSession.getRealm();
|
||||
|
||||
int issuedAt = Time.currentTime();
|
||||
codeJWT.issuedAt(issuedAt);
|
||||
codeJWT.expiration(issuedAt + realm.getAccessCodeLifespan());
|
||||
|
||||
SecretKey aesKey = session.keys().getActiveAesKey(realm).getSecretKey();
|
||||
SecretKey hmacKey = session.keys().getActiveHmacKey(realm).getSecretKey();
|
||||
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.tracef("Using AES key of length '%d' bytes and HMAC key of length '%d' bytes . Client: '%s', User Session: '%s'", aesKey.getEncoded().length,
|
||||
hmacKey.getEncoded().length, clientSession.getClient().getClientId(), clientSession.getUserSession().getId());
|
||||
}
|
||||
|
||||
try {
|
||||
return TokenUtil.jweDirectEncode(aesKey, hmacKey, codeJWT);
|
||||
} catch (JWEException jweEx) {
|
||||
throw new RuntimeException(jweEx);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generateCode(AuthenticatedClientSessionModel clientSession, String actionId) {
|
||||
String userSessionId = clientSession.getUserSession().getId();
|
||||
String clientUUID = clientSession.getClient().getId();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("uss.");
|
||||
sb.append(actionId);
|
||||
sb.append('.');
|
||||
sb.append(userSessionId);
|
||||
sb.append('.');
|
||||
sb.append(clientUUID);
|
||||
|
||||
return sb.toString();
|
||||
@Override
|
||||
public boolean verifyCode(KeycloakSession session, String code, AuthenticatedClientSessionModel clientSession) {
|
||||
if (codeJWT == null) {
|
||||
throw new IllegalStateException("Illegal use. codeJWT not yet set");
|
||||
}
|
||||
|
||||
UUID codeId = UUID.fromString(codeJWT.getId());
|
||||
CodeToTokenStoreProvider singleUseCache = session.getProvider(CodeToTokenStoreProvider.class);
|
||||
|
||||
if (singleUseCache.putIfAbsent(codeId)) {
|
||||
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.tracef("Added code '%s' to single-use cache. User session: %s, client: %s", codeJWT.getId(), codeJWT.getUserSessionId(), codeJWT.getIssuedFor());
|
||||
}
|
||||
|
||||
return true;
|
||||
} else {
|
||||
logger.warnf("Code '%s' already used for userSession '%s' and client '%s'.", codeJWT.getId(), codeJWT.getUserSessionId(), codeJWT.getIssuedFor());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void removeExpiredSession(KeycloakSession session, AuthenticatedClientSessionModel clientSession) {
|
||||
throw new IllegalStateException("Not yet implemented");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getNote(AuthenticatedClientSessionModel clientSession, String name) {
|
||||
return clientSession.getNote(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeNote(AuthenticatedClientSessionModel clientSession, String name) {
|
||||
clientSession.removeNote(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setNote(AuthenticatedClientSessionModel clientSession, String name, String value) {
|
||||
clientSession.setNote(name, value);
|
||||
public boolean isExpired(KeycloakSession session, String code, AuthenticatedClientSessionModel clientSession) {
|
||||
return !codeJWT.isActive();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -46,19 +46,14 @@ public class UserSessionCrossDCManager {
|
|||
}
|
||||
|
||||
|
||||
// get userSession if it has "authenticatedClientSession" of specified client attached to it and there is "CODE_TO_TOKEN" action. Otherwise download it from remoteCache
|
||||
// get userSession if it has "authenticatedClientSession" of specified client attached to it. Otherwise download it from remoteCache
|
||||
// TODO Probably remove this method once AuthenticatedClientSession.getAction is removed and information is moved to OAuth code JWT instead
|
||||
public UserSessionModel getUserSessionWithClientAndCodeToTokenAction(RealmModel realm, String id, String clientUUID) {
|
||||
public UserSessionModel getUserSessionWithClient(RealmModel realm, String id, String clientUUID) {
|
||||
|
||||
return kcSession.sessions().getUserSessionWithPredicate(realm, id, false, (UserSessionModel userSession) -> {
|
||||
|
||||
Map<String, AuthenticatedClientSessionModel> 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);
|
||||
|
||||
});
|
||||
}
|
||||
|
|
|
@ -297,7 +297,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
|
|||
|
||||
ClientSessionCode<AuthenticationSessionModel> clientSessionCode = new ClientSessionCode<>(session, realmModel, authSession);
|
||||
clientSessionCode.setAction(AuthenticationSessionModel.Action.AUTHENTICATE.name());
|
||||
clientSessionCode.getCode();
|
||||
clientSessionCode.getOrGenerateCode();
|
||||
authSession.setProtocol(client.getProtocol());
|
||||
authSession.setRedirectUri(redirectUri);
|
||||
authSession.setClientNote(OIDCLoginProtocol.STATE_PARAM, UUID.randomUUID().toString());
|
||||
|
@ -1046,7 +1046,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
|
|||
|
||||
if (clientSessionCode != null) {
|
||||
authSession = clientSessionCode.getClientSession();
|
||||
String relayState = clientSessionCode.getCode();
|
||||
String relayState = clientSessionCode.getOrGenerateCode();
|
||||
encodedState = IdentityBrokerState.decoded(relayState, authSession.getClient().getClientId());
|
||||
}
|
||||
|
||||
|
|
|
@ -740,8 +740,8 @@ public class LoginActionsService {
|
|||
authSession.setTimestamp(Time.currentTime());
|
||||
|
||||
String clientId = authSession.getClient().getClientId();
|
||||
URI redirect = firstBrokerLogin ? Urls.identityProviderAfterFirstBrokerLogin(uriInfo.getBaseUri(), realm.getName(), accessCode.getCode(), clientId) :
|
||||
Urls.identityProviderAfterPostBrokerLogin(uriInfo.getBaseUri(), realm.getName(), accessCode.getCode(), clientId) ;
|
||||
URI redirect = firstBrokerLogin ? Urls.identityProviderAfterFirstBrokerLogin(uriInfo.getBaseUri(), realm.getName(), accessCode.getOrGenerateCode(), clientId) :
|
||||
Urls.identityProviderAfterPostBrokerLogin(uriInfo.getBaseUri(), realm.getName(), accessCode.getOrGenerateCode(), clientId) ;
|
||||
logger.debugf("Redirecting to '%s' ", redirect);
|
||||
|
||||
return Response.status(302).location(redirect).build();
|
||||
|
|
|
@ -133,7 +133,7 @@ public class SessionCodeChecks {
|
|||
}
|
||||
|
||||
// object retrieve
|
||||
AuthenticationSessionModel authSession = ClientSessionCode.getClientSession(code, session, realm, AuthenticationSessionModel.class);
|
||||
AuthenticationSessionModel authSession = ClientSessionCode.getClientSession(code, session, realm, event, AuthenticationSessionModel.class);
|
||||
if (authSession != null) {
|
||||
return authSession;
|
||||
}
|
||||
|
@ -240,7 +240,7 @@ public class SessionCodeChecks {
|
|||
return false;
|
||||
}
|
||||
} else {
|
||||
ClientSessionCode.ParseResult<AuthenticationSessionModel> result = ClientSessionCode.parseResult(code, session, realm, AuthenticationSessionModel.class);
|
||||
ClientSessionCode.ParseResult<AuthenticationSessionModel> result = ClientSessionCode.parseResult(code, session, realm, event, AuthenticationSessionModel.class);
|
||||
clientCode = result.getCode();
|
||||
if (clientCode == null) {
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ package org.keycloak.services.resources.admin;
|
|||
import org.jboss.resteasy.annotations.cache.NoCache;
|
||||
import org.keycloak.common.util.PemUtils;
|
||||
import org.keycloak.jose.jws.AlgorithmType;
|
||||
import org.keycloak.keys.HmacKeyMetadata;
|
||||
import org.keycloak.keys.SecretKeyMetadata;
|
||||
import org.keycloak.keys.RsaKeyMetadata;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeyManager;
|
||||
|
@ -65,6 +65,7 @@ public class KeyResource {
|
|||
Map<String, String> 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<KeysMetadataRepresentation.KeyMetadataRepresentation> l = new LinkedList<>();
|
||||
|
@ -79,7 +80,7 @@ public class KeyResource {
|
|||
r.setCertificate(PemUtils.encodeCertificate(m.getCertificate()));
|
||||
l.add(r);
|
||||
}
|
||||
for (HmacKeyMetadata m : session.keys().getHmacKeys(realm, true)) {
|
||||
for (SecretKeyMetadata m : session.keys().getHmacKeys(realm, true)) {
|
||||
KeysMetadataRepresentation.KeyMetadataRepresentation r = new KeysMetadataRepresentation.KeyMetadataRepresentation();
|
||||
r.setProviderId(m.getProviderId());
|
||||
r.setProviderPriority(m.getProviderPriority());
|
||||
|
@ -88,6 +89,15 @@ public class KeyResource {
|
|||
r.setType(AlgorithmType.HMAC.name());
|
||||
l.add(r);
|
||||
}
|
||||
for (SecretKeyMetadata m : session.keys().getAesKeys(realm, true)) {
|
||||
KeysMetadataRepresentation.KeyMetadataRepresentation r = new KeysMetadataRepresentation.KeyMetadataRepresentation();
|
||||
r.setProviderId(m.getProviderId());
|
||||
r.setProviderPriority(m.getProviderPriority());
|
||||
r.setKid(m.getKid());
|
||||
r.setStatus(m.getStatus() != null ? m.getStatus().name() : null);
|
||||
r.setType(AlgorithmType.AES.name());
|
||||
l.add(r);
|
||||
}
|
||||
|
||||
keys.setKeys(l);
|
||||
|
||||
|
|
|
@ -77,7 +77,7 @@ public class TwitterIdentityProvider extends AbstractIdentityProvider<OAuth2Iden
|
|||
|
||||
@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 @@ public class TwitterIdentityProvider extends AbstractIdentityProvider<OAuth2Iden
|
|||
protected class Endpoint {
|
||||
protected RealmModel realm;
|
||||
protected AuthenticationCallback callback;
|
||||
protected EventBuilder event;
|
||||
|
||||
@Context
|
||||
protected KeycloakSession session;
|
||||
|
@ -174,9 +175,12 @@ public class TwitterIdentityProvider extends AbstractIdentityProvider<OAuth2Iden
|
|||
@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 class TwitterIdentityProvider extends AbstractIdentityProvider<OAuth2Iden
|
|||
|
||||
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 class TwitterIdentityProvider extends AbstractIdentityProvider<OAuth2Iden
|
|||
}
|
||||
|
||||
private void sendErrorEvent() {
|
||||
EventBuilder event = new EventBuilder(realm, session, clientConnection);
|
||||
event.event(EventType.LOGIN);
|
||||
event.error("twitter_login_failed");
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
#
|
||||
|
||||
org.keycloak.keys.GeneratedHmacKeyProviderFactory
|
||||
org.keycloak.keys.GeneratedAesKeyProviderFactory
|
||||
org.keycloak.keys.GeneratedRsaKeyProviderFactory
|
||||
org.keycloak.keys.JavaKeystoreKeyProviderFactory
|
||||
org.keycloak.keys.ImportedRsaKeyProviderFactory
|
|
@ -20,6 +20,7 @@ package org.keycloak.testsuite.rest.resource;
|
|||
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 class TestCacheResource {
|
|||
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")
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -107,7 +107,7 @@ public class ConcurrentLoginTest extends AbstractConcurrencyTest {
|
|||
LoginTask loginTask = null;
|
||||
|
||||
try (CloseableHttpClient httpClient = HttpClientBuilder.create().setRedirectStrategy(new LaxRedirectStrategy()).build()) {
|
||||
loginTask = new LoginTask(httpClient, userSessionId, 100, 1, Arrays.asList(
|
||||
loginTask = new LoginTask(httpClient, userSessionId, 100, 1, false, Arrays.asList(
|
||||
createHttpClientContextForUser(httpClient, "test-user@localhost", "password")
|
||||
));
|
||||
run(DEFAULT_THREADS, DEFAULT_CLIENTS_COUNT, loginTask);
|
||||
|
@ -131,6 +131,29 @@ public class ConcurrentLoginTest extends AbstractConcurrencyTest {
|
|||
return context;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void concurrentLoginSingleUserSingleClient() throws Throwable {
|
||||
log.info("*********************************************");
|
||||
long start = System.currentTimeMillis();
|
||||
|
||||
AtomicReference<String> userSessionId = new AtomicReference<>();
|
||||
LoginTask loginTask = null;
|
||||
|
||||
try (CloseableHttpClient httpClient = HttpClientBuilder.create().setRedirectStrategy(new LaxRedirectStrategy()).build()) {
|
||||
loginTask = new LoginTask(httpClient, userSessionId, 100, 1, true, Arrays.asList(
|
||||
createHttpClientContextForUser(httpClient, "test-user@localhost", "password")
|
||||
));
|
||||
run(DEFAULT_THREADS, DEFAULT_CLIENTS_COUNT, loginTask);
|
||||
int clientSessionsCount = testingClient.testing().getClientSessionsCountInUserSession("test", userSessionId.get());
|
||||
Assert.assertEquals(2, clientSessionsCount);
|
||||
} finally {
|
||||
long end = System.currentTimeMillis() - start;
|
||||
log.infof("Statistics: %s", loginTask == null ? "??" : loginTask.getHistogram());
|
||||
log.info("concurrentLoginSingleUserSingleClient took " + (end/1000) + "s");
|
||||
log.info("*********************************************");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void concurrentLoginMultipleUsers() throws Throwable {
|
||||
log.info("*********************************************");
|
||||
|
@ -140,7 +163,7 @@ public class ConcurrentLoginTest extends AbstractConcurrencyTest {
|
|||
LoginTask loginTask = null;
|
||||
|
||||
try (CloseableHttpClient httpClient = HttpClientBuilder.create().setRedirectStrategy(new LaxRedirectStrategy()).build()) {
|
||||
loginTask = new LoginTask(httpClient, userSessionId, 100, 1, Arrays.asList(
|
||||
loginTask = new LoginTask(httpClient, userSessionId, 100, 1, false, Arrays.asList(
|
||||
createHttpClientContextForUser(httpClient, "test-user@localhost", "password"),
|
||||
createHttpClientContextForUser(httpClient, "john-doh@localhost", "password"),
|
||||
createHttpClientContextForUser(httpClient, "roleRichUser", "password")
|
||||
|
@ -157,6 +180,60 @@ public class ConcurrentLoginTest extends AbstractConcurrencyTest {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void concurrentCodeReuseShouldFail() throws Throwable {
|
||||
log.info("*********************************************");
|
||||
long start = System.currentTimeMillis();
|
||||
|
||||
|
||||
for (int i=0 ; i<10 ; i++) {
|
||||
OAuthClient oauth1 = new OAuthClient();
|
||||
oauth1.init(adminClient, driver);
|
||||
oauth1.clientId("client0");
|
||||
|
||||
OAuthClient.AuthorizationEndpointResponse resp = oauth1.doLogin("test-user@localhost", "password");
|
||||
String code = resp.getCode();
|
||||
Assert.assertNotNull(code);
|
||||
String codeURL = driver.getCurrentUrl();
|
||||
|
||||
|
||||
AtomicInteger codeToTokenSuccessCount = new AtomicInteger(0);
|
||||
AtomicInteger codeToTokenErrorsCount = new AtomicInteger(0);
|
||||
|
||||
KeycloakRunnable codeToTokenTask = new KeycloakRunnable() {
|
||||
|
||||
@Override
|
||||
public void run(int threadIndex, Keycloak keycloak, RealmResource realm) throws Throwable {
|
||||
log.infof("Trying to execute codeURL: %s, threadIndex: %i", codeURL, threadIndex);
|
||||
|
||||
OAuthClient.AccessTokenResponse resp = oauth1.doAccessTokenRequest(code, "password");
|
||||
if (resp.getAccessToken() != null && resp.getError() == null) {
|
||||
codeToTokenSuccessCount.incrementAndGet();
|
||||
} else if (resp.getAccessToken() == null && resp.getError() != null) {
|
||||
codeToTokenErrorsCount.incrementAndGet();
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
run(DEFAULT_THREADS, DEFAULT_THREADS, codeToTokenTask);
|
||||
|
||||
oauth1.openLogout();
|
||||
|
||||
Assert.assertEquals(1, codeToTokenSuccessCount.get());
|
||||
Assert.assertEquals(DEFAULT_THREADS - 1, codeToTokenErrorsCount.get());
|
||||
|
||||
log.infof("Iteration %i passed successfully", i);
|
||||
}
|
||||
|
||||
long end = System.currentTimeMillis() - start;
|
||||
log.info("concurrentCodeReuseShouldFail took " + (end/1000) + "s");
|
||||
log.info("*********************************************");
|
||||
|
||||
}
|
||||
|
||||
|
||||
protected String getPageContent(String url, CloseableHttpClient httpClient, HttpClientContext context) throws IOException {
|
||||
HttpGet request = new HttpGet(url);
|
||||
|
||||
|
@ -237,6 +314,7 @@ public class ConcurrentLoginTest extends AbstractConcurrencyTest {
|
|||
return m;
|
||||
}
|
||||
|
||||
|
||||
public class LoginTask implements KeycloakRunnable {
|
||||
|
||||
private final AtomicInteger clientIndex = new AtomicInteger();
|
||||
|
@ -256,9 +334,10 @@ public class ConcurrentLoginTest extends AbstractConcurrencyTest {
|
|||
private final int retryCount;
|
||||
private final AtomicInteger[] retryHistogram;
|
||||
private final AtomicInteger totalInvocations = new AtomicInteger();
|
||||
private final boolean sameClient;
|
||||
private final List<HttpClientContext> clientContexts;
|
||||
|
||||
public LoginTask(CloseableHttpClient httpClient, AtomicReference<String> userSessionId, int retryDelayMs, int retryCount, List<HttpClientContext> clientContexts) {
|
||||
public LoginTask(CloseableHttpClient httpClient, AtomicReference<String> userSessionId, int retryDelayMs, int retryCount, boolean sameClient, List<HttpClientContext> clientContexts) {
|
||||
this.httpClient = httpClient;
|
||||
this.userSessionId = userSessionId;
|
||||
this.retryDelayMs = retryDelayMs;
|
||||
|
@ -267,12 +346,13 @@ public class ConcurrentLoginTest extends AbstractConcurrencyTest {
|
|||
for (int i = 0; i < retryHistogram.length; i ++) {
|
||||
retryHistogram[i] = new AtomicInteger();
|
||||
}
|
||||
this.sameClient = sameClient;
|
||||
this.clientContexts = clientContexts;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(int threadIndex, Keycloak keycloak, RealmResource realm) throws Throwable {
|
||||
int i = clientIndex.getAndIncrement();
|
||||
int i = sameClient ? 0 : clientIndex.getAndIncrement();
|
||||
OAuthClient oauth1 = oauthClient.get();
|
||||
oauth1.clientId("client" + i);
|
||||
log.infof("%d [%s]: Accessing login page for %s", threadIndex, Thread.currentThread().getName(), oauth1.getClientId());
|
||||
|
|
|
@ -75,7 +75,7 @@ public class ConcurrentLoginCrossDCTest extends ConcurrentLoginTest {
|
|||
LoginTask loginTask = null;
|
||||
|
||||
try (CloseableHttpClient httpClient = HttpClientBuilder.create().setRedirectStrategy(new LaxRedirectStrategy()).build()) {
|
||||
loginTask = new LoginTask(httpClient, userSessionId, LOGIN_TASK_DELAY_MS, LOGIN_TASK_RETRIES, Arrays.asList(
|
||||
loginTask = new LoginTask(httpClient, userSessionId, LOGIN_TASK_DELAY_MS, LOGIN_TASK_RETRIES, false, Arrays.asList(
|
||||
createHttpClientContextForUser(httpClient, "test-user@localhost", "password")
|
||||
));
|
||||
HttpUriRequest request = handleLogin(getPageContent(oauth.getLoginFormUrl(), httpClient, HttpClientContext.create()), "test-user@localhost", "password");
|
||||
|
|
|
@ -168,7 +168,7 @@ public class GeneratedHmacKeyProviderTest extends AbstractKeycloakTest {
|
|||
rep.getConfig().putSingle("secretSize", "1234");
|
||||
|
||||
Response response = adminClient.realm("test").components().add(rep);
|
||||
assertErrror(response, "'Secret size' should be 32, 64, 128, 256 or 512");
|
||||
assertErrror(response, "'Secret size' should be 16, 24, 32, 64, 128, 256 or 512");
|
||||
}
|
||||
|
||||
protected void assertErrror(Response response, String error) {
|
||||
|
|
|
@ -307,7 +307,6 @@ public class AccessTokenTest extends AbstractKeycloakTest {
|
|||
events.expectCodeToToken(codeId, sessionId)
|
||||
.removeDetail(Details.TOKEN_ID)
|
||||
.user((String) null)
|
||||
.session((String) null)
|
||||
.removeDetail(Details.REFRESH_TOKEN_ID)
|
||||
.removeDetail(Details.REFRESH_TOKEN_TYPE)
|
||||
.error(Errors.INVALID_CODE).assertEvent();
|
||||
|
@ -334,8 +333,8 @@ public class AccessTokenTest extends AbstractKeycloakTest {
|
|||
|
||||
setTimeOffset(0);
|
||||
|
||||
AssertEvents.ExpectedEvent expectedEvent = events.expectCodeToToken(codeId, null);
|
||||
expectedEvent.error("invalid_code")
|
||||
AssertEvents.ExpectedEvent expectedEvent = events.expectCodeToToken(codeId, codeId);
|
||||
expectedEvent.error("expired_code")
|
||||
.removeDetail(Details.TOKEN_ID)
|
||||
.removeDetail(Details.REFRESH_TOKEN_ID)
|
||||
.removeDetail(Details.REFRESH_TOKEN_TYPE)
|
||||
|
@ -380,7 +379,7 @@ public class AccessTokenTest extends AbstractKeycloakTest {
|
|||
response = oauth.doAccessTokenRequest(code, "password");
|
||||
Assert.assertEquals(400, response.getStatusCode());
|
||||
|
||||
AssertEvents.ExpectedEvent expectedEvent = events.expectCodeToToken(codeId, null);
|
||||
AssertEvents.ExpectedEvent expectedEvent = events.expectCodeToToken(codeId, codeId);
|
||||
expectedEvent.error("invalid_code")
|
||||
.removeDetail(Details.TOKEN_ID)
|
||||
.removeDetail(Details.REFRESH_TOKEN_ID)
|
||||
|
|
|
@ -22,6 +22,7 @@ import org.junit.Rule;
|
|||
import org.junit.Test;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.OAuthErrorException;
|
||||
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.models.Constants;
|
||||
|
@ -73,7 +74,6 @@ public class AuthorizationCodeTest extends AbstractKeycloakTest {
|
|||
Assert.assertNull(response.getError());
|
||||
|
||||
String codeId = events.expectLogin().assertEvent().getDetails().get(Details.CODE_ID);
|
||||
assertCode(codeId, response.getCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -89,7 +89,6 @@ public class AuthorizationCodeTest extends AbstractKeycloakTest {
|
|||
String code = driver.findElement(By.id(OAuth2Constants.CODE)).getAttribute("value");
|
||||
|
||||
String codeId = events.expectLogin().detail(Details.REDIRECT_URI, "http://localhost:8180/auth/realms/test/protocol/openid-connect/oauth/oob").assertEvent().getDetails().get(Details.CODE_ID);
|
||||
assertCode(codeId, code);
|
||||
|
||||
ClientManager.realm(adminClient.realm("test")).clientId("test-app").removeRedirectUris(Constants.INSTALLED_APP_URN);
|
||||
}
|
||||
|
@ -104,7 +103,6 @@ public class AuthorizationCodeTest extends AbstractKeycloakTest {
|
|||
Assert.assertNotNull(response.getCode());
|
||||
|
||||
String codeId = events.expectLogin().assertEvent().getDetails().get(Details.CODE_ID);
|
||||
assertCode(codeId, response.getCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -119,7 +117,6 @@ public class AuthorizationCodeTest extends AbstractKeycloakTest {
|
|||
Assert.assertNull(response.getError());
|
||||
|
||||
String codeId = events.expectLogin().assertEvent().getDetails().get(Details.CODE_ID);
|
||||
assertCode(codeId, response.getCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -151,11 +148,6 @@ public class AuthorizationCodeTest extends AbstractKeycloakTest {
|
|||
assertEquals("OpenIdConnect.AuthenticationProperties=2302984sdlk", state);
|
||||
|
||||
String codeId = events.expectLogin().assertEvent().getDetails().get(Details.CODE_ID);
|
||||
assertCode(codeId, code);
|
||||
}
|
||||
|
||||
private void assertCode(String expectedCodeId, String actualCode) {
|
||||
assertEquals(expectedCodeId, actualCode.split("\\.")[2]);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -33,6 +33,7 @@ import org.keycloak.models.UserSessionModel;
|
|||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.models.UserManager;
|
||||
import org.keycloak.sessions.CommonClientSessionModel;
|
||||
import org.keycloak.testsuite.rule.KeycloakRule;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
@ -169,14 +170,14 @@ public class UserSessionProviderTest {
|
|||
int time = clientSession.getTimestamp();
|
||||
assertEquals(null, clientSession.getAction());
|
||||
|
||||
clientSession.setAction(AuthenticatedClientSessionModel.Action.CODE_TO_TOKEN.name());
|
||||
clientSession.setAction(AuthenticatedClientSessionModel.Action.LOGGED_OUT.name());
|
||||
clientSession.setTimestamp(time + 10);
|
||||
|
||||
kc.stopSession(session, true);
|
||||
session = kc.startSession();
|
||||
|
||||
AuthenticatedClientSessionModel updated = session.sessions().getUserSession(realm, userSessionId).getAuthenticatedClientSessions().get(clientUUID);
|
||||
assertEquals(AuthenticatedClientSessionModel.Action.CODE_TO_TOKEN.name(), updated.getAction());
|
||||
assertEquals(AuthenticatedClientSessionModel.Action.LOGGED_OUT.name(), updated.getAction());
|
||||
assertEquals(time + 10, updated.getTimestamp());
|
||||
}
|
||||
|
||||
|
@ -190,11 +191,11 @@ public class UserSessionProviderTest {
|
|||
UserSessionModel userSession = session.sessions().getUserSession(realm, userSessionId);
|
||||
AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(clientUUID);
|
||||
|
||||
clientSession.setAction(AuthenticatedClientSessionModel.Action.CODE_TO_TOKEN.name());
|
||||
clientSession.setAction(AuthenticatedClientSessionModel.Action.LOGGED_OUT.name());
|
||||
clientSession.setNote("foo", "bar");
|
||||
|
||||
AuthenticatedClientSessionModel updated = session.sessions().getUserSession(realm, userSessionId).getAuthenticatedClientSessions().get(clientUUID);
|
||||
assertEquals(AuthenticatedClientSessionModel.Action.CODE_TO_TOKEN.name(), updated.getAction());
|
||||
assertEquals(AuthenticatedClientSessionModel.Action.LOGGED_OUT.name(), updated.getAction());
|
||||
assertEquals("bar", updated.getNote("foo"));
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue