KEYCLOAK-5007 Used single-use cache for tracke OAuth code. OAuth code changed to be encrypted and signed JWT

This commit is contained in:
mposolda 2017-09-26 09:31:15 +02:00
parent 63673c4328
commit 3b6e1f4e93
69 changed files with 1574 additions and 464 deletions

View file

@ -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) {

View file

@ -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());
}

View file

@ -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);
}
}

View 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);
}
}

View file

@ -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();

View file

@ -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];
}
}

View file

@ -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;
}

View file

@ -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;
/**

View file

@ -25,6 +25,7 @@ public enum AlgorithmType {
RSA,
HMAC,
AES,
ECDSA
}

View 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;
}
}

View file

@ -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);
}
}
}

View file

@ -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";

View file

@ -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;
}
}
};

View file

@ -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() {
}
}

View file

@ -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";
}
}

View file

@ -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

View file

@ -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;

View file

@ -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

View file

@ -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";
}
}

View file

@ -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);
}
}

View file

@ -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";
}
}

View file

@ -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() {

View file

@ -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();
}

View file

@ -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) {

View file

@ -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;
}
}

View file

@ -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 {

View file

@ -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);
}

View file

@ -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> {
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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

View file

@ -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 {
}

View file

@ -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;
}
}
}

View file

@ -56,7 +56,6 @@ public interface CommonClientSessionModel {
public static enum Action {
OAUTH_GRANT,
CODE_TO_TOKEN,
AUTHENTICATE,
LOGGED_OUT,
LOGGING_OUT,

View file

@ -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() {

View file

@ -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();
}

View file

@ -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))

View file

@ -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");
}

View file

@ -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;

View file

@ -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;
}
}

View file

@ -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() {
}
}

View file

@ -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();
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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();
}

View file

@ -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);

View file

@ -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);
}
}

View file

@ -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)) {

View file

@ -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 {

View file

@ -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);
}
}
}

View file

@ -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();
}
}

View file

@ -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);
});
}

View file

@ -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());
}

View file

@ -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();

View file

@ -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) {

View file

@ -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);

View file

@ -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");
}

View file

@ -16,6 +16,7 @@
#
org.keycloak.keys.GeneratedHmacKeyProviderFactory
org.keycloak.keys.GeneratedAesKeyProviderFactory
org.keycloak.keys.GeneratedRsaKeyProviderFactory
org.keycloak.keys.JavaKeystoreKeyProviderFactory
org.keycloak.keys.ImportedRsaKeyProviderFactory

View file

@ -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")

View file

@ -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")

View file

@ -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());

View file

@ -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");

View file

@ -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) {

View file

@ -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)

View file

@ -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]);
}
}

View file

@ -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"));
}