Compare commits

..
22 Commits
Author SHA1 Message Date
Pavel Drozd bfafe65814 KEYCLOAK-8919 - SSSDTest updated error message in assert 2018-11-29 21:31:46 +01:00
vramik 1b8dc04459 KEYCLOAK-8817 skip EntitlementAPITest.testOfflineRequestingPartyToken for auth-server-undertow 2018-11-29 13:38:26 +01:00
Pedro Igor 4355c89b9d [KEYCLOAK-7365] - No need to check roles when refreshing tokens 2018-11-29 08:51:25 -02:00
rmartinc 1b37394276 KEYCLOAK-7242: LDAPS not working with truststore SPI and connection timeout 2018-11-29 11:21:46 +01:00
Hynek Mlnarik ded82fff3d KEYCLOAK-8941 Fix order of stopping test servers 2018-11-29 11:16:34 +01:00
Sebastian Laskawiec 4fbbaf18aa KEYCLOAK-8830 Stabilize ExportImportTest 2018-11-29 10:33:00 +01:00
vramik 5ad929b9ee KEYCLOAK-8955 adapter installation fails on windows - eap7 2018-11-29 10:18:58 +01:00
Tomasz Prętki 2b9b1ba45f [KEYCLOAK-8823] - PathMatcher doesn't prefer overloaded templated resources 2018-11-28 11:39:11 -02:00
vramik 8b122de425 KEYCLOAK-8951 Update HOW-TO-RUN.md regarding fuse7 testing 2018-11-28 07:01:43 +01:00
vmuzikar 7d75377813 KEYCLOAK-8944 Fix ProfileAssume for backward adapter compat. testing 2018-11-27 13:58:41 +01:00
Stan Silvert 3c8dddf33e KEYCLOAK-8758: Implement i18n/l10n 2018-11-26 16:39:06 -05:00
Stefan Guilhen 311e848460 KEYCLOAK-8504 Ensure the authenticationFlowBindingOverrides client configuration references a valid authentication flow id when a realm is imported 2018-11-23 22:09:14 +01:00
Bruno Oliveira da Silva 607bb1b995 [KEYCLOAK-8943] Fix instructions to run Fuse 7 integration tests 2018-11-23 17:15:42 +01:00
Pedro Igor 91637120ee [KEYCLOAK-5052] - LDAP group names containing / in the name violates SIBILING_NAME constraint in db 2018-11-23 08:48:08 -02:00
Hynek Mlnarik d90a5d1367 KEYCLOAK-8594 Fix missing option to Base64 encoder 2018-11-22 21:48:00 +01:00
Hynek Mlnarik d395043fc7 KEYCLOAK-8707 Fix client template to scope migration 2018-11-22 15:07:47 +01:00
Hynek Mlnarik c9cd060417 KEYCLOAK-8824 Fix servlet filter versions 2018-11-22 14:20:46 +01:00
vramik abd6e560ac KEYCLOAK-8914 add missing dependency - keycloak-saml-servlet-filter-adapter 2018-11-22 11:29:22 +01:00
mposolda 6e93ca36af KEYCLOAK-8519 OIDCScopeTest.testClientDisplayedOnConsentScreenWithEmptyConsentText failing on Oracle 2018-11-22 09:30:01 +01:00
vramik 2d727fc54c KEYCLOAK-8909 fix KcOidcBrokerLogoutTest for product 2018-11-22 09:28:37 +01:00
mposolda 6db1f60e27 KEYCLOAK-7774 KEYCLOAK-8438 Errors when SSO authenticating to same client multiple times concurrently in more browser tabs 2018-11-21 21:51:32 +01:00
Stefan Guilhen 8af1ca8fc3 KEYCLOAK-8414 use the clientId when the ClientScopeModel is an instance of ClientModel 2018-11-20 15:08:10 +01:00
66 changed files with 2288 additions and 410 deletions

No files matched your search

+3
View File
@@ -31,6 +31,9 @@
<description/>
<properties>
<maven.compiler.target>1.7</maven.compiler.target>
<maven.compiler.source>1.7</maven.compiler.source>
<keycloak.osgi.export>
org.keycloak.adapters.servlet.*
</keycloak.osgi.export>
+3
View File
@@ -31,6 +31,9 @@
<description/>
<properties>
<maven.compiler.target>1.7</maven.compiler.target>
<maven.compiler.source>1.7</maven.compiler.source>
<keycloak.osgi.export>
org.keycloak.adapters.servlet.*
</keycloak.osgi.export>
@@ -28,6 +28,7 @@ public abstract class PathMatcher<P> {
public P matches(final String targetUri) {
int patternCount = 0;
int bracketsPatternCount = 0;
P matchingPath = null;
P matchingAnyPath = null;
P matchingAnySuffixPath = null;
@@ -50,8 +51,9 @@ public abstract class PathMatcher<P> {
if (templateUri != null) {
int length = expectedUri.split("\\/").length;
int bracketsLength = expectedUri.split("\\{").length;
if (exactMatch(expectedUri, targetUri, templateUri) && (patternCount == 0 || length > patternCount)) {
if (exactMatch(expectedUri, targetUri, templateUri) && (patternCount == 0 || length > patternCount || bracketsLength < bracketsPatternCount)) {
matchingUri = templateUri;
P resolved = resolvePathConfig(entry, targetUri);
@@ -60,6 +62,7 @@ public abstract class PathMatcher<P> {
}
patternCount = length;
bracketsPatternCount = bracketsLength;
}
}
}
@@ -1,39 +0,0 @@
/*
* 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;
}
}
@@ -42,6 +42,7 @@ import javax.naming.directory.SearchResult;
import javax.naming.ldap.Control;
import javax.naming.ldap.InitialLdapContext;
import javax.naming.ldap.LdapContext;
import javax.naming.ldap.LdapName;
import javax.naming.ldap.PagedResultsControl;
import javax.naming.ldap.PagedResultsResponseControl;
import java.io.IOException;
@@ -195,7 +196,7 @@ public class LDAPOperationManager {
int max = 5;
for (int i=0 ; i<max ; i++) {
try {
context.rename(oldDn, dn);
context.rename(new LdapName(oldDn), new LdapName(dn));
return dn;
} catch (NameAlreadyBoundException ex) {
if (!fallback) {
@@ -250,7 +251,7 @@ public class LDAPOperationManager {
return execute(new LdapOperation<List<SearchResult>>() {
@Override
public List<SearchResult> execute(LdapContext context) throws NamingException {
NamingEnumeration<SearchResult> search = context.search(baseDN, filter, cons);
NamingEnumeration<SearchResult> search = context.search(new LdapName(baseDN), filter, cons);
while (search.hasMoreElements()) {
result.add(search.nextElement());
@@ -295,7 +296,7 @@ public class LDAPOperationManager {
PagedResultsControl pagedControls = new PagedResultsControl(identityQuery.getLimit(), cookie, Control.CRITICAL);
context.setRequestControls(new Control[] { pagedControls });
NamingEnumeration<SearchResult> search = context.search(baseDN, filter, cons);
NamingEnumeration<SearchResult> search = context.search(new LdapName(baseDN), filter, cons);
while (search.hasMoreElements()) {
result.add(search.nextElement());
@@ -407,7 +408,7 @@ public class LDAPOperationManager {
@Override
public SearchResult execute(LdapContext context) throws NamingException {
NamingEnumeration<SearchResult> search = context.search(baseDN, filter, cons);
NamingEnumeration<SearchResult> search = context.search(new LdapName(baseDN), filter, cons);
try {
if (search.hasMoreElements()) {
@@ -451,7 +452,7 @@ public class LDAPOperationManager {
NamingEnumeration<Binding> enumeration = null;
try {
enumeration = context.listBindings(dn);
enumeration = context.listBindings(new LdapName(dn));
while (enumeration.hasMore()) {
Binding binding = enumeration.next();
@@ -460,7 +461,7 @@ public class LDAPOperationManager {
destroySubcontext(context, name);
}
context.unbind(dn);
context.unbind(new LdapName(dn));
} finally {
try {
enumeration.close();
@@ -550,7 +551,7 @@ public class LDAPOperationManager {
@Override
public Void execute(LdapContext context) throws NamingException {
context.modifyAttributes(dn, mods);
context.modifyAttributes(new LdapName(dn), mods);
return null;
}
@@ -595,7 +596,7 @@ public class LDAPOperationManager {
execute(new LdapOperation<Void>() {
@Override
public Void execute(LdapContext context) throws NamingException {
DirContext subcontext = context.createSubcontext(name, attributes);
DirContext subcontext = context.createSubcontext(new LdapName(name), attributes);
subcontext.close();
@@ -17,6 +17,7 @@
package org.keycloak.models.sessions.infinispan;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
@@ -44,28 +45,43 @@ public class InfinispanCodeToTokenStoreProvider implements CodeToTokenStoreProvi
this.codeCache = actionKeyCache;
}
@Override
public boolean putIfAbsent(UUID codeId) {
ActionTokenValueEntity tokenValue = new ActionTokenValueEntity(null);
int lifespanInSeconds = session.getContext().getRealm().getAccessCodeLifespan();
@Override
public void put(UUID codeId, int lifespanSeconds, Map<String, String> codeData) {
ActionTokenValueEntity tokenValue = new ActionTokenValueEntity(codeData);
try {
BasicCache<UUID, ActionTokenValueEntity> cache = codeCache.get();
ActionTokenValueEntity existing = cache.putIfAbsent(codeId, tokenValue, lifespanInSeconds, TimeUnit.SECONDS);
return existing == null;
cache.put(codeId, tokenValue, lifespanSeconds, TimeUnit.SECONDS);
} catch (HotRodClientException re) {
// No need to retry. The hotrod (remoteCache) has some retries in itself in case of some random network error happened.
// In case of lock conflict, we don't want to retry anyway as there was likely an attempt to use the code from different place.
if (logger.isDebugEnabled()) {
logger.debugf(re, "Failed when adding code %s", codeId);
}
return false;
throw re;
}
}
@Override
public Map<String, String> remove(UUID codeId) {
try {
BasicCache<UUID, ActionTokenValueEntity> cache = codeCache.get();
ActionTokenValueEntity existing = cache.remove(codeId);
return existing == null ? null : existing.getNotes();
} catch (HotRodClientException re) {
// No need to retry. The hotrod (remoteCache) has some retries in itself in case of some random network error happened.
// In case of lock conflict, we don't want to retry anyway as there was likely an attempt to remove the code from different place.
if (logger.isDebugEnabled()) {
logger.debugf(re, "Failed when removing code %s", codeId);
}
return null;
}
}
@Override
public void close() {
@@ -0,0 +1,187 @@
/*
* 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.cluster.infinispan;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import org.infinispan.Cache;
import org.jboss.logging.Logger;
import org.junit.Assert;
import org.keycloak.common.util.Time;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity;
import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
import org.keycloak.models.sessions.infinispan.initializer.DistributedCacheConcurrentWritesTest;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class ConcurrencyDistributedRemoveSessionTest {
protected static final Logger logger = Logger.getLogger(ConcurrencyJDGRemoveSessionTest.class);
private static final int ITERATIONS = 10000;
private static final AtomicInteger errorsCounter = new AtomicInteger(0);
private static final AtomicInteger successfulListenerWrites = new AtomicInteger(0);
private static final AtomicInteger successfulListenerWrites2 = new AtomicInteger(0);
private static Map<String, AtomicInteger> removalCounts = new ConcurrentHashMap<>();
private static final UUID CLIENT_1_UUID = UUID.randomUUID();
public static void main(String[] args) throws Exception {
Cache<String, SessionEntityWrapper<UserSessionEntity>> cache1 = DistributedCacheConcurrentWritesTest.createManager("node1").getCache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME);
Cache<String, SessionEntityWrapper<UserSessionEntity>> cache2 = DistributedCacheConcurrentWritesTest.createManager("node2").getCache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME);
// Create caches, listeners and finally worker threads
Thread worker1 = createWorker(cache1, 1);
Thread worker2 = createWorker(cache2, 2);
Thread worker3 = createWorker(cache1, 1);
Thread worker4 = createWorker(cache2, 2);
// Create 100 initial sessions
for (int i=0 ; i<ITERATIONS ; i++) {
String sessionId = String.valueOf(i);
SessionEntityWrapper<UserSessionEntity> wrappedSession = createSessionEntity(sessionId);
cache1.put(sessionId, wrappedSession);
removalCounts.put(sessionId, new AtomicInteger(0));
}
logger.info("SESSIONS CREATED");
// Create 100 initial sessions
for (int i=0 ; i<ITERATIONS ; i++) {
String sessionId = String.valueOf(i);
SessionEntityWrapper loadedWrapper = cache2.get(sessionId);
Assert.assertNotNull("Loaded wrapper for key " + sessionId, loadedWrapper);
}
logger.info("SESSIONS AVAILABLE ON DC2");
long start = System.currentTimeMillis();
try {
worker1.start();
worker2.start();
worker3.start();
worker4.start();
worker1.join();
worker2.join();
worker3.join();
worker4.join();
logger.info("SESSIONS REMOVED");
Map<Integer, Integer> histogram = new HashMap<>();
for (Map.Entry<String, AtomicInteger> entry : removalCounts.entrySet()) {
int count = entry.getValue().get();
int current = histogram.get(count) == null ? 0 : histogram.get(count);
current++;
histogram.put(count, current);
}
logger.infof("Histogram: %s", histogram.toString());
logger.infof("Errors: %d", errorsCounter.get());
long took = System.currentTimeMillis() - start;
logger.infof("took %d ms", took);
} finally {
Thread.sleep(2000);
// Finish JVM
cache1.getCacheManager().stop();
cache2.getCacheManager().stop();
}
}
private static SessionEntityWrapper<UserSessionEntity> createSessionEntity(String sessionId) {
// Create 100 initial sessions
UserSessionEntity session = new UserSessionEntity();
session.setId(sessionId);
session.setRealmId("foo");
session.setBrokerSessionId("!23123123");
session.setBrokerUserId(null);
session.setUser("foo");
session.setLoginUsername("foo");
session.setIpAddress("123.44.143.178");
session.setStarted(Time.currentTime());
session.setLastSessionRefresh(Time.currentTime());
AuthenticatedClientSessionEntity clientSession = new AuthenticatedClientSessionEntity(UUID.randomUUID());
clientSession.setAuthMethod("saml");
clientSession.setAction("something");
clientSession.setTimestamp(1234);
session.getAuthenticatedClientSessions().put(CLIENT_1_UUID.toString(), clientSession.getId());
SessionEntityWrapper<UserSessionEntity> wrappedSession = new SessionEntityWrapper<>(session);
return wrappedSession;
}
private static Thread createWorker(Cache<String, SessionEntityWrapper<UserSessionEntity>> cache, int threadId) {
System.out.println("Retrieved cache: " + threadId);
return new CacheWorker(cache, threadId);
}
private static class CacheWorker extends Thread {
private final Cache<String, Object> cache;
private final int myThreadId;
private CacheWorker(Cache cache, int myThreadId) {
this.cache = cache;
this.myThreadId = myThreadId;
}
@Override
public void run() {
for (int i=0 ; i<ITERATIONS ; i++) {
String sessionId = String.valueOf(i);
Object o = cache.remove(sessionId);
if (o != null) {
removalCounts.get(sessionId).incrementAndGet();
}
}
}
}
}
@@ -18,7 +18,10 @@
package org.keycloak.cluster.infinispan;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import org.infinispan.Cache;
@@ -30,6 +33,7 @@ import org.infinispan.client.hotrod.annotation.ClientListener;
import org.infinispan.client.hotrod.event.ClientCacheEntryCreatedEvent;
import org.infinispan.client.hotrod.event.ClientCacheEntryModifiedEvent;
import org.infinispan.client.hotrod.event.ClientCacheEntryRemovedEvent;
import org.infinispan.client.hotrod.exceptions.HotRodClientException;
import org.infinispan.context.Flag;
import org.infinispan.manager.EmbeddedCacheManager;
import org.infinispan.persistence.remote.configuration.RemoteStoreConfigurationBuilder;
@@ -61,13 +65,13 @@ public class ConcurrencyJDGRemoveSessionTest {
private static RemoteCache remoteCache1;
private static RemoteCache remoteCache2;
private static final AtomicInteger failedReplaceCounter = new AtomicInteger(0);
private static final AtomicInteger failedReplaceCounter2 = new AtomicInteger(0);
private static final AtomicInteger errorsCounter = new AtomicInteger(0);
private static final AtomicInteger successfulListenerWrites = new AtomicInteger(0);
private static final AtomicInteger successfulListenerWrites2 = new AtomicInteger(0);
//private static Map<String, EntryInfo> state = new HashMap<>();
private static Map<String, AtomicInteger> removalCounts = new ConcurrentHashMap<>();
private static final UUID CLIENT_1_UUID = UUID.randomUUID();
@@ -78,12 +82,16 @@ public class ConcurrencyJDGRemoveSessionTest {
// Create caches, listeners and finally worker threads
Thread worker1 = createWorker(cache1, 1);
Thread worker2 = createWorker(cache2, 2);
Thread worker3 = createWorker(cache1, 1);
Thread worker4 = createWorker(cache2, 2);
// Create 100 initial sessions
for (int i=0 ; i<ITERATIONS ; i++) {
String sessionId = String.valueOf(i);
SessionEntityWrapper<UserSessionEntity> wrappedSession = createSessionEntity(sessionId);
cache1.put(sessionId, wrappedSession);
removalCounts.put(sessionId, new AtomicInteger(0));
}
logger.info("SESSIONS CREATED");
@@ -101,25 +109,44 @@ public class ConcurrencyJDGRemoveSessionTest {
long start = System.currentTimeMillis();
try {
// Just running in current thread
worker1.run();
worker1.start();
worker2.start();
worker3.start();
worker4.start();
worker1.join();
worker2.join();
worker3.join();
worker4.join();
logger.info("SESSIONS REMOVED");
Map<Integer, Integer> histogram = new HashMap<>();
for (Map.Entry<String, AtomicInteger> entry : removalCounts.entrySet()) {
int count = entry.getValue().get();
int current = histogram.get(count) == null ? 0 : histogram.get(count);
current++;
histogram.put(count, current);
}
logger.infof("Histogram: %s", histogram.toString());
logger.infof("Errors: %d", errorsCounter.get());
//Thread.sleep(5000);
// Doing it in opposite direction to ensure that newer are checked first.
// This us currently FAILING (expected) as listeners are executed asynchronously.
for (int i=ITERATIONS-1 ; i>=0 ; i--) {
String sessionId = String.valueOf(i);
logger.infof("Before call cache2.get: %s", sessionId);
SessionEntityWrapper loadedWrapper = cache2.get(sessionId);
Assert.assertNull("Loaded wrapper not null for key " + sessionId, loadedWrapper);
}
logger.info("SESSIONS NOT AVAILABLE ON DC2");
// for (int i=ITERATIONS-1 ; i>=0 ; i--) {
// String sessionId = String.valueOf(i);
//
// logger.infof("Before call cache2.get: %s", sessionId);
//
// SessionEntityWrapper loadedWrapper = cache2.get(sessionId);
// Assert.assertNull("Loaded wrapper not null for key " + sessionId, loadedWrapper);
// }
//
// logger.info("SESSIONS NOT AVAILABLE ON DC2");
long took = System.currentTimeMillis() - start;
logger.infof("took %d ms", took);
@@ -271,19 +298,30 @@ public class ConcurrencyJDGRemoveSessionTest {
for (int i=0 ; i<ITERATIONS ; i++) {
String sessionId = String.valueOf(i);
remoteCache.remove(sessionId);
try {
Object o = remoteCache
.withFlags(org.infinispan.client.hotrod.Flag.FORCE_RETURN_VALUE)
.remove(sessionId);
logger.infof("Session %s removed on DC1", sessionId);
// Check if it's immediately seen that session is removed on 2nd DC
RemoteCache secondDCRemoteCache = myThreadId == 1 ? remoteCache2 : remoteCache1;
SessionEntityWrapper thatSession = (SessionEntityWrapper) secondDCRemoteCache.get(sessionId);
Assert.assertNull("Session with ID " + sessionId + " not removed on the other DC. ThreadID: " + myThreadId, thatSession);
// Also check that it's immediatelly removed on my DC
SessionEntityWrapper mySession = (SessionEntityWrapper) remoteCache.get(sessionId);
Assert.assertNull("Session with ID " + sessionId + " not removed on the other DC. ThreadID: " + myThreadId, mySession);
if (o != null) {
removalCounts.get(sessionId).incrementAndGet();
}
} catch (HotRodClientException hrce) {
errorsCounter.incrementAndGet();
}
//
//
// logger.infof("Session %s removed on DC1", sessionId);
//
// // Check if it's immediately seen that session is removed on 2nd DC
// RemoteCache secondDCRemoteCache = myThreadId == 1 ? remoteCache2 : remoteCache1;
// SessionEntityWrapper thatSession = (SessionEntityWrapper) secondDCRemoteCache.get(sessionId);
// Assert.assertNull("Session with ID " + sessionId + " not removed on the other DC. ThreadID: " + myThreadId, thatSession);
//
// // Also check that it's immediatelly removed on my DC
// SessionEntityWrapper mySession = (SessionEntityWrapper) remoteCache.get(sessionId);
// Assert.assertNull("Session with ID " + sessionId + " not removed on the other DC. ThreadID: " + myThreadId, mySession);
}
}
@@ -0,0 +1,68 @@
/*
* Copyright 2018 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.connections.jpa.updater.liquibase.custom;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import liquibase.exception.CustomChangeException;
import liquibase.statement.core.InsertStatement;
import liquibase.structure.core.Table;
/**
*
* @author hmlnarik
*/
public class JpaUpdate4_0_0_DefaultClientScopes extends CustomKeycloakTask {
@Override
protected void generateStatementsImpl() throws CustomChangeException {
String clientTableName = database.correctObjectName("CLIENT", Table.class);
String clientScopeClientTableName = database.correctObjectName("CLIENT_SCOPE_CLIENT", Table.class);
try (PreparedStatement statement = jdbcConnection.prepareStatement("SELECT ID, CLIENT_TEMPLATE_ID FROM " + clientTableName);
ResultSet rs = statement.executeQuery()) {
while (rs.next()) {
String clientId = rs.getString(1);
String clientTemplateId = rs.getString(2);
if (clientId == null || clientId.trim().isEmpty()) {
continue;
}
if (clientTemplateId == null || clientTemplateId.trim().isEmpty()) {
continue;
}
statements.add(
new InsertStatement(null, null, clientScopeClientTableName)
.addColumnValue("CLIENT_ID", clientId.trim())
.addColumnValue("SCOPE_ID", clientTemplateId.trim())
.addColumnValue("DEFAULT_SCOPE", Boolean.TRUE)
);
}
confirmationMessage.append("Updated " + statements.size() + " records in CLIENT_SCOPE_CLIENT table");
} catch (Exception e) {
throw new CustomChangeException(getTaskId() + ": Exception when updating data from previous version", e);
}
}
@Override
protected String getTaskId() {
return "Update 4.0.0.Final (Default client scopes)";
}
}
@@ -21,20 +21,17 @@ import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.ModelException;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleContainerModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.jpa.entities.ClientAttributeEntity;
import org.keycloak.models.jpa.entities.ClientEntity;
import org.keycloak.models.jpa.entities.ClientScopeClientMappingEntity;
import org.keycloak.models.jpa.entities.ClientScopeEntity;
import org.keycloak.models.jpa.entities.ClientScopeRoleMappingEntity;
import org.keycloak.models.jpa.entities.ProtocolMapperEntity;
import org.keycloak.models.jpa.entities.RoleEntity;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory;
import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
@@ -44,6 +41,7 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -302,25 +300,45 @@ public class ClientAdapter implements ClientModel, JpaModel<ClientEntity> {
@Override
public void setAttribute(String name, String value) {
entity.getAttributes().put(name, value);
for (ClientAttributeEntity attr : entity.getAttributes()) {
if (attr.getName().equals(name)) {
attr.setValue(value);
return;
}
}
ClientAttributeEntity attr = new ClientAttributeEntity();
attr.setName(name);
attr.setValue(value);
attr.setClient(entity);
em.persist(attr);
entity.getAttributes().add(attr);
}
@Override
public void removeAttribute(String name) {
entity.getAttributes().remove(name);
Iterator<ClientAttributeEntity> it = entity.getAttributes().iterator();
while (it.hasNext()) {
ClientAttributeEntity attr = it.next();
if (attr.getName().equals(name)) {
it.remove();
em.remove(attr);
}
}
}
@Override
public String getAttribute(String name) {
return entity.getAttributes().get(name);
return getAttributes().get(name);
}
@Override
public Map<String, String> getAttributes() {
Map<String, String> copy = new HashMap<>();
copy.putAll(entity.getAttributes());
return copy;
Map<String, String> attrs = new HashMap<>();
for (ClientAttributeEntity attr : entity.getAttributes()) {
attrs.put(attr.getName(), attr.getValue());
}
return attrs;
}
@Override
@@ -24,6 +24,7 @@ import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleContainerModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.jpa.entities.ClientScopeAttributeEntity;
import org.keycloak.models.jpa.entities.ClientScopeEntity;
import org.keycloak.models.jpa.entities.ClientScopeRoleMappingEntity;
import org.keycloak.models.jpa.entities.ProtocolMapperEntity;
@@ -34,6 +35,7 @@ import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -282,18 +284,37 @@ public class ClientScopeAdapter implements ClientScopeModel, JpaModel<ClientScop
@Override
public void setAttribute(String name, String value) {
entity.getAttributes().put(name, value);
for (ClientScopeAttributeEntity attr : entity.getAttributes()) {
if (attr.getName().equals(name)) {
attr.setValue(value);
return;
}
}
ClientScopeAttributeEntity attr = new ClientScopeAttributeEntity();
attr.setName(name);
attr.setValue(value);
attr.setClientScope(entity);
em.persist(attr);
entity.getAttributes().add(attr);
}
@Override
public void removeAttribute(String name) {
entity.getAttributes().remove(name);
Iterator<ClientScopeAttributeEntity> it = entity.getAttributes().iterator();
while (it.hasNext()) {
ClientScopeAttributeEntity attr = it.next();
if (attr.getName().equals(name)) {
it.remove();
em.remove(attr);
}
}
}
@Override
public String getAttribute(String name) {
return entity.getAttributes().get(name);
return getAttributes().get(name);
}
public static ClientScopeEntity toClientScopeEntity(ClientScopeModel model, EntityManager em) {
@@ -305,9 +326,11 @@ public class ClientScopeAdapter implements ClientScopeModel, JpaModel<ClientScop
@Override
public Map<String, String> getAttributes() {
Map<String, String> copy = new HashMap<>();
copy.putAll(entity.getAttributes());
return copy;
Map<String, String> attrs = new HashMap<>();
for (ClientScopeAttributeEntity attr : entity.getAttributes()) {
attrs.put(attr.getName(), attr.getValue());
}
return attrs;
}
@@ -0,0 +1,141 @@
/*
* 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.jpa.entities;
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.IdClass;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
@Table(name="CLIENT_ATTRIBUTES")
@Entity
@IdClass(ClientAttributeEntity.Key.class)
public class ClientAttributeEntity {
@Id
@ManyToOne(fetch= FetchType.LAZY)
@JoinColumn(name = "CLIENT_ID")
protected ClientEntity client;
@Id
@Column(name="NAME")
protected String name;
@Column(name = "VALUE", length = 4000)
protected String value;
public ClientEntity getClient() {
return client;
}
public void setClient(ClientEntity client) {
this.client = client;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public static class Key implements Serializable {
protected ClientEntity client;
protected String name;
public Key() {
}
public Key(ClientEntity client, String name) {
this.client = client;
this.name = name;
}
public ClientEntity getClient() {
return client;
}
public String getName() {
return name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ClientAttributeEntity.Key key = (ClientAttributeEntity.Key) o;
if (client != null ? !client.getId().equals(key.client != null ? key.client.getId() : null) : key.client != null) return false;
if (name != null ? !name.equals(key.name != null ? key.name : null) : key.name != null) return false;
return true;
}
@Override
public int hashCode() {
int result = client != null ? client.getId().hashCode() : 0;
result = 31 * result + (name != null ? name.hashCode() : 0);
return result;
}
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!(o instanceof ClientAttributeEntity)) return false;
ClientAttributeEntity key = (ClientAttributeEntity) o;
if (client != null ? !client.getId().equals(key.client != null ? key.client.getId() : null) : key.client != null) return false;
if (name != null ? !name.equals(key.name != null ? key.name : null) : key.name != null) return false;
return true;
}
@Override
public int hashCode() {
int result = client != null ? client.getId().hashCode() : 0;
result = 31 * result + (name != null ? name.hashCode() : 0);
return result;
}
}
@@ -104,11 +104,8 @@ public class ClientEntity {
@CollectionTable(name = "REDIRECT_URIS", joinColumns={ @JoinColumn(name="CLIENT_ID") })
protected Set<String> redirectUris = new HashSet<String>();
@ElementCollection
@MapKeyColumn(name="NAME")
@Column(name="VALUE", length = 4000)
@CollectionTable(name="CLIENT_ATTRIBUTES", joinColumns={ @JoinColumn(name="CLIENT_ID") })
protected Map<String, String> attributes = new HashMap<String, String>();
@OneToMany(cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy = "client")
protected Collection<ClientAttributeEntity> attributes = new ArrayList<>();
@ElementCollection
@MapKeyColumn(name="BINDING_NAME")
@@ -278,11 +275,11 @@ public class ClientEntity {
this.fullScopeAllowed = fullScopeAllowed;
}
public Map<String, String> getAttributes() {
public Collection<ClientAttributeEntity> getAttributes() {
return attributes;
}
public void setAttributes(Map<String, String> attributes) {
public void setAttributes(Collection<ClientAttributeEntity> attributes) {
this.attributes = attributes;
}
@@ -0,0 +1,140 @@
/*
* 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.jpa.entities;
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.IdClass;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
@Table(name="CLIENT_SCOPE_ATTRIBUTES")
@Entity
@IdClass(ClientScopeAttributeEntity.Key.class)
public class ClientScopeAttributeEntity {
@Id
@ManyToOne(fetch= FetchType.LAZY)
@JoinColumn(name = "SCOPE_ID")
protected ClientScopeEntity clientScope;
@Id
@Column(name="NAME")
protected String name;
@Column(name = "VALUE", length = 2048)
protected String value;
public ClientScopeEntity getClientScope() {
return clientScope;
}
public void setClientScope(ClientScopeEntity clientScope) {
this.clientScope = clientScope;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public static class Key implements Serializable {
protected ClientScopeEntity clientScope;
protected String name;
public Key() {
}
public Key(ClientScopeEntity clientScope, String name) {
this.clientScope = clientScope;
this.name = name;
}
public ClientScopeEntity getClientScope() {
return clientScope;
}
public String getName() {
return name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ClientScopeAttributeEntity.Key key = (ClientScopeAttributeEntity.Key) o;
if (clientScope != null ? !clientScope.getId().equals(key.clientScope != null ? key.clientScope.getId() : null) : key.clientScope != null) return false;
if (name != null ? !name.equals(key.name != null ? key.name : null) : key.name != null) return false;
return true;
}
@Override
public int hashCode() {
int result = clientScope != null ? clientScope.getId().hashCode() : 0;
result = 31 * result + (name != null ? name.hashCode() : 0);
return result;
}
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!(o instanceof ClientScopeAttributeEntity)) return false;
ClientScopeAttributeEntity key = (ClientScopeAttributeEntity) o;
if (clientScope != null ? !clientScope.getId().equals(key.clientScope != null ? key.clientScope.getId() : null) : key.clientScope != null) return false;
if (name != null ? !name.equals(key.name != null ? key.name : null) : key.name != null) return false;
return true;
}
@Override
public int hashCode() {
int result = clientScope != null ? clientScope.getId().hashCode() : 0;
result = 31 * result + (name != null ? name.hashCode() : 0);
return result;
}
}
@@ -22,22 +22,17 @@ import org.hibernate.annotations.Nationalized;
import javax.persistence.Access;
import javax.persistence.AccessType;
import javax.persistence.CascadeType;
import javax.persistence.CollectionTable;
import javax.persistence.Column;
import javax.persistence.ElementCollection;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.MapKeyColumn;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import javax.persistence.UniqueConstraint;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -66,11 +61,8 @@ public class ClientScopeEntity {
private String protocol;
@ElementCollection
@MapKeyColumn(name="NAME")
@Column(name="VALUE", length = 2048)
@CollectionTable(name="CLIENT_SCOPE_ATTRIBUTES", joinColumns={ @JoinColumn(name="SCOPE_ID") })
protected Map<String, String> attributes = new HashMap<String, String>();
@OneToMany(cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy = "clientScope")
protected Collection<ClientScopeAttributeEntity> attributes = new ArrayList<>();
public RealmEntity getRealm() {
return realm;
@@ -120,11 +112,11 @@ public class ClientScopeEntity {
this.protocol = protocol;
}
public Map<String, String> getAttributes() {
public Collection<ClientScopeAttributeEntity> getAttributes() {
return attributes;
}
public void setAttributes(Map<String, String> attributes) {
public void setAttributes(Collection<ClientScopeAttributeEntity> attributes) {
this.attributes = attributes;
}
@@ -85,7 +85,12 @@
</changeSet>
<changeSet author="mposolda@redhat.com" id="4.0.0-KEYCLOAK-5579">
<changeSet author="mposolda@redhat.com" id="4.0.0-KEYCLOAK-5579-fixed">
<preConditions onFail="MARK_RAN" onSqlOutput="TEST">
<not>
<changeSetExecuted id="4.0.0-KEYCLOAK-5579" author="mposolda@redhat.com" changeLogFile="META-INF/jpa-changelog-4.0.0.xml" />
</not>
</preConditions>
<!-- 1 - Rename clientTemplate to clientScope and drop some unused things from clientTemplate -->
<dropForeignKeyConstraint baseTableName="CLIENT_TEMPLATE_ATTRIBUTES" constraintName="FK_CL_TEMPL_ATTR_TEMPL" />
@@ -147,15 +152,6 @@
constraintName="FK_CL_SCOPE_ATTR_SCOPE" referencedTableName="CLIENT_SCOPE" referencedColumnNames="ID" />
<!-- 2 - Client binding to more clientScopes -->
<!-- Foreign key dropped above TODO:mposolda migration existing clientTemplate to default clientScope -->
<dropColumn tableName="CLIENT" columnName="CLIENT_TEMPLATE_ID" />
<dropDefaultValue tableName="CLIENT" columnName="USE_TEMPLATE_CONFIG"/>
<dropDefaultValue tableName="CLIENT" columnName="USE_TEMPLATE_SCOPE" />
<dropDefaultValue tableName="CLIENT" columnName="USE_TEMPLATE_MAPPERS" />
<dropColumn tableName="CLIENT" columnName="USE_TEMPLATE_CONFIG" />
<dropColumn tableName="CLIENT" columnName="USE_TEMPLATE_SCOPE" />
<dropColumn tableName="CLIENT" columnName="USE_TEMPLATE_MAPPERS" />
<createTable tableName="CLIENT_SCOPE_CLIENT">
<column name="CLIENT_ID" type="VARCHAR(36)">
<constraints nullable="false"/>
@@ -171,6 +167,16 @@
<addForeignKeyConstraint baseColumnNames="CLIENT_ID" baseTableName="CLIENT_SCOPE_CLIENT" constraintName="FK_C_CLI_SCOPE_CLIENT" referencedColumnNames="ID" referencedTableName="CLIENT"/>
<addForeignKeyConstraint baseColumnNames="SCOPE_ID" baseTableName="CLIENT_SCOPE_CLIENT" constraintName="FK_C_CLI_SCOPE_SCOPE" referencedColumnNames="ID" referencedTableName="CLIENT_SCOPE"/>
<customChange class="org.keycloak.connections.jpa.updater.liquibase.custom.JpaUpdate4_0_0_DefaultClientScopes" />
<dropColumn tableName="CLIENT" columnName="CLIENT_TEMPLATE_ID" />
<dropDefaultValue tableName="CLIENT" columnName="USE_TEMPLATE_CONFIG"/>
<dropDefaultValue tableName="CLIENT" columnName="USE_TEMPLATE_SCOPE" />
<dropDefaultValue tableName="CLIENT" columnName="USE_TEMPLATE_MAPPERS" />
<dropColumn tableName="CLIENT" columnName="USE_TEMPLATE_CONFIG" />
<dropColumn tableName="CLIENT" columnName="USE_TEMPLATE_SCOPE" />
<dropColumn tableName="CLIENT" columnName="USE_TEMPLATE_MAPPERS" />
<!-- Default client scopes (global scopes configured at realm level) -->
<createTable tableName="DEFAULT_CLIENT_SCOPE">
<column name="REALM_ID" type="VARCHAR(36)">
@@ -21,6 +21,7 @@
version="1.0">
<persistence-unit name="keycloak-default" transaction-type="RESOURCE_LOCAL">
<class>org.keycloak.models.jpa.entities.ClientEntity</class>
<class>org.keycloak.models.jpa.entities.ClientAttributeEntity</class>
<class>org.keycloak.models.jpa.entities.CredentialEntity</class>
<class>org.keycloak.models.jpa.entities.CredentialAttributeEntity</class>
<class>org.keycloak.models.jpa.entities.RealmEntity</class>
@@ -54,6 +55,7 @@
<class>org.keycloak.models.jpa.entities.GroupRoleMappingEntity</class>
<class>org.keycloak.models.jpa.entities.UserGroupMembershipEntity</class>
<class>org.keycloak.models.jpa.entities.ClientScopeEntity</class>
<class>org.keycloak.models.jpa.entities.ClientScopeAttributeEntity</class>
<class>org.keycloak.models.jpa.entities.ClientScopeRoleMappingEntity</class>
<class>org.keycloak.models.jpa.entities.ClientScopeClientMappingEntity</class>
<class>org.keycloak.models.jpa.entities.DefaultClientScopeRealmMappingEntity</class>
@@ -139,7 +139,7 @@ public class RedirectBindingUtil {
*/
public static String deflateBase64Encode(byte[] stringToEncode) throws IOException {
byte[] deflatedMsg = DeflateUtil.encode(stringToEncode);
return Base64.encodeBytes(deflatedMsg);
return Base64.encodeBytes(deflatedMsg, Base64.DONT_BREAK_LINES);
}
/**
@@ -17,6 +17,7 @@
package org.keycloak.models;
import java.util.Map;
import java.util.UUID;
import org.keycloak.provider.Provider;
@@ -30,5 +31,23 @@ import org.keycloak.provider.Provider;
*/
public interface CodeToTokenStoreProvider extends Provider {
boolean putIfAbsent(UUID codeId);
/**
* Stores the given data and guarantees that data should be available in the store for at least the time specified by {@param lifespanSeconds} parameter
* @param codeId
* @param lifespanSeconds
* @param codeData
* @return true if data were successfully put
*/
void put(UUID codeId, int lifespanSeconds, Map<String, String> codeData);
/**
* This method returns data just if removal was successful. Implementation should guarantee that "remove" is single-use. So if
* 2 threads (even on different cluster nodes or on different cross-dc nodes) calls "remove(123)" concurrently, then just one of them
* is allowed to succeed and return data back. It can't happen that both will succeed.
*
* @param codeId
* @return context data related to OAuth2 code. It returns null if there are not context data available.
*/
Map<String, String> remove(UUID codeId);
}
@@ -55,7 +55,6 @@ import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -432,15 +431,18 @@ public final class KeycloakModelUtils {
}
private static GroupModel findSubGroup(String[] path, int index, GroupModel parent) {
private static GroupModel findSubGroup(String[] segments, int index, GroupModel parent) {
for (GroupModel group : parent.getSubGroups()) {
if (group.getName().equals(path[index])) {
if (path.length == index + 1) {
String groupName = group.getName();
String[] pathSegments = formatPathSegments(segments, index, groupName);
if (groupName.equals(pathSegments[index])) {
if (pathSegments.length == index + 1) {
return group;
}
else {
if (index + 1 < path.length) {
GroupModel found = findSubGroup(path, index + 1, group);
if (index + 1 < pathSegments.length) {
GroupModel found = findSubGroup(pathSegments, index + 1, group);
if (found != null) return found;
} else {
return null;
@@ -452,6 +454,44 @@ public final class KeycloakModelUtils {
return null;
}
/**
* Given the {@code pathParts} of a group with the given {@code groupName}, format the {@pathParts} in order to ignore
* group names containing a {@code /} character.
*
* @param segments the path segments
* @param index the index pointing to the position to start looking for the group name
* @param groupName the groupName
* @return a new array of strings with the correct segments in case the group has a name containing slashes
*/
private static String[] formatPathSegments(String[] segments, int index, String groupName) {
String[] nameSegments = groupName.split("/");
if (nameSegments.length > 1 && segments.length >= nameSegments.length) {
for (int i = 0; i < nameSegments.length; i++) {
if (!nameSegments[i].equals(segments[index + i])) {
return segments;
}
}
int numMergedIndexes = nameSegments.length - 1;
String[] newPath = new String[segments.length - numMergedIndexes];
for (int i = 0; i < newPath.length; i++) {
if (i == index) {
newPath[i] = groupName;
} else if (i > index) {
newPath[i] = segments[i + numMergedIndexes];
} else {
newPath[i] = segments[i];
}
}
return newPath;
}
return segments;
}
public static GroupModel findGroupByPath(RealmModel realm, String path) {
if (path == null) {
return null;
@@ -466,14 +506,17 @@ public final class KeycloakModelUtils {
if (split.length == 0) return null;
GroupModel found = null;
for (GroupModel group : realm.getTopLevelGroups()) {
if (group.getName().equals(split[0])) {
if (split.length == 1) {
String groupName = group.getName();
String[] pathSegments = formatPathSegments(split, 0, groupName);
if (groupName.equals(pathSegments[0])) {
if (pathSegments.length == 1) {
found = group;
break;
}
else {
if (split.length > 1) {
found = findSubGroup(split, 1, group);
if (pathSegments.length > 1) {
found = findSubGroup(pathSegments, 1, group);
if (found != null) break;
}
}
@@ -570,7 +613,14 @@ public final class KeycloakModelUtils {
return clientScope;
}
}
// check if we are referencing a client instead of a scope
if (realm.getClients() != null) {
for (ClientModel client : realm.getClients()) {
if (clientScopeName.equals(client.getClientId())) {
return client;
}
}
}
return null;
}
@@ -660,7 +660,11 @@ public class ModelToRepresentation {
List<String> grantedClientScopes = new LinkedList<>();
for (ClientScopeModel clientScope : model.getGrantedClientScopes()) {
grantedClientScopes.add(clientScope.getName());
if (clientScope instanceof ClientModel) {
grantedClientScopes.add(((ClientModel) clientScope).getClientId());
} else {
grantedClientScopes.add(clientScope.getName());
}
}
UserConsentRepresentation consentRep = new UserConsentRepresentation();
@@ -258,7 +258,7 @@ public class RepresentationToModel {
if (rep.getOtpPolicyType() != null) newRealm.setOTPPolicy(toPolicy(rep));
else newRealm.setOTPPolicy(OTPPolicy.DEFAULT_POLICY);
importAuthenticationFlows(newRealm, rep);
Map<String, String> mappedFlows = importAuthenticationFlows(newRealm, rep);
if (rep.getRequiredActions() != null) {
for (RequiredActionProviderRepresentation action : rep.getRequiredActions()) {
RequiredActionProviderModel model = toModel(action);
@@ -300,7 +300,7 @@ public class RepresentationToModel {
}
if (rep.getClients() != null) {
createClients(session, rep, newRealm);
createClients(session, rep, newRealm, mappedFlows);
}
importRoles(rep.getRoles(), newRealm);
@@ -584,7 +584,8 @@ public class RepresentationToModel {
}
}
public static void importAuthenticationFlows(RealmModel newRealm, RealmRepresentation rep) {
public static Map<String, String> importAuthenticationFlows(RealmModel newRealm, RealmRepresentation rep) {
Map<String, String> mappedFlows = new HashMap<>();
if (rep.getAuthenticationFlows() == null) {
// assume this is an old version being imported
DefaultAuthenticationFlows.migrateFlows(newRealm);
@@ -596,8 +597,11 @@ public class RepresentationToModel {
for (AuthenticationFlowRepresentation flowRep : rep.getAuthenticationFlows()) {
AuthenticationFlowModel model = toModel(flowRep);
// make sure new id is generated for new AuthenticationFlowModel instance
String previousId = model.getId();
model.setId(null);
model = newRealm.addAuthenticationFlow(model);
// store the mapped ids so that clients can reference the correct flow when importing the authenticationFlowBindingOverrides
mappedFlows.put(previousId, model.getId());
}
for (AuthenticationFlowRepresentation flowRep : rep.getAuthenticationFlows()) {
AuthenticationFlowModel model = newRealm.getFlowByAlias(flowRep.getAlias());
@@ -675,6 +679,8 @@ public class RepresentationToModel {
}
DefaultAuthenticationFlows.addIdentityProviderAuthenticator(newRealm, defaultProvider);
return mappedFlows;
}
private static void convertDeprecatedSocialProviders(RealmRepresentation rep) {
@@ -1073,10 +1079,10 @@ public class RepresentationToModel {
// CLIENTS
private static Map<String, ClientModel> createClients(KeycloakSession session, RealmRepresentation rep, RealmModel realm) {
private static Map<String, ClientModel> createClients(KeycloakSession session, RealmRepresentation rep, RealmModel realm, Map<String, String> mappedFlows) {
Map<String, ClientModel> appMap = new HashMap<String, ClientModel>();
for (ClientRepresentation resourceRep : rep.getClients()) {
ClientModel app = createClient(session, realm, resourceRep, false);
ClientModel app = createClient(session, realm, resourceRep, false, mappedFlows);
appMap.put(app.getClientId(), app);
}
return appMap;
@@ -1090,6 +1096,10 @@ public class RepresentationToModel {
* @return
*/
public static ClientModel createClient(KeycloakSession session, RealmModel realm, ClientRepresentation resourceRep, boolean addDefaultRoles) {
return createClient(session, realm, resourceRep, addDefaultRoles, null);
}
private static ClientModel createClient(KeycloakSession session, RealmModel realm, ClientRepresentation resourceRep, boolean addDefaultRoles, Map<String, String> mappedFlows) {
logger.debugv("Create client: {0}", resourceRep.getClientId());
ClientModel client = resourceRep.getId() != null ? realm.addClient(resourceRep.getId(), resourceRep.getClientId()) : realm.addClient(resourceRep.getClientId());
@@ -1164,10 +1174,14 @@ public class RepresentationToModel {
continue;
} else {
String flowId = entry.getValue();
// check if flow id was mapped when the flows were imported
if (mappedFlows != null && mappedFlows.containsKey(flowId)) {
flowId = mappedFlows.get(flowId);
}
if (client.getRealm().getAuthenticationFlowById(flowId) == null) {
throw new RuntimeException("Unable to resolve auth flow binding override for: " + entry.getKey());
}
client.setAuthenticationFlowBindingOverride(entry.getKey(), entry.getValue());
client.setAuthenticationFlowBindingOverride(entry.getKey(), flowId);
}
}
}
@@ -40,4 +40,11 @@ public interface ClientSessionContext {
Set<ProtocolMapperModel> getProtocolMappers();
String getScopeString();
void setAttribute(String name, Object value);
<T> T getAttribute(String attribute, Class<T> clazz);
String AUTHENTICATION_SESSION_ATTR = "AUTH_SESSION_ATTR";
}
@@ -126,8 +126,6 @@ public abstract class AuthorizationEndpointBase {
return response;
}
// Attach session once no requiredActions or other things are required
processor.attachSession();
} catch (Exception e) {
return processor.handleBrowserException(e);
}
@@ -29,7 +29,6 @@ import org.keycloak.events.EventType;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionContext;
import org.keycloak.models.TokenManager;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
@@ -39,16 +38,21 @@ import org.keycloak.protocol.oidc.utils.OIDCResponseMode;
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.adapters.action.PushNotBeforeAction;
import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.ServicesLogger;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.AuthenticationSessionManager;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.protocol.oidc.utils.OAuth2Code;
import org.keycloak.protocol.oidc.utils.OAuth2CodeParser;
import org.keycloak.services.managers.ResourceAdminManager;
import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.sessions.CommonClientSessionModel;
import org.keycloak.util.TokenUtil;
import java.io.IOException;
import java.net.URI;
import java.util.UUID;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
@@ -179,7 +183,6 @@ public class OIDCLoginProtocol implements LoginProtocol {
@Override
public Response authenticated(UserSessionModel userSession, ClientSessionContext clientSessionCtx) {
AuthenticatedClientSessionModel clientSession= clientSessionCtx.getClientSession();
ClientSessionCode<AuthenticatedClientSessionModel> accessCode = new ClientSessionCode<>(session, realm, clientSession);
String responseTypeParam = clientSession.getNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM);
String responseModeParam = clientSession.getNote(OIDCLoginProtocol.RESPONSE_MODE_PARAM);
@@ -197,10 +200,27 @@ public class OIDCLoginProtocol implements LoginProtocol {
redirectUri.addParam(OAuth2Constants.SESSION_STATE, userSession.getId());
}
AuthenticationSessionModel authSession = clientSessionCtx.getAttribute(ClientSessionContext.AUTHENTICATION_SESSION_ATTR, AuthenticationSessionModel.class);
if (authSession == null) {
// Shouldn't happen if correctly used
throw new IllegalStateException("AuthenticationSession attachement not set in the ClientSessionContext");
}
String nonce = authSession.getClientNote(OIDCLoginProtocol.NONCE_PARAM);
clientSessionCtx.setAttribute(OIDCLoginProtocol.NONCE_PARAM, nonce);
// Standard or hybrid flow
String code = null;
if (responseType.hasResponseType(OIDCResponseType.CODE)) {
code = accessCode.getOrGenerateCode();
OAuth2Code codeData = new OAuth2Code(UUID.randomUUID(),
Time.currentTime() + userSession.getRealm().getAccessCodeLifespan(),
nonce,
authSession.getClientNote(OAuth2Constants.SCOPE),
authSession.getClientNote(OIDCLoginProtocol.REDIRECT_URI_PARAM),
authSession.getClientNote(OIDCLoginProtocol.CODE_CHALLENGE_PARAM),
authSession.getClientNote(OIDCLoginProtocol.CODE_CHALLENGE_METHOD_PARAM));
code = OAuth2CodeParser.persistCode(session, clientSession, codeData);
redirectUri.addParam(OAuth2Constants.CODE, code);
}
@@ -180,9 +180,10 @@ public class TokenManager {
throw new OAuthErrorException(OAuthErrorException.INVALID_SCOPE, "Client no longer has requested consent from user");
}
clientSessionCtx.setAttribute(OIDCLoginProtocol.NONCE_PARAM, oldToken.getNonce());
// recreate token.
AccessToken newToken = createClientAccessToken(session, realm, client, user, userSession, clientSessionCtx);
verifyAccess(oldToken, newToken);
return new TokenValidation(user, userSession, clientSessionCtx, newToken);
}
@@ -433,7 +434,10 @@ public class TokenManager {
// Remove authentication session now
new AuthenticationSessionManager(session).removeAuthenticationSession(userSession.getRealm(), authSession, true);
return DefaultClientSessionContext.fromClientSessionAndClientScopeIds(clientSession, clientScopeIds);
ClientSessionContext clientSessionCtx = DefaultClientSessionContext.fromClientSessionAndClientScopeIds(clientSession, clientScopeIds);
clientSessionCtx.setAttribute(ClientSessionContext.AUTHENTICATION_SESSION_ATTR, authSession);
return clientSessionCtx;
}
@@ -534,33 +538,6 @@ public class TokenManager {
return true;
}
// TODO: Remove this check entirely? It should be sufficient to check granted consents (client scopes) during refresh token
private void verifyAccess(AccessToken token, AccessToken newToken) throws OAuthErrorException {
if (token.getRealmAccess() != null) {
if (newToken.getRealmAccess() == null) throw new OAuthErrorException(OAuthErrorException.INVALID_SCOPE, "User no long has permission for realm roles");
for (String roleName : token.getRealmAccess().getRoles()) {
if (!newToken.getRealmAccess().getRoles().contains(roleName)) {
throw new OAuthErrorException(OAuthErrorException.INVALID_SCOPE, "User no long has permission for realm role: " + roleName);
}
}
}
if (token.getResourceAccess() != null) {
for (Map.Entry<String, AccessToken.Access> entry : token.getResourceAccess().entrySet()) {
AccessToken.Access appAccess = newToken.getResourceAccess(entry.getKey());
if (appAccess == null && !entry.getValue().getRoles().isEmpty()) {
throw new OAuthErrorException(OAuthErrorException.INVALID_SCOPE, "User or client no longer has role permissions for client key: " + entry.getKey());
}
for (String roleName : entry.getValue().getRoles()) {
if (!appAccess.getRoles().contains(roleName)) {
throw new OAuthErrorException(OAuthErrorException.INVALID_SCOPE, "User no long has permission for client role " + roleName);
}
}
}
}
}
public AccessToken transformAccessToken(KeycloakSession session, AccessToken token,
UserSessionModel userSession, ClientSessionContext clientSessionCtx) {
@@ -614,7 +591,7 @@ public class TokenManager {
AuthenticatedClientSessionModel clientSession = clientSessionCtx.getClientSession();
token.issuer(clientSession.getNote(OIDCLoginProtocol.ISSUER));
token.setNonce(clientSession.getNote(OIDCLoginProtocol.NONCE_PARAM));
token.setNonce(clientSessionCtx.getAttribute(OIDCLoginProtocol.NONCE_PARAM, String.class));
token.setScope(clientSessionCtx.getScopeString());
// Best effort for "acr" value. Use 0 if clientSession was authenticated through cookie ( SSO )
@@ -75,7 +75,8 @@ import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.AuthenticationSessionManager;
import org.keycloak.services.managers.BruteForceProtector;
import org.keycloak.services.managers.ClientManager;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.protocol.oidc.utils.OAuth2Code;
import org.keycloak.protocol.oidc.utils.OAuth2CodeParser;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.resources.Cors;
import org.keycloak.services.resources.IdentityBrokerService;
@@ -275,8 +276,8 @@ public class TokenEndpoint {
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, "Missing parameter: " + OAuth2Constants.CODE, Response.Status.BAD_REQUEST);
}
ClientSessionCode.ParseResult<AuthenticatedClientSessionModel> parseResult = ClientSessionCode.parseResult(code, null, session, realm, client, event, AuthenticatedClientSessionModel.class);
if (parseResult.isAuthSessionNotFound() || parseResult.isIllegalHash()) {
OAuth2CodeParser.ParseResult parseResult = OAuth2CodeParser.parseCode(session, code, realm, event);
if (parseResult.isIllegalCode()) {
AuthenticatedClientSessionModel clientSession = parseResult.getClientSession();
// Attempt to use same code twice should invalidate existing clientSession
@@ -291,7 +292,7 @@ public class TokenEndpoint {
AuthenticatedClientSessionModel clientSession = parseResult.getClientSession();
if (parseResult.isExpiredToken()) {
if (parseResult.isExpiredCode()) {
event.error(Errors.EXPIRED_CODE);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, "Code is expired", Response.Status.BAD_REQUEST);
}
@@ -317,7 +318,8 @@ public class TokenEndpoint {
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, "User disabled", Response.Status.BAD_REQUEST);
}
String redirectUri = clientSession.getNote(OIDCLoginProtocol.REDIRECT_URI_PARAM);
OAuth2Code codeData = parseResult.getCodeData();
String redirectUri = codeData.getRedirectUriParam();
String redirectUriParam = formParams.getFirst(OAuth2Constants.REDIRECT_URI);
// KEYCLOAK-4478 Backwards compatibility with the adapters earlier than KC 3.4.2
@@ -349,8 +351,8 @@ public class TokenEndpoint {
// https://tools.ietf.org/html/rfc7636#section-4.6
String codeVerifier = formParams.getFirst(OAuth2Constants.CODE_VERIFIER);
String codeChallenge = clientSession.getNote(OIDCLoginProtocol.CODE_CHALLENGE_PARAM);
String codeChallengeMethod = clientSession.getNote(OIDCLoginProtocol.CODE_CHALLENGE_METHOD_PARAM);
String codeChallenge = codeData.getCodeChallenge();
String codeChallengeMethod = codeData.getCodeChallengeMethod();
String authUserId = user.getId();
String authUsername = user.getUsername();
if (authUserId == null) {
@@ -406,7 +408,7 @@ public class TokenEndpoint {
// Compute client scopes again from scope parameter. Check if user still has them granted
// (but in code-to-token request, it could just theoretically happen that they are not available)
String scopeParam = clientSession.getNote(OAuth2Constants.SCOPE);
String scopeParam = codeData.getScope();
Set<ClientScopeModel> clientScopes = TokenManager.getRequestedClientScopes(scopeParam, client);
if (!TokenManager.verifyConsentStillAvailable(session, user, client, clientScopes)) {
event.error(Errors.NOT_ALLOWED);
@@ -415,6 +417,9 @@ public class TokenEndpoint {
ClientSessionContext clientSessionCtx = DefaultClientSessionContext.fromClientSessionAndClientScopes(clientSession, clientScopes);
// Set nonce as an attribute in the ClientSessionContext. Will be used for the token generation
clientSessionCtx.setAttribute(OIDCLoginProtocol.NONCE_PARAM, codeData.getNonce());
AccessToken token = tokenManager.createClientAccessToken(session, realm, client, user, userSession, clientSessionCtx);
TokenManager.AccessTokenResponseBuilder responseBuilder = tokenManager.responseBuilder(realm, client, event, session, userSession, clientSessionCtx)
@@ -0,0 +1,128 @@
/*
* 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.protocol.oidc.utils;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* Data associated with the oauth2 code.
*
* Those data are typically valid just for the very short time - they're created at the point before we redirect to the application
* after successful and they're removed when application sends requests to the token endpoint (code-to-token endpoint) to exchange the
* single-use OAuth2 code parameter for those data.
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class OAuth2Code {
private static final String ID_NOTE = "id";
private static final String EXPIRATION_NOTE = "exp";
private static final String NONCE_NOTE = "nonce";
private static final String SCOPE_NOTE = "scope";
private static final String REDIRECT_URI_PARAM_NOTE = "redirectUri";
private static final String CODE_CHALLENGE_NOTE = "code_challenge";
private static final String CODE_CHALLENGE_METHOD_NOTE = "code_challenge_method";
private final UUID id;
private final int expiration;
private final String nonce;
private final String scope;
private final String redirectUriParam;
private final String codeChallenge;
private final String codeChallengeMethod;
public OAuth2Code(UUID id, int expiration, String nonce, String scope, String redirectUriParam,
String codeChallenge, String codeChallengeMethod) {
this.id = id;
this.expiration = expiration;
this.nonce = nonce;
this.scope = scope;
this.redirectUriParam = redirectUriParam;
this.codeChallenge = codeChallenge;
this.codeChallengeMethod = codeChallengeMethod;
}
private OAuth2Code(Map<String, String> data) {
id = UUID.fromString(data.get(ID_NOTE));
expiration = Integer.parseInt(data.get(EXPIRATION_NOTE));
nonce = data.get(NONCE_NOTE);
scope = data.get(SCOPE_NOTE);
redirectUriParam = data.get(REDIRECT_URI_PARAM_NOTE);
codeChallenge = data.get(CODE_CHALLENGE_NOTE);
codeChallengeMethod = data.get(CODE_CHALLENGE_METHOD_NOTE);
}
public static final OAuth2Code deserializeCode(Map<String, String> data) {
return new OAuth2Code(data);
}
public Map<String, String> serializeCode() {
Map<String, String> result = new HashMap<>();
result.put(ID_NOTE, id.toString());
result.put(EXPIRATION_NOTE, String.valueOf(expiration));
result.put(NONCE_NOTE, nonce);
result.put(SCOPE_NOTE, scope);
result.put(REDIRECT_URI_PARAM_NOTE, redirectUriParam);
result.put(CODE_CHALLENGE_NOTE, codeChallenge);
result.put(CODE_CHALLENGE_METHOD_NOTE, codeChallengeMethod);
return result;
}
public UUID getId() {
return id;
}
public int getExpiration() {
return expiration;
}
public String getNonce() {
return nonce;
}
public String getScope() {
return scope;
}
public String getRedirectUriParam() {
return redirectUriParam;
}
public String getCodeChallenge() {
return codeChallenge;
}
public String getCodeChallengeMethod() {
return codeChallengeMethod;
}
}
@@ -0,0 +1,195 @@
/*
* 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.protocol.oidc.utils;
import java.util.Map;
import java.util.UUID;
import java.util.regex.Pattern;
import org.jboss.logging.Logger;
import org.keycloak.common.util.Time;
import org.keycloak.events.Details;
import org.keycloak.events.EventBuilder;
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.services.managers.UserSessionCrossDCManager;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class OAuth2CodeParser {
private static final Logger logger = Logger.getLogger(OAuth2CodeParser.class);
private static final Pattern DOT = Pattern.compile("\\.");
/**
* Will persist the code to the cache and return the object with the codeData and code correctly set
*
* @param session
* @param clientSession
* @param codeData
* @return code parameter to be used in OAuth2 handshake
*/
public static String persistCode(KeycloakSession session, AuthenticatedClientSessionModel clientSession, OAuth2Code codeData) {
CodeToTokenStoreProvider codeStore = session.getProvider(CodeToTokenStoreProvider.class);
UUID key = codeData.getId();
if (key == null) {
throw new IllegalStateException("ID not present in the data");
}
Map<String, String> serialized = codeData.serializeCode();
codeStore.put(key, clientSession.getUserSession().getRealm().getAccessCodeLifespan(), serialized);
return key.toString() + "." + clientSession.getUserSession().getId() + "." + clientSession.getClient().getId();
}
/**
* Will parse the code and retrieve the corresponding OAuth2Code and AuthenticatedClientSessionModel. Will also check if code wasn't already
* used and if it wasn't expired. If it was already used (or other error happened during parsing), then returned parser will have "isIllegalHash"
* set to true. If it was expired, the parser will have "isExpired" set to true
*
* @param session
* @param code
* @param realm
* @param event
* @return
*/
public static ParseResult parseCode(KeycloakSession session, String code, RealmModel realm, EventBuilder event) {
ParseResult result = new ParseResult(code);
String[] parsed = DOT.split(code, 3);
if (parsed.length < 3) {
logger.warn("Invalid format of the code");
return result.illegalCode();
}
String userSessionId = parsed[1];
String clientUUID = parsed[2];
event.detail(Details.CODE_ID, userSessionId);
event.session(userSessionId);
// Parse UUID
UUID codeUUID;
try {
codeUUID = UUID.fromString(parsed[0]);
} catch (IllegalArgumentException re) {
logger.warn("Invalid format of the UUID in the code");
return result.illegalCode();
}
// Retrieve UserSession
UserSessionModel userSession = new UserSessionCrossDCManager(session).getUserSessionWithClient(realm, userSessionId, clientUUID);
if (userSession == null) {
// Needed to track if code is invalid or was already used.
userSession = session.sessions().getUserSession(realm, userSessionId);
if (userSession == null) {
return result.illegalCode();
}
}
result.clientSession = userSession.getAuthenticatedClientSessionByClient(clientUUID);
CodeToTokenStoreProvider codeStore = session.getProvider(CodeToTokenStoreProvider.class);
Map<String, String> codeData = codeStore.remove(codeUUID);
// Either code not available or was already used
if (codeData == null) {
logger.warnf("Code '%s' already used for userSession '%s' and client '%s'.", codeUUID, userSessionId, clientUUID);
return result.illegalCode();
}
logger.tracef("Successfully verified code '%s'. User session: '%s', client: '%s'", codeUUID, userSessionId, clientUUID);
result.codeData = OAuth2Code.deserializeCode(codeData);
// Finally doublecheck if code is not expired
int currentTime = Time.currentTime();
if (currentTime > result.codeData.getExpiration()) {
return result.expiredCode();
}
return result;
}
public static class ParseResult {
private final String code;
private OAuth2Code codeData;
private AuthenticatedClientSessionModel clientSession;
private boolean isIllegalCode = false;
private boolean isExpiredCode = false;
private ParseResult(String code, OAuth2Code codeData, AuthenticatedClientSessionModel clientSession) {
this.code = code;
this.codeData = codeData;
this.clientSession = clientSession;
this.isIllegalCode = false;
this.isExpiredCode = false;
}
private ParseResult(String code) {
this.code = code;
}
public String getCode() {
return code;
}
public OAuth2Code getCodeData() {
return codeData;
}
public AuthenticatedClientSessionModel getClientSession() {
return clientSession;
}
public boolean isIllegalCode() {
return isIllegalCode;
}
public boolean isExpiredCode() {
return isExpiredCode;
}
private ParseResult illegalCode() {
this.isIllegalCode = true;
return this;
}
private ParseResult expiredCode() {
this.isExpiredCode = true;
return this;
}
}
}
@@ -38,7 +38,6 @@ 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;
@@ -60,10 +59,6 @@ class CodeGenerateUtil {
PARSERS.put(AuthenticationSessionModel.class, () -> {
return new AuthenticationSessionModelParser();
});
PARSERS.put(AuthenticatedClientSessionModel.class, () -> {
return new AuthenticatedClientSessionModelParser();
});
}
@@ -166,119 +161,4 @@ class CodeGenerateUtil {
}
private static class AuthenticatedClientSessionModelParser implements ClientSessionParser<AuthenticatedClientSessionModel> {
private CodeJWT codeJWT;
@Override
public AuthenticatedClientSessionModel parseSession(String code, String tabId, KeycloakSession session, RealmModel realm, ClientModel client, EventBuilder event) {
SecretKey aesKey = session.keys().getActiveAesKey(realm).getSecretKey();
SecretKey hmacKey = session.keys().getActiveHmacKey(realm).getSecretKey();
try {
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.getAuthenticatedClientSessionByClient(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 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 boolean isExpired(KeycloakSession session, String code, AuthenticatedClientSessionModel clientSession) {
return !codeJWT.isActive();
}
@Override
public int getTimestamp(AuthenticatedClientSessionModel clientSession) {
return clientSession.getTimestamp();
}
@Override
public void setTimestamp(AuthenticatedClientSessionModel clientSession, int timestamp) {
clientSession.setTimestamp(timestamp);
}
@Override
public String getClientNote(AuthenticatedClientSessionModel clientSession, String noteKey) {
return clientSession.getNote(noteKey);
}
}
}
@@ -17,7 +17,9 @@
package org.keycloak.services.util;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.jboss.logging.Logger;
@@ -56,6 +58,8 @@ public class DefaultClientSessionContext implements ClientSessionContext {
// All roles of user expanded. It doesn't yet take into account permitted clientScopes
private Set<RoleModel> userRoles;
private Map<String, Object> attributes = new HashMap<>();
private DefaultClientSessionContext(AuthenticatedClientSessionModel clientSession, Set<String> clientScopeIds) {
this.clientSession = clientSession;
this.clientScopeIds = clientScopeIds;
@@ -177,6 +181,19 @@ public class DefaultClientSessionContext implements ClientSessionContext {
}
@Override
public void setAttribute(String name, Object value) {
attributes.put(name, value);
}
@Override
public <T> T getAttribute(String name, Class<T> clazz) {
Object value = attributes.get(name);
return clazz.cast(value);
}
// Loading data
private Set<ClientScopeModel> loadClientScopes() {
@@ -101,4 +101,10 @@ public class SSLSocketFactory extends javax.net.ssl.SSLSocketFactory {
public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
return sslsf.createSocket(address, port, localAddress, localPort);
}
@Override
public Socket createSocket() throws IOException {
return sslsf.createSocket();
}
}
@@ -148,9 +148,9 @@ Assumed you downloaded `jboss-fuse-karaf-6.3.0.redhat-229.zip`
-Dtest=Fuse*AdapterTest
### JBoss Fuse 7.0
### JBoss Fuse 7.X
1) Download JBoss Fuse 7.0 to your filesystem. It can be downloaded from http://origin-repository.jboss.org/nexus/content/groups/m2-proxy/org/jboss/fuse/fuse-karaf
1) Download JBoss Fuse 7 to your filesystem. It can be downloaded from http://origin-repository.jboss.org/nexus/content/groups/m2-proxy/org/jboss/fuse/fuse-karaf
Assumed you downloaded `fuse-karaf-7.0.0.fuse-000202.zip`
2) Install to your local maven repository and change the properties according to your env (This step can be likely avoided if you somehow configure your local maven settings to point directly to Fuse repo):
@@ -170,8 +170,8 @@ Assumed you downloaded `fuse-karaf-7.0.0.fuse-000202.zip`
# Prepare Fuse server
mvn -f testsuite/integration-arquillian/servers/pom.xml \
clean install \
-Papp-server-fuse70 \
-Dfuse70.version=7.0.0.fuse-000202 \
-Papp-server-fuse7x \
-Dfuse7x.version=7.0.0.fuse-000202 \
-Dapp.server.karaf.update.config=true \
-Dmaven.local.settings=$HOME/.m2/settings.xml \
-Drepositories=,http://REPO-SERVER/brewroot/repos/sso-7.1-build/latest/maven/ \
@@ -180,7 +180,7 @@ Assumed you downloaded `fuse-karaf-7.0.0.fuse-000202.zip`
# Run the Fuse adapter tests
mvn -f testsuite/integration-arquillian/tests/base/pom.xml \
clean test \
-Papp-server-fuse70 \
-Papp-server-fuse7x \
-Dtest=Fuse*AdapterTest
@@ -0,0 +1,17 @@
set "NOPAUSE=true"
set cli_file=%2
set working_dir=%3
cd %working_dir%
if "%4"=="-Dserver.config" (
set server_config=%5
) else (
set server_config=standalone.xml
)
if "%4"=="-Djboss.server.config.dir" (
jboss-cli.bat --file=%cli_file% -Dserver.config=%server_config% -Djboss.server.config.dir=%5
) else (
jboss-cli.bat --file=%cli_file% -Dserver.config=%server_config%
)
@@ -39,10 +39,27 @@
<saml-adapter.version>${project.version}</saml-adapter.version>
<skip.elytron.adapter.installation>true</skip.elytron.adapter.installation>
<skip.adapter.offline.installation>true</skip.adapter.offline.installation>
<cli.executable>./jboss-cli.${script.suffix}</cli.executable>
<cli.working.dir>${app.server.jboss.home}/bin</cli.working.dir>
<cli.win.working.dir/>
</properties>
<profiles>
<profile>
<id>windows-properties-cli</id>
<activation>
<os>
<family>Windows</family>
</os>
</activation>
<properties>
<cli.executable>run-jboss-cli.bat</cli.executable>
<cli.working.dir>${common.resources}/win</cli.working.dir>
<cli.win.working.dir>${app.server.jboss.home}/bin</cli.win.working.dir>
</properties>
</profile>
<profile>
<id>app-server-jboss-submodules</id>
<activation>
@@ -249,10 +266,16 @@
</goals>
<configuration>
<skip>${skip.apply.offline.cli}</skip><!--eap6-->
<executable>./jboss-cli.${script.suffix}</executable>
<workingDirectory>${app.server.jboss.home}/bin</workingDirectory>
<executable>${cli.executable}</executable>
<workingDirectory>${cli.working.dir}</workingDirectory>
<arguments>
<argument>--file=${common.resources}/cli/add-adapter-log-level.cli</argument>
<!--
following attribute is required just for windows
see ${common.resources}/win/run-jboss-cli.bat
!! the ordering is important, if you need to change it edit run-jboss-cli.bat !!
-->
<argument>${cli.win.working.dir}</argument>
</arguments>
</configuration>
</execution>
@@ -264,10 +287,16 @@
</goals>
<configuration>
<skip>${skip.apply.offline.cli}</skip><!--eap6-->
<executable>./jboss-cli.${script.suffix}</executable>
<workingDirectory>${app.server.jboss.home}/bin</workingDirectory>
<executable>${cli.executable}</executable>
<workingDirectory>${cli.working.dir}</workingDirectory>
<arguments>
<argument>--file=${common.resources}/cli/add-adapter-log-level.cli</argument>
<!--
following attribute is required just for windows
see ${common.resources}/win/run-jboss-cli.bat
!! the ordering is important, if you need to change it edit run-jboss-cli.bat !!
-->
<argument>${cli.win.working.dir}</argument>
<argument>-Dserver.config=standalone-ha.xml</argument>
</arguments>
</configuration>
@@ -282,10 +311,16 @@
</goals>
<configuration>
<skip>${skip.elytron.adapter.installation}</skip>
<executable>./jboss-cli.${script.suffix}</executable>
<workingDirectory>${app.server.jboss.home}/bin</workingDirectory>
<executable>${cli.executable}</executable>
<workingDirectory>${cli.working.dir}</workingDirectory>
<arguments>
<argument>--file=${app.server.jboss.home}/bin/adapter-elytron-install-offline.cli</argument>
<!--
following attribute is required just for windows
see ${common.resources}/win/run-jboss-cli.bat
!! the ordering is important, if you need to change it edit run-jboss-cli.bat !!
-->
<argument>${cli.win.working.dir}</argument>
</arguments>
</configuration>
</execution>
@@ -298,10 +333,16 @@
</goals>
<configuration>
<skip>${skip.elytron.adapter.installation}</skip>
<executable>./jboss-cli.${script.suffix}</executable>
<workingDirectory>${app.server.jboss.home}/bin</workingDirectory>
<executable>${cli.executable}</executable>
<workingDirectory>${cli.working.dir}</workingDirectory>
<arguments>
<argument>--file=${app.server.jboss.home}/bin/adapter-elytron-install-offline.cli</argument>
<!--
following attribute is required just for windows
see ${common.resources}/win/run-jboss-cli.bat
!! the ordering is important, if you need to change it edit run-jboss-cli.bat !!
-->
<argument>${cli.win.working.dir}</argument>
<argument>-Dserver.config=standalone-ha.xml</argument>
</arguments>
</configuration>
@@ -315,10 +356,16 @@
</goals>
<configuration>
<skip>${skip.elytron.adapter.installation}</skip>
<executable>./jboss-cli.${script.suffix}</executable>
<workingDirectory>${app.server.jboss.home}/bin</workingDirectory>
<executable>${cli.executable}</executable>
<workingDirectory>${cli.working.dir}</workingDirectory>
<arguments>
<argument>--file=${app.server.jboss.home}/bin/adapter-elytron-install-saml-offline.cli</argument>
<!--
following attribute is required just for windows
see ${common.resources}/win/run-jboss-cli.bat
!! the ordering is important, if you need to change it edit run-jboss-cli.bat !!
-->
<argument>${cli.win.working.dir}</argument>
</arguments>
</configuration>
</execution>
@@ -331,10 +378,16 @@
</goals>
<configuration>
<skip>${skip.elytron.adapter.installation}</skip>
<executable>./jboss-cli.${script.suffix}</executable>
<workingDirectory>${app.server.jboss.home}/bin</workingDirectory>
<executable>${cli.executable}</executable>
<workingDirectory>${cli.working.dir}</workingDirectory>
<arguments>
<argument>--file=${app.server.jboss.home}/bin/adapter-elytron-install-saml-offline.cli</argument>
<!--
following attribute is required just for windows
see ${common.resources}/win/run-jboss-cli.bat
!! the ordering is important, if you need to change it edit run-jboss-cli.bat !!
-->
<argument>${cli.win.working.dir}</argument>
<argument>-Dserver.config=standalone-ha.xml</argument>
</arguments>
</configuration>
@@ -349,10 +402,16 @@
</goals>
<configuration>
<skip>${skip.adapter.offline.installation}</skip>
<executable>./jboss-cli.${script.suffix}</executable>
<workingDirectory>${app.server.jboss.home}/bin</workingDirectory>
<executable>${cli.executable}</executable>
<workingDirectory>${cli.working.dir}</workingDirectory>
<arguments>
<argument>--file=${app.server.jboss.home}/bin/adapter-install-offline.cli</argument>
<!--
following attribute is required just for windows
see ${common.resources}/win/run-jboss-cli.bat
!! the ordering is important, if you need to change it edit run-jboss-cli.bat !!
-->
<argument>${cli.win.working.dir}</argument>
</arguments>
</configuration>
</execution>
@@ -365,10 +424,16 @@
</goals>
<configuration>
<skip>${skip.adapter.offline.installation}</skip>
<executable>./jboss-cli.${script.suffix}</executable>
<workingDirectory>${app.server.jboss.home}/bin</workingDirectory>
<executable>${cli.executable}</executable>
<workingDirectory>${cli.working.dir}</workingDirectory>
<arguments>
<argument>--file=${app.server.jboss.home}/bin/adapter-install-offline.cli</argument>
<!--
following attribute is required just for windows
see ${common.resources}/win/run-jboss-cli.bat
!! the ordering is important, if you need to change it edit run-jboss-cli.bat !!
-->
<argument>${cli.win.working.dir}</argument>
<argument>-Dserver.config=standalone-ha.xml</argument>
</arguments>
</configuration>
@@ -382,10 +447,16 @@
</goals>
<configuration>
<skip>${skip.adapter.offline.installation}</skip>
<executable>./jboss-cli.${script.suffix}</executable>
<workingDirectory>${app.server.jboss.home}/bin</workingDirectory>
<executable>${cli.executable}</executable>
<workingDirectory>${cli.working.dir}</workingDirectory>
<arguments>
<argument>--file=${app.server.jboss.home}/bin/adapter-install-saml-offline.cli</argument>
<!--
following attribute is required just for windows
see ${common.resources}/win/run-jboss-cli.bat
!! the ordering is important, if you need to change it edit run-jboss-cli.bat !!
-->
<argument>${cli.win.working.dir}</argument>
</arguments>
</configuration>
</execution>
@@ -398,10 +469,16 @@
</goals>
<configuration>
<skip>${skip.adapter.offline.installation}</skip>
<executable>./jboss-cli.${script.suffix}</executable>
<workingDirectory>${app.server.jboss.home}/bin</workingDirectory>
<executable>${cli.executable}</executable>
<workingDirectory>${cli.working.dir}</workingDirectory>
<arguments>
<argument>--file=${app.server.jboss.home}/bin/adapter-install-saml-offline.cli</argument>
<!--
following attribute is required just for windows
see ${common.resources}/win/run-jboss-cli.bat
!! the ordering is important, if you need to change it edit run-jboss-cli.bat !!
-->
<argument>${cli.win.working.dir}</argument>
<argument>-Dserver.config=standalone-ha.xml</argument>
</arguments>
</configuration>
@@ -414,10 +491,16 @@
</goals>
<configuration>
<skip>${skip.apply.offline.cli}</skip><!--eap6-->
<workingDirectory>${app.server.jboss.home}/bin</workingDirectory>
<executable>./jboss-cli.sh</executable>
<executable>${cli.executable}</executable>
<workingDirectory>${cli.working.dir}</workingDirectory>
<arguments>
<argument>--file=${common.resources}/cli/add-secured-deployments.cli</argument>
<!--
following attribute is required just for windows
see ${common.resources}/win/run-jboss-cli.bat
!! the ordering is important, if you need to change it edit run-jboss-cli.bat !!
-->
<argument>${cli.win.working.dir}</argument>
<argument>-Djboss.server.config.dir=${app.server.jboss.home}/standalone-secured-deployments/configuration</argument>
</arguments>
</configuration>
@@ -430,10 +513,16 @@
</goals>
<configuration>
<skip>${skip.configure.clustered.scenario}</skip><!--eap6, wildfly9-->
<workingDirectory>${app.server.jboss.home}/bin</workingDirectory>
<executable>./jboss-cli.sh</executable>
<executable>${cli.executable}</executable>
<workingDirectory>${cli.working.dir}</workingDirectory>
<arguments>
<argument>--file=${common.resources}/cli/configure-cluster-config.cli</argument>
<!--
following attribute is required just for windows
see ${common.resources}/win/run-jboss-cli.bat
!! the ordering is important, if you need to change it edit run-jboss-cli.bat !!
-->
<argument>${cli.win.working.dir}</argument>
<argument>-Djboss.server.config.dir=${app.server.jboss.home}/standalone-cluster/configuration</argument>
</arguments>
</configuration>
@@ -446,10 +535,16 @@
</goals>
<configuration>
<skip>${skip.configure.clustered.scenario}</skip><!--eap6, wildfly9-->
<workingDirectory>${app.server.jboss.home}/bin</workingDirectory>
<executable>./jboss-cli.sh</executable>
<executable>${cli.executable}</executable>
<workingDirectory>${cli.working.dir}</workingDirectory>
<arguments>
<argument>--file=${common.resources}/cli/configure-crossdc-config.cli</argument>
<!--
following attribute is required just for windows
see ${common.resources}/win/run-jboss-cli.bat
!! the ordering is important, if you need to change it edit run-jboss-cli.bat !!
-->
<argument>${cli.win.working.dir}</argument>
<argument>-Djboss.server.config.dir=${app.server.jboss.home}/standalone-crossdc/configuration</argument>
</arguments>
</configuration>
@@ -36,6 +36,10 @@
<groupId>org.keycloak</groupId>
<artifactId>keycloak-saml-undertow-adapter</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-saml-servlet-filter-adapter</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-servers-app-server-spi</artifactId>
@@ -132,12 +132,27 @@ public class TestLDAPResource {
LDAPObject defaultGroup1 = LDAPTestUtils.createLDAPGroup(session, realm, ldapModel, "defaultGroup1", descriptionAttrName, "Default Group1 - description");
LDAPObject defaultGroup11 = LDAPTestUtils.createLDAPGroup(session, realm, ldapModel, "defaultGroup11");
LDAPObject defaultGroup12 = LDAPTestUtils.createLDAPGroup(session, realm, ldapModel, "defaultGroup12", descriptionAttrName, "Default Group12 - description");
LDAPTestUtils.createLDAPGroup(session, realm, ldapModel, "Team 2016/2017", descriptionAttrName, "A group with slashes in the name");
LDAPObject teamChild20182019 = LDAPTestUtils.createLDAPGroup(session, realm, ldapModel, "Team Child 2018/2019", descriptionAttrName, "A child group with slashes in the name");
LDAPObject teamSubChild20202021 = LDAPTestUtils.createLDAPGroup(session, realm, ldapModel, "Team SubChild 2020/2021", descriptionAttrName, "A sub child group with slashes in the name");
LDAPObject defaultGroup13 = LDAPTestUtils.createLDAPGroup(session, realm, ldapModel, "defaultGroup13", descriptionAttrName, "Default Group13 - description");
LDAPObject teamSubChild20222023 = LDAPTestUtils.createLDAPGroup(session, realm, ldapModel, "Team SubChild 2022/2023/A/B/C/D/E", descriptionAttrName, "A sub child group with slashes in the name");
LDAPObject defaultGroup14 = LDAPTestUtils.createLDAPGroup(session, realm, ldapModel, "defaultGroup14", descriptionAttrName, "Default Group14 - description");
LDAPObject teamRoot20242025 = LDAPTestUtils.createLDAPGroup(session, realm, ldapModel, "Team Root 2024/2025/A/B/C/D", descriptionAttrName, "A sub child group with slashes in the name");
LDAPObject defaultGroup15 = LDAPTestUtils.createLDAPGroup(session, realm, ldapModel, "defaultGroup15", descriptionAttrName, "Default Group15 - description");
LDAPObject teamSubChild20262027 = LDAPTestUtils.createLDAPGroup(session, realm, ldapModel, "Team SubChild 2026/2027", descriptionAttrName, "A sub child group with slashes in the name");
LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, "not-used", group1, group11, false);
LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, "not-used", group1, group12, true);
LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, "not-used", defaultGroup1, defaultGroup11, false);
LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, "not-used", defaultGroup1, defaultGroup12, true);
LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, "not-used", defaultGroup1, teamChild20182019, true);
LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, "not-used", teamChild20182019, teamSubChild20202021, true);
LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, "not-used", defaultGroup13, teamSubChild20222023, true);
LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, "not-used", teamSubChild20222023, defaultGroup14, true);
LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, "not-used", teamRoot20242025, defaultGroup15, true);
LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, "not-used", defaultGroup15, teamSubChild20262027, true);
// Sync LDAP groups to Keycloak DB
ComponentModel mapperModel = LDAPTestUtils.getSubcomponentByName(realm, ldapModel, "groupsMapper");
@@ -124,6 +124,14 @@
{
"name": "Pattern 16",
"uris": ["/keycloak-7269/sub-resource1", "/keycloak-7269/sub-resource2/*", "/keycloak-7269/sub-resource1/{test-pattern}/specialSuffix"]
},
{
"name": "Pattern 17",
"uris": ["/keycloak-8823/resource/{version}/subresource/{id}/{other}"]
},
{
"name": "Pattern 17 Entities",
"uris": ["/keycloak-8823/resource/{version}/subresource/{id}/entities"]
}
],
"policies": [
@@ -316,6 +324,26 @@
"resources": "[\"Pattern 16\"]",
"applyPolicies": "[\"Default Policy\"]"
}
},
{
"name": "Pattern 17 Permission",
"type": "resource",
"logic": "POSITIVE",
"decisionStrategy": "UNANIMOUS",
"config": {
"resources": "[\"Pattern 17\"]",
"applyPolicies": "[\"Default Policy\"]"
}
},
{
"name": "Pattern 17 Entities Permission",
"type": "resource",
"logic": "POSITIVE",
"decisionStrategy": "UNANIMOUS",
"config": {
"resources": "[\"Pattern 17 Entities\"]",
"applyPolicies": "[\"Default Policy\"]"
}
}
],
"scopes": []
@@ -85,6 +85,14 @@
{
"name": "Pattern 16",
"path": "/keycloak-7269/sub-resource1/{test-pattern}/specialSuffix"
},
{
"name": "Pattern 17",
"path": "/keycloak-8823/resource/{version}/subresource/{id}/{other}"
},
{
"name": "Pattern 17 Entities",
"path": "/keycloak-8823/resource/{version}/subresource/{id}/entities"
}
]
}
@@ -39,10 +39,11 @@ public class ProfileAssume {
static {
String host = System.getProperty("auth.server.host", "localhost");
String port = System.getProperty("auth.server.http.port", "8180");
boolean adapterCompatTesting = Boolean.parseBoolean(System.getProperty("testsuite.adapter.compat.testing"));
String authServerContextRoot = "http://" + host + ":" + port;
try {
Keycloak adminClient = AdminClientUtil.createAdminClient(false, authServerContextRoot);
Keycloak adminClient = AdminClientUtil.createAdminClient(adapterCompatTesting, authServerContextRoot);
ProfileInfoRepresentation profileInfo = adminClient.serverInfo().getInfo().getProfileInfo();
profile = profileInfo.getName();
List<String> disabled = profileInfo.getDisabledFeatures();
@@ -31,7 +31,6 @@ import org.jboss.arquillian.core.spi.Validate;
import org.jboss.arquillian.core.api.annotation.Inject;
import org.jboss.arquillian.core.api.annotation.Observes;
import org.jboss.arquillian.test.spi.event.suite.After;
import org.jboss.arquillian.test.spi.event.suite.AfterSuite;
import org.jboss.arquillian.test.spi.event.suite.Before;
import org.jboss.logging.Logger;
import static org.junit.Assert.assertThat;
@@ -44,7 +43,12 @@ import org.keycloak.testsuite.crossdc.DC;
import org.keycloak.testsuite.crossdc.ServerSetup;
import java.util.Collection;
import java.util.function.Consumer;
import java.util.stream.Stream;
import org.jboss.arquillian.container.spi.Container;
import org.jboss.arquillian.container.spi.event.StopContainer;
import org.jboss.arquillian.container.spi.event.StopSuiteContainers;
import org.jboss.arquillian.core.api.Event;
import org.jboss.arquillian.test.spi.event.suite.AfterSuite;
/**
*
@@ -58,6 +62,9 @@ public class CrossDCTestEnricher {
@Inject
private static Instance<ContainerController> containerController;
@Inject
private Event<StopContainer> stopContainer;
private static final Map<ContainerInfo, Keycloak> backendAdminClients = new HashMap<>();
private static final Map<ContainerInfo, KeycloakTestingClient> backendTestingClients = new HashMap<>();
@@ -87,6 +94,28 @@ public class CrossDCTestEnricher {
ServerSetup cacheServers = annotation.cacheServers();
ServerSetup authServers = annotation.authServers();
// Stop auth servers that otherwise could be hang connecting to a cache server stopped next
switch (authServers) {
case ALL_NODES_IN_EVERY_DC:
break;
case FIRST_NODE_IN_EVERY_DC:
DC.validDcsStream().forEach((DC dc) -> stopAuthServerBackendNode(dc, 1));
break;
case FIRST_NODE_IN_FIRST_DC:
stopAuthServerBackendNode(DC.FIRST, 1);
forAllBackendNodesInDc(DC.SECOND, CrossDCTestEnricher::stopAuthServerBackendNode);
break;
case ALL_NODES_IN_FIRST_DC_FIRST_NODE_IN_SECOND_DC:
stopAuthServerBackendNode(DC.SECOND, 1);
break;
case ALL_NODES_IN_FIRST_DC_NO_NODES_IN_SECOND_DC:
forAllBackendNodesInDc(DC.SECOND, CrossDCTestEnricher::stopAuthServerBackendNode);
break;
}
switch (cacheServers) {
case ALL_NODES_IN_EVERY_DC:
case FIRST_NODE_IN_EVERY_DC: //the same as ALL_NODES_IN_EVERY_DC as there is only one cache server per DC
@@ -107,24 +136,19 @@ public class CrossDCTestEnricher {
break;
case FIRST_NODE_IN_EVERY_DC:
DC.validDcsStream().forEach((DC dc) -> startAuthServerBackendNode(dc, 0));
DC.validDcsStream().forEach((DC dc) -> stopAuthServerBackendNode(dc, 1));
break;
case FIRST_NODE_IN_FIRST_DC:
startAuthServerBackendNode(DC.FIRST, 0);
stopAuthServerBackendNode(DC.FIRST, 1);
forAllBackendNodesInDc(DC.SECOND, CrossDCTestEnricher::stopAuthServerBackendNode);
break;
case ALL_NODES_IN_FIRST_DC_FIRST_NODE_IN_SECOND_DC:
forAllBackendNodesInDc(DC.FIRST, CrossDCTestEnricher::startAuthServerBackendNode);
startAuthServerBackendNode(DC.SECOND, 0);
stopAuthServerBackendNode(DC.SECOND, 1);
break;
case ALL_NODES_IN_FIRST_DC_NO_NODES_IN_SECOND_DC:
forAllBackendNodesInDc(DC.FIRST, CrossDCTestEnricher::startAuthServerBackendNode);
forAllBackendNodesInDc(DC.SECOND, CrossDCTestEnricher::stopAuthServerBackendNode);
break;
}
@@ -137,11 +161,30 @@ public class CrossDCTestEnricher {
restorePeriodicTasks();
}
public void afterSuite(@Observes(precedence = 4) AfterSuite event) {
if (!suiteContext.isAuthServerCrossDc()) return;
// Unfortunately, in AfterSuite, containerController context is already cleaned so stopAuthServerBackendNode()
// and stopCacheServer cannot be used. On the other hand, Arquillian by default does not guarantee that cache
// servers are terminated only after auth servers were, so the termination has to be done in this enricher.
forAllBackendNodesStream()
.map(ContainerInfo::getArquillianContainer)
.map(StopContainer::new)
.forEach(stopContainer::fire);
DC.validDcsStream()
.map(CrossDCTestEnricher::getCacheServer)
.map(ContainerInfo::getArquillianContainer)
.map(StopContainer::new)
.forEach(stopContainer::fire);
}
public void stopSuiteContainers(@Observes(precedence = 4) StopSuiteContainers event) {
if (!suiteContext.isAuthServerCrossDc()) return;
DC.validDcsStream().forEach(CrossDCTestEnricher::stopCacheServer);
forAllBackendNodes(CrossDCTestEnricher::stopAuthServerBackendNode);
DC.validDcsStream().forEach(CrossDCTestEnricher::stopCacheServer);
}
private static void createRESTClientsForNode(ContainerInfo node) {
@@ -257,11 +300,15 @@ public class CrossDCTestEnricher {
}
public static void forAllBackendNodes(Consumer<ContainerInfo> functionOnContainerInfo) {
suiteContext.getDcAuthServerBackendsInfo().stream()
.flatMap(Collection::stream)
forAllBackendNodesStream()
.forEach(functionOnContainerInfo);
}
public static Stream<ContainerInfo> forAllBackendNodesStream() {
return suiteContext.getDcAuthServerBackendsInfo().stream()
.flatMap(Collection::stream);
}
public static void forAllBackendNodesInDc(DC dc, Consumer<ContainerInfo> functionOnContainerInfo) {
assertValidDc(dc);
suiteContext.getDcAuthServerBackendsInfo().get(dc.ordinal()).stream()
@@ -760,6 +760,10 @@ public class OAuthClient {
return redirectUri;
}
public String getNonce() {
return nonce;
}
public String getLoginFormUrl() {
UriBuilder b = OIDCLoginProtocolService.authUrl(UriBuilder.fromUri(baseUrl));
if (responseType != null) {
@@ -487,6 +487,41 @@ public class ServletPolicyEnforcerTest extends AbstractExampleAdapterTest {
});
}
@Test
public void testOverloadedTemplateUri() {
performTests(() -> {
login("alice", "alice");
navigateTo("/keycloak-8823/resource/v1/subresource/123/entities");
assertFalse(wasDenied());
navigateTo("/keycloak-8823/resource/v1/subresource/123/someother");
assertFalse(wasDenied());
updatePermissionPolicies("Pattern 17 Entities Permission", "Deny Policy");
login("alice", "alice");
navigateTo("/keycloak-8823/resource/v1/subresource/123/entities");
assertTrue(wasDenied());
navigateTo("/keycloak-8823/resource/v1/subresource/123/someother");
assertFalse(wasDenied());
updatePermissionPolicies("Pattern 17 Entities Permission", "Default Policy");
updatePermissionPolicies("Pattern 17 Permission", "Deny Policy");
login("alice", "alice");
navigateTo("/keycloak-8823/resource/v1/subresource/123/entities");
assertFalse(wasDenied());
navigateTo("/keycloak-8823/resource/v1/subresource/123/someother");
assertTrue(wasDenied());
updatePermissionPolicies("Pattern 17 Entities Permission", "Default Policy");
updatePermissionPolicies("Pattern 17 Permission", "Default Policy");
login("alice", "alice");
navigateTo("/keycloak-8823/resource/v1/subresource/123/entities");
assertFalse(wasDenied());
navigateTo("/keycloak-8823/resource/v1/subresource/123/someother");
assertFalse(wasDenied());
});
}
private void navigateTo(String path) {
this.driver.navigate().to(getResourceServerUrl() + path);
}
@@ -71,6 +71,9 @@ public class UserFederationLdapConnectionTest extends AbstractAdminTest {
response = realm.testLDAPConnection(LDAPConnectionTestManager.TEST_AUTHENTICATION, "ldaps://localhost:10636", "uid=admin,ou=system", "secret", "true", null);
assertStatus(response, 204);
response = realm.testLDAPConnection(LDAPConnectionTestManager.TEST_AUTHENTICATION, "ldaps://localhost:10636", "uid=admin,ou=system", "secret", "true", "10000");
assertStatus(response, 204);
}
private void assertStatus(Response response, int status) {
@@ -22,6 +22,7 @@ import org.junit.Test;
import org.keycloak.admin.client.resource.ClientScopesResource;
import org.keycloak.admin.client.resource.ProtocolMappersResource;
import org.keycloak.admin.client.resource.RoleMappingResource;
import org.keycloak.common.util.ObjectUtil;
import org.keycloak.events.admin.OperationType;
import org.keycloak.events.admin.ResourceType;
import org.keycloak.models.AccountRoles;
@@ -135,12 +136,19 @@ public class ClientScopeTest extends AbstractClientTest {
scopeRep.setName("scope1");
scopeRep.setDescription("scope1-desc");
scopeRep.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
Map<String, String> attrs = new HashMap<>();
attrs.put("someAttr", "someAttrValue");
attrs.put("emptyAttr", "");
scopeRep.setAttributes(attrs);
String scope1Id = createClientScope(scopeRep);
// Assert created attributes
scopeRep = clientScopes().get(scope1Id).toRepresentation();
Assert.assertEquals("scope1", scopeRep.getName());
Assert.assertEquals("scope1-desc", scopeRep.getDescription());
Assert.assertEquals("someAttrValue", scopeRep.getAttributes().get("someAttr"));
Assert.assertTrue(ObjectUtil.isBlank(scopeRep.getAttributes().get("emptyAttr")));
Assert.assertEquals(OIDCLoginProtocol.LOGIN_PROTOCOL, scopeRep.getProtocol());
@@ -149,6 +157,9 @@ public class ClientScopeTest extends AbstractClientTest {
scopeRep.setDescription("scope1-desc-updated");
scopeRep.setProtocol(SamlProtocol.LOGIN_PROTOCOL);
// Test update attribute to some non-blank value
scopeRep.getAttributes().put("emptyAttr", "someValue");
clientScopes().get(scope1Id).update(scopeRep);
assertAdminEvents.assertEvent(getRealmId(), OperationType.UPDATE, AdminEventPaths.clientScopeResourcePath(scope1Id), scopeRep, ResourceType.CLIENT_SCOPE);
@@ -158,6 +169,8 @@ public class ClientScopeTest extends AbstractClientTest {
Assert.assertEquals("scope1-updated", scopeRep.getName());
Assert.assertEquals("scope1-desc-updated", scopeRep.getDescription());
Assert.assertEquals(SamlProtocol.LOGIN_PROTOCOL, scopeRep.getProtocol());
Assert.assertEquals("someAttrValue", scopeRep.getAttributes().get("someAttr"));
Assert.assertEquals("someValue", scopeRep.getAttributes().get("emptyAttr"));
// Remove scope1
clientScopes().get(scope1Id).remove();
@@ -48,6 +48,8 @@ import org.keycloak.OAuth2Constants;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.admin.client.resource.ClientsResource;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.common.util.Retry;
@@ -56,12 +58,14 @@ import org.keycloak.testsuite.util.ClientBuilder;
import org.keycloak.testsuite.util.OAuthClient;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import org.apache.http.client.CookieStore;
import org.apache.http.impl.client.BasicCookieStore;
import org.hamcrest.Matchers;
import org.keycloak.util.JsonSerialization;
import static org.hamcrest.Matchers.containsString;
@@ -321,6 +325,10 @@ public class ConcurrentLoginTest extends AbstractConcurrencyTest {
protected OAuthClient initialValue() {
OAuthClient oauth1 = new OAuthClient();
oauth1.init(driver);
// Add some randomness to nonce and redirectUri. Verify that login is successful and nonce will match
oauth1.nonce(KeycloakModelUtils.generateId());
oauth1.redirectUri(oauth.getRedirectUri() + "?some=" + new Random().nextInt(1024));
return oauth1;
}
};
@@ -375,16 +383,25 @@ public class ConcurrentLoginTest extends AbstractConcurrencyTest {
accessResRef.set(accessRes);
// Refresh access + refresh token using refresh token
AtomicReference<OAuthClient.AccessTokenResponse> refreshResRef = new AtomicReference<>();
int invocationIndex = Retry.execute(() -> {
OAuthClient.AccessTokenResponse refreshRes = oauth1.doRefreshTokenRequest(accessResRef.get().getRefreshToken(), "password");
Assert.assertEquals("AccessTokenResponse: client: " + oauth1.getClientId() + ", error: '" + refreshRes.getError() + "' desc: '" + refreshRes.getErrorDescription() + "'",
200, refreshRes.getStatusCode());
refreshResRef.set(refreshRes);
}, retryCount, retryDelayMs);
retryHistogram[invocationIndex].incrementAndGet();
AccessToken token = JsonSerialization.readValue(new JWSInput(accessResRef.get().getAccessToken()).getContent(), AccessToken.class);
Assert.assertEquals("Invalid nonce.", token.getNonce(), oauth1.getNonce());
AccessToken refreshedToken = JsonSerialization.readValue(new JWSInput(refreshResRef.get().getAccessToken()).getContent(), AccessToken.class);
Assert.assertEquals("Invalid nonce.", refreshedToken.getNonce(), oauth1.getNonce());
if (userSessionId.get() == null) {
AccessToken token = oauth1.verifyToken(accessResRef.get().getAccessToken());
userSessionId.set(token.getSessionState());
}
}
@@ -77,6 +77,7 @@ import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import org.keycloak.representations.idm.authorization.ScopePermissionRepresentation;
import org.keycloak.representations.idm.authorization.UserPolicyRepresentation;
import org.keycloak.testsuite.util.ClientBuilder;
import org.keycloak.testsuite.util.ContainerAssume;
import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.RealmBuilder;
import org.keycloak.testsuite.util.RoleBuilder;
@@ -1169,6 +1170,8 @@ public class EntitlementAPITest extends AbstractAuthzTest {
@Test
public void testOfflineRequestingPartyToken() throws Exception {
ContainerAssume.assumeNotAuthServerUndertow();
ClientResource client = getClient(getRealm(), RESOURCE_SERVER_TEST);
AuthorizationResource authorization = client.authorization();
@@ -73,7 +73,9 @@ public class KcOidcBrokerLogoutTest extends AbstractBaseBrokerTest {
logoutFromRealm(bc.consumerRealmName(), "kc-oidc-idp");
driver.navigate().to(getAccountUrl(REALM_PROV_NAME));
waitForPage(driver, "keycloak account management", true);
//could be 'keycloak account management' or 'rh-sso account management'
waitForPage(driver, " account management", true);
}
@Test
@@ -25,6 +25,7 @@ import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.admin.client.resource.ClientScopeResource;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.authentication.AuthenticationFlow;
import org.keycloak.common.constants.KerberosConstants;
import org.keycloak.models.Constants;
import org.keycloak.models.LDAPConstants;
@@ -59,16 +60,20 @@ import org.keycloak.testsuite.client.KeycloakTestingClient;
import org.keycloak.testsuite.util.RealmRepUtil;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.keycloak.common.Profile;
import org.hamcrest.Matcher;
import org.hamcrest.Matchers;
import static org.junit.Assert.assertThat;
/**
*
@@ -133,6 +138,16 @@ public class ExportImportUtil {
Assert.assertEquals("client-secret", application.getClientAuthenticatorType());
Assert.assertEquals("client-jwt", otherApp.getClientAuthenticatorType());
// test authenticationFlowBindingOverrides
Map<String, String> flowMap = otherApp.getAuthenticationFlowBindingOverrides();
Assert.assertNotNull(flowMap);
Assert.assertEquals(1, flowMap.size());
Assert.assertTrue(flowMap.containsKey("browser"));
// if the authentication flows were correctly imported there must be a flow whose id matches the one in the authenticationFlowBindingOverrides
AuthenticationFlowRepresentation flowRep = realmRsc.flows().getFlow(flowMap.get("browser"));
Assert.assertNotNull(flowRep);
Assert.assertEquals("browser", flowRep.getAlias());
// Test finding applications by ID
Assert.assertNull(ApiUtil.findClientResourceById(realmRsc, "982734"));
Assert.assertEquals(application.getId(), ApiUtil.findClientResourceById(realmRsc, application.getId()).toRepresentation().getId());
@@ -368,7 +383,7 @@ public class ExportImportUtil {
Map<String, Object> appAdminConsent = findConsentByClientId(consents, application.getClientId());
Assert.assertNotNull(appAdminConsent);
Assert.assertTrue(isClientScopeGranted(appAdminConsent, OAuth2Constants.OFFLINE_ACCESS));
Assert.assertTrue(isClientScopeGranted(appAdminConsent, OAuth2Constants.OFFLINE_ACCESS, "roles", "profile", "email", "account", "web-origins"));
Map<String, Object> otherAppAdminConsent = findConsentByClientId(consents, otherApp.getClientId());//admin.getConsentByClient(otherApp.getId());
Assert.assertFalse(isClientScopeGranted(otherAppAdminConsent, OAuth2Constants.OFFLINE_ACCESS));
@@ -392,9 +407,9 @@ public class ExportImportUtil {
}
private static boolean isClientScopeGranted(Map<String, Object> consent, String clientScopeName) {
private static boolean isClientScopeGranted(Map<String, Object> consent, String... clientScopeNames) {
if (consent.get("grantedClientScopes") == null) return false;
return ((List)consent.get("grantedClientScopes")).contains(clientScopeName);
return ((List)consent.get("grantedClientScopes")).containsAll(Arrays.asList(clientScopeNames));
}
@@ -634,21 +649,39 @@ public class ExportImportUtil {
Assert.assertTrue(!source.stream().filter(object -> !predicate.stream().filter(predicate1 -> predicate1.test(object)).findFirst().isPresent()).findAny().isPresent());
}
private static Matcher<Iterable<? super String>> getDefaultClientScopeNameMatcher(ClientRepresentation rep) {
switch (rep.getClientId()) {
case "client-with-template":
return Matchers.hasItem("Default_test_template");
default:
return Matchers.not(Matchers.hasItem("Default_test_template"));
}
}
public static void testClientDefaultClientScopes(RealmResource realm) {
for (ClientRepresentation rep : realm.clients().findAll(true)) {
Matcher<Iterable<? super String>> expectedDefaultClientScopeNames = getDefaultClientScopeNameMatcher(rep);
assertThat("Default client scopes for " + rep.getClientId(), rep.getDefaultClientScopes(), expectedDefaultClientScopeNames);
}
}
public static void testRealmDefaultClientScopes(RealmResource realm) {
// Assert built-in scopes were created in realm
List<ClientScopeRepresentation> clientScopes = realm.clientScopes().findAll();
Map<String, ClientScopeRepresentation> clientScopesMap = clientScopes
.stream().collect(Collectors.toMap(clientScope -> clientScope.getName(), clientScope -> clientScope));
Map<String, ClientScopeRepresentation> clientScopesMap = clientScopes.stream()
.collect(Collectors.toMap(ClientScopeRepresentation::getName, Function.identity()));
org.keycloak.testsuite.Assert.assertTrue(clientScopesMap.containsKey(OAuth2Constants.SCOPE_PROFILE));
org.keycloak.testsuite.Assert.assertTrue(clientScopesMap.containsKey(OAuth2Constants.SCOPE_EMAIL));
org.keycloak.testsuite.Assert.assertTrue(clientScopesMap.containsKey(OAuth2Constants.SCOPE_ADDRESS));
org.keycloak.testsuite.Assert.assertTrue(clientScopesMap.containsKey(OAuth2Constants.SCOPE_PHONE));
org.keycloak.testsuite.Assert.assertTrue(clientScopesMap.containsKey(OAuth2Constants.OFFLINE_ACCESS));
org.keycloak.testsuite.Assert.assertTrue(clientScopesMap.containsKey(OIDCLoginProtocolFactory.ROLES_SCOPE));
org.keycloak.testsuite.Assert.assertTrue(clientScopesMap.containsKey(OIDCLoginProtocolFactory.WEB_ORIGINS_SCOPE));
org.keycloak.testsuite.Assert.assertTrue(clientScopesMap.containsKey(SamlProtocolFactory.SCOPE_ROLE_LIST));
assertThat(clientScopesMap.keySet(), Matchers.hasItems(
OAuth2Constants.SCOPE_PROFILE,
OAuth2Constants.SCOPE_EMAIL,
OAuth2Constants.SCOPE_ADDRESS,
OAuth2Constants.SCOPE_PHONE,
OAuth2Constants.OFFLINE_ACCESS,
OIDCLoginProtocolFactory.ROLES_SCOPE,
OIDCLoginProtocolFactory.WEB_ORIGINS_SCOPE,
SamlProtocolFactory.SCOPE_ROLE_LIST
));
// Check content of some client scopes
Map<String, ProtocolMapperRepresentation> protocolMappers = clientScopesMap.get(OAuth2Constants.SCOPE_EMAIL).getProtocolMappers()
@@ -661,17 +694,21 @@ public class ExportImportUtil {
org.keycloak.testsuite.Assert.assertNames(offlineRoleScopes, OAuth2Constants.OFFLINE_ACCESS);
// Check default client scopes and optional client scopes expected
Map<String, ClientScopeRepresentation> defaultClientScopes = realm.getDefaultDefaultClientScopes()
.stream().collect(Collectors.toMap(clientScope -> clientScope.getName(), clientScope -> clientScope));
org.keycloak.testsuite.Assert.assertTrue(defaultClientScopes.containsKey(OAuth2Constants.SCOPE_PROFILE));
org.keycloak.testsuite.Assert.assertTrue(defaultClientScopes.containsKey(OAuth2Constants.SCOPE_EMAIL));
org.keycloak.testsuite.Assert.assertTrue(defaultClientScopes.containsKey(OIDCLoginProtocolFactory.ROLES_SCOPE));
org.keycloak.testsuite.Assert.assertTrue(defaultClientScopes.containsKey(OIDCLoginProtocolFactory.WEB_ORIGINS_SCOPE));
Set<String> defaultClientScopes = realm.getDefaultDefaultClientScopes()
.stream().map(ClientScopeRepresentation::getName).collect(Collectors.toSet());
assertThat(defaultClientScopes, Matchers.hasItems(
OAuth2Constants.SCOPE_PROFILE,
OAuth2Constants.SCOPE_EMAIL,
OIDCLoginProtocolFactory.ROLES_SCOPE,
OIDCLoginProtocolFactory.WEB_ORIGINS_SCOPE
));
Map<String, ClientScopeRepresentation> optionalClientScopes = realm.getDefaultOptionalClientScopes()
.stream().collect(Collectors.toMap(clientScope -> clientScope.getName(), clientScope -> clientScope));
org.keycloak.testsuite.Assert.assertTrue(optionalClientScopes.containsKey(OAuth2Constants.SCOPE_ADDRESS));
org.keycloak.testsuite.Assert.assertTrue(optionalClientScopes.containsKey(OAuth2Constants.SCOPE_PHONE));
org.keycloak.testsuite.Assert.assertTrue(optionalClientScopes.containsKey(OAuth2Constants.OFFLINE_ACCESS));
Set<String> optionalClientScopes = realm.getDefaultOptionalClientScopes()
.stream().map(ClientScopeRepresentation::getName).collect(Collectors.toSet());
assertThat(optionalClientScopes, Matchers.hasItems(
OAuth2Constants.SCOPE_ADDRESS,
OAuth2Constants.SCOPE_PHONE,
OAuth2Constants.OFFLINE_ACCESS
));
}
}
@@ -116,6 +116,20 @@ public class LDAPGroupMapperTest extends AbstractLDAPTest {
GroupModel group12 = KeycloakModelUtils.findGroupByPath(appRealm, "/group1/group12");
john.joinGroup(group12);
mary.joinGroup(group12);
// This group should already exists as it was imported from LDAP
GroupModel groupWithSlashesInName = KeycloakModelUtils.findGroupByPath(appRealm, "Team 2016/2017");
john.joinGroup(groupWithSlashesInName);
mary.joinGroup(groupWithSlashesInName);
// This group should already exists as it was imported from LDAP
GroupModel groupChildWithSlashesInName = KeycloakModelUtils.findGroupByPath(appRealm, "defaultGroup1/Team Child 2018/2019");
john.joinGroup(groupChildWithSlashesInName);
mary.joinGroup(groupChildWithSlashesInName);
Assert.assertEquals("Team SubChild 2020/2021", KeycloakModelUtils.findGroupByPath(appRealm, "defaultGroup1/Team Child 2018/2019/Team SubChild 2020/2021").getName());
Assert.assertEquals("defaultGroup14", KeycloakModelUtils.findGroupByPath(appRealm, "defaultGroup13/Team SubChild 2022/2023/A/B/C/D/E/defaultGroup14").getName());
Assert.assertEquals("Team SubChild 2026/2027", KeycloakModelUtils.findGroupByPath(appRealm, "Team Root 2024/2025/A/B/C/D/defaultGroup15/Team SubChild 2026/2027").getName());
});
@@ -155,16 +169,20 @@ public class LDAPGroupMapperTest extends AbstractLDAPTest {
GroupModel group1 = KeycloakModelUtils.findGroupByPath(appRealm, "/group1");
GroupModel group11 = KeycloakModelUtils.findGroupByPath(appRealm, "/group1/group11");
GroupModel group12 = KeycloakModelUtils.findGroupByPath(appRealm, "/group1/group12");
GroupModel groupTeam20162017 = KeycloakModelUtils.findGroupByPath(appRealm, "Team 2016/2017");
GroupModel groupTeamChild20182019 = KeycloakModelUtils.findGroupByPath(appRealm, "defaultGroup1/Team Child 2018/2019");
UserModel john = session.users().getUserByUsername("johnkeycloak", appRealm);
UserModel mary = session.users().getUserByUsername("marykeycloak", appRealm);
Set<GroupModel> johnGroups = john.getGroups();
Assert.assertEquals(2, johnGroups.size());
Assert.assertEquals(4, johnGroups.size());
long groupCount = john.getGroupsCount();
Assert.assertEquals(2, groupCount);
Assert.assertEquals(4, groupCount);
Assert.assertTrue(johnGroups.contains(group1));
Assert.assertFalse(johnGroups.contains(group11));
Assert.assertTrue(johnGroups.contains(group12));
Assert.assertTrue(johnGroups.contains(groupTeam20162017));
Assert.assertTrue(johnGroups.contains(groupTeamChild20182019));
Set<GroupModel> johnGroupsWithGr = john.getGroups("gr", 0, 10);
Assert.assertEquals(2, johnGroupsWithGr.size());
@@ -178,24 +196,38 @@ public class LDAPGroupMapperTest extends AbstractLDAPTest {
Set<GroupModel> johnGroupsWith12 = john.getGroups("12", 0, 10);
Assert.assertEquals(1, johnGroupsWith12.size());
Set<GroupModel> johnGroupsWith2017 = john.getGroups("2017", 0, 10);
Assert.assertEquals(1, johnGroupsWith2017.size());
Set<GroupModel> johnGroupsWith2018 = john.getGroups("2018", 0, 10);
Assert.assertEquals(1, johnGroupsWith2017.size());
// 4 - Check through userProvider
List<UserModel> group1Members = session.users().getGroupMembers(appRealm, group1, 0, 10);
List<UserModel> group11Members = session.users().getGroupMembers(appRealm, group11, 0, 10);
List<UserModel> group12Members = session.users().getGroupMembers(appRealm, group12, 0, 10);
List<UserModel> groupTeam20162017Members = session.users().getGroupMembers(appRealm, groupTeam20162017, 0, 10);
List<UserModel> groupTeam20182019Members = session.users().getGroupMembers(appRealm, groupTeamChild20182019, 0, 10);
Assert.assertEquals(1, group1Members.size());
Assert.assertEquals("johnkeycloak", group1Members.get(0).getUsername());
Assert.assertEquals(1, group11Members.size());
Assert.assertEquals("marykeycloak", group11Members.get(0).getUsername());
Assert.assertEquals(2, group12Members.size());
Assert.assertEquals(2, groupTeam20162017Members.size());
Assert.assertEquals(2, groupTeam20182019Members.size());
// 4 - Delete some group mappings and check they are deleted
john.leaveGroup(group1);
john.leaveGroup(group12);
john.leaveGroup(groupTeam20162017);
john.leaveGroup(groupTeamChild20182019);
mary.leaveGroup(group1);
mary.leaveGroup(group12);
mary.leaveGroup(groupTeam20162017);
mary.leaveGroup(groupTeamChild20182019);
johnGroups = john.getGroups();
Assert.assertEquals(0, johnGroups.size());
@@ -96,7 +96,8 @@ public class LDAPSpecialCharsTest extends AbstractLDAPTest {
String descriptionAttrName = getGroupDescriptionLDAPAttrName(ctx.getLdapProvider());
LDAPObject groupSpecialCharacters = LDAPTestUtils.createLDAPGroup(session, appRealm, ctx.getLdapModel(), "group-spec,ia*l_characžter)s", descriptionAttrName, "group-special-characters");
LDAPTestUtils.createLDAPGroup(session, appRealm, ctx.getLdapModel(), "group-spec,ia*l_characžter)s", descriptionAttrName, "group-special-characters");
LDAPTestUtils.createLDAPGroup(session, appRealm, ctx.getLdapModel(), "group/with/three/slashes", descriptionAttrName, "group-with-three-slashes");
// Resync LDAP groups to Keycloak DB
ComponentModel mapperModel = LDAPTestUtils.getSubcomponentByName(appRealm, ctx.getLdapModel(), "groupsMapper");
@@ -170,10 +171,15 @@ public class LDAPSpecialCharsTest extends AbstractLDAPTest {
specialUser.joinGroup(specialGroup);
GroupModel groupWithSlashes = KeycloakModelUtils.findGroupByPath(appRealm, "/group/with/three/slashes");
Assert.assertNotNull(groupWithSlashes);
specialUser.joinGroup(groupWithSlashes);
// 2 - Check that group mappings are in LDAP and hence available through federation
Set<GroupModel> userGroups = specialUser.getGroups();
Assert.assertEquals(1, userGroups.size());
Assert.assertEquals(2, userGroups.size());
Assert.assertTrue(userGroups.contains(specialGroup));
// 3 - Check through userProvider
@@ -182,9 +188,15 @@ public class LDAPSpecialCharsTest extends AbstractLDAPTest {
Assert.assertEquals(1, groupMembers.size());
Assert.assertEquals("jamees,key*cložak)ppp", groupMembers.get(0).getUsername());
groupMembers = session.users().getGroupMembers(appRealm, groupWithSlashes, 0, 10);
Assert.assertEquals(1, groupMembers.size());
Assert.assertEquals("jamees,key*cložak)ppp", groupMembers.get(0).getUsername());
// 4 - Delete some group mappings and check they are deleted
specialUser.leaveGroup(specialGroup);
specialUser.leaveGroup(groupWithSlashes);
userGroups = specialUser.getGroups();
Assert.assertEquals(0, userGroups.size());
@@ -86,7 +86,7 @@ public class FallbackKeyProviderTest extends AbstractKeycloakTest {
Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
providers = realmsResouce().realm("test").components().query(realmId, "org.keycloak.keys.KeyProvider");
assertProviders(providers, "fallback-RS256", "fallback-HS256", "fallback-AES");
assertProviders(providers, "fallback-RS256", "fallback-HS256");
}
@Test
@@ -106,6 +106,7 @@ public abstract class AbstractMigrationTest extends AbstractKeycloakTest {
if (supportsAuthzService) {
expectedClientIds.add("authz-servlet");
expectedClientIds.add("client-with-template");
}
assertNames(migrationRealm.clients().findAll(), expectedClientIds.toArray(new String[expectedClientIds.size()]));
@@ -211,6 +212,7 @@ public abstract class AbstractMigrationTest extends AbstractKeycloakTest {
protected void testMigrationTo4_0_0() {
testRealmDefaultClientScopes(this.masterRealm);
testRealmDefaultClientScopes(this.migrationRealm);
testClientDefaultClientScopes(this.migrationRealm);
testOfflineScopeAddedToClient();
}
@@ -493,6 +495,11 @@ public abstract class AbstractMigrationTest extends AbstractKeycloakTest {
ExportImportUtil.testRealmDefaultClientScopes(realm);
}
private void testClientDefaultClientScopes(RealmResource realm) {
log.info("Testing default client scopes transferred from client scope in realm: " + realm.toRepresentation().getRealm());
ExportImportUtil.testClientDefaultClientScopes(realm);
}
private void testOfflineScopeAddedToClient() {
log.infof("Testing offline_access optional scope present in realm %s for client migration-test-client", migrationRealm.toRepresentation().getRealm());
@@ -25,7 +25,6 @@ import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.arquillian.DeploymentTargetModifier;
import org.keycloak.testsuite.runonserver.RunOnServerDeployment;
import org.keycloak.testsuite.utils.io.IOUtil;
import org.keycloak.testsuite.util.WaitUtils;
import org.keycloak.util.JsonSerialization;
import java.io.IOException;
@@ -67,6 +67,11 @@ public class ClientModelTest extends AbstractKeycloakTest {
"org.keycloak.testsuite.model");
}
@Override
protected boolean isImportAfterEachMethod() {
return true;
}
@Override
public void addTestRealms(List<RealmRepresentation> testRealms) {
RealmRepresentation realm = new RealmRepresentation();
@@ -301,6 +306,7 @@ public class ClientModelTest extends AbstractKeycloakTest {
currentSession.realms().removeClient(client.getId(), realm);
currentSession.realms().removeClient(copyClient.getId(), realm);
currentSession.realms().removeRealm(realm.getId());
});
}
@@ -63,6 +63,7 @@ import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.lessThan;
import static org.hamcrest.Matchers.lessThanOrEqualTo;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
import static org.keycloak.testsuite.admin.ApiUtil.findUserByUsername;
@@ -91,6 +92,12 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
RealmRepresentation realmRepresentation = loadJson(getClass().getResourceAsStream("/testrealm.json"), RealmRepresentation.class);
realmRepresentation.getClients().add(org.keycloak.testsuite.util.ClientBuilder.create()
.clientId("service-account-app")
.serviceAccount()
.secret("secret")
.build());
RealmBuilder realm = RealmBuilder.edit(realmRepresentation)
.testEventListener();
@@ -136,6 +143,7 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
@Test
public void refreshTokenRequest() throws Exception {
oauth.nonce("123456");
oauth.doLogin("test-user@localhost", "password");
EventRepresentation loginEvent = events.expectLogin().assertEvent();
@@ -147,12 +155,14 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
AccessToken token = oauth.verifyToken(tokenResponse.getAccessToken());
assertEquals("123456", token.getNonce());
String refreshTokenString = tokenResponse.getRefreshToken();
RefreshToken refreshToken = oauth.parseRefreshToken(refreshTokenString);
EventRepresentation tokenEvent = events.expectCodeToToken(codeId, sessionId).assertEvent();
Assert.assertNotNull(refreshTokenString);
assertNotNull(refreshTokenString);
assertEquals("bearer", tokenResponse.getTokenType());
@@ -200,6 +210,8 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
Assert.assertNotEquals(tokenEvent.getDetails().get(Details.TOKEN_ID), refreshEvent.getDetails().get(Details.TOKEN_ID));
Assert.assertNotEquals(tokenEvent.getDetails().get(Details.REFRESH_TOKEN_ID), refreshEvent.getDetails().get(Details.UPDATED_REFRESH_TOKEN_ID));
assertEquals("123456", refreshedToken.getNonce());
setTimeOffset(0);
}
@Test
@@ -849,6 +861,17 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
events.expectRefresh(refreshToken.getId(), sessionId).user(userId).clearDetails().error(Errors.INVALID_TOKEN).assertEvent();
}
@Test
public void refreshTokenServiceAccount() throws Exception {
OAuthClient.AccessTokenResponse response = oauth.clientId("service-account-app").doClientCredentialsGrantAccessTokenRequest("secret");
assertNotNull(response.getRefreshToken());
response = oauth.doRefreshTokenRequest(response.getRefreshToken(), "secret");
assertNotNull(response.getRefreshToken());
}
protected Response executeRefreshToken(WebTarget refreshTarget, String refreshToken) {
String header = BasicAuthHelper.createHeader("test-app", "password");
Form form = new Form();
@@ -903,7 +926,7 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
EventRepresentation tokenEvent = events.expectCodeToToken(codeId, sessionId).assertEvent();
Assert.assertNotNull(refreshTokenString);
assertNotNull(refreshTokenString);
assertEquals("bearer", tokenResponse.getTokenType());
@@ -1026,7 +1049,7 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
EventRepresentation tokenEvent = events.expectCodeToToken(codeId, sessionId).assertEvent();
Assert.assertNotNull(refreshTokenString);
assertNotNull(refreshTokenString);
assertEquals("bearer", tokenResponse.getTokenType());
@@ -0,0 +1,53 @@
/*
* Copyright 2018 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.testsuite.saml;
import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
import org.keycloak.saml.common.exceptions.ProcessingException;
import org.keycloak.saml.common.util.DocumentUtil;
import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request;
import org.keycloak.testsuite.util.SamlClient;
import org.keycloak.testsuite.util.SamlClient.Binding;
import org.keycloak.testsuite.util.SamlClientBuilder;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.util.EntityUtils;
import org.junit.Test;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertThat;
import static org.keycloak.testsuite.saml.AbstractSamlTest.REALM_NAME;
import static org.keycloak.testsuite.saml.AbstractSamlTest.SAML_ASSERTION_CONSUMER_URL_SALES_POST;
import static org.keycloak.testsuite.saml.AbstractSamlTest.SAML_CLIENT_ID_SALES_POST;
/**
*
* @author hmlnarik
*/
public class SamlRedirectBindingTest extends AbstractSamlTest {
@Test
public void testNoWhitespaceInLoginRequest() throws Exception {
AuthnRequestType authnRequest = SamlClient.createLoginRequestDocument(SAML_CLIENT_ID_SALES_POST, SAML_ASSERTION_CONSUMER_URL_SALES_POST, getAuthServerSamlEndpoint(REALM_NAME));
HttpUriRequest req = SamlClient.Binding.REDIRECT.createSamlUnsignedRequest(getAuthServerSamlEndpoint(REALM_NAME), null, SAML2Request.convert(authnRequest));
String url = req.getURI().getQuery();
assertThat(url, not(containsString(" ")));
assertThat(url, not(containsString("\n")));
assertThat(url, not(containsString("\r")));
assertThat(url, not(containsString("\t")));
}
}
@@ -2108,7 +2108,15 @@
}, {
"client" : "security-admin-console",
"roles" : [ "realm-admin" ]
} ]
} ],
"migration-test-client": [
{
"clientTemplate": "Default test template",
"roles": [
"migration-test-client-role"
]
}
]
},
"clients" : [ {
"id" : "6f27b0c3-9fc0-4e04-b69a-2031349acf04",
@@ -2802,6 +2810,33 @@
"useTemplateScope" : false,
"useTemplateMappers" : false
},
{
"id": "26519045-3cd4-4f2a-974b-e1447907834a",
"clientId": "client-with-template",
"surrogateAuthRequired": false,
"enabled": true,
"clientAuthenticatorType": "client-secret",
"secret": "**********",
"redirectUris": [],
"webOrigins": [],
"notBefore": 0,
"bearerOnly": false,
"consentRequired": false,
"standardFlowEnabled": true,
"implicitFlowEnabled": false,
"directAccessGrantsEnabled": true,
"serviceAccountsEnabled": false,
"publicClient": true,
"frontchannelLogout": false,
"protocol": "openid-connect",
"attributes": {},
"fullScopeAllowed": false,
"nodeReRegistrationTimeout": -1,
"clientTemplate": "Default test template",
"useTemplateConfig": false,
"useTemplateScope": true,
"useTemplateMappers": true
},
{
"id": "70e8e897-82d4-49ab-82c9-c37e1a48b6bb",
"clientId": "authz-servlet",
@@ -2858,7 +2893,15 @@
]
}
}],
"clientTemplates" : [ ],
"clientTemplates": [
{
"id": "d43ae38d-80a1-471a-9a80-5f9a0d34d7a4",
"name": "Default test template",
"description": "Test client template",
"protocol": "openid-connect",
"fullScopeAllowed": false
}
],
"browserSecurityHeaders" : {
"xContentTypeOptions" : "nosniff",
"xFrameOptions" : "SAMEORIGIN",
@@ -313,6 +313,16 @@
"notBefore" : 0,
"groups" : [ ]
} ],
"clientScopeMappings" : {
"migration-test-client": [
{
"clientTemplate": "Default test template",
"roles": [
"migration-test-client-role"
]
}
]
},
"clients" : [ {
"id" : "6b9ba4ca-fb7c-4e17-a3e6-88f3a17397cc",
"clientId" : "account",
@@ -1010,6 +1020,33 @@
"useTemplateScope" : false,
"useTemplateMappers" : false
},
{
"id": "26519045-3cd4-4f2a-974b-e1447907834a",
"clientId": "client-with-template",
"surrogateAuthRequired": false,
"enabled": true,
"clientAuthenticatorType": "client-secret",
"secret": "**********",
"redirectUris": [],
"webOrigins": [],
"notBefore": 0,
"bearerOnly": false,
"consentRequired": false,
"standardFlowEnabled": true,
"implicitFlowEnabled": false,
"directAccessGrantsEnabled": true,
"serviceAccountsEnabled": false,
"publicClient": true,
"frontchannelLogout": false,
"protocol": "openid-connect",
"attributes": {},
"fullScopeAllowed": false,
"nodeReRegistrationTimeout": -1,
"clientTemplate": "Default test template",
"useTemplateConfig": false,
"useTemplateScope": true,
"useTemplateMappers": true
},
{
"id": "70e8e897-82d4-49ab-82c9-c37e1a48b6bb",
"clientId": "authz-servlet",
@@ -1066,7 +1103,15 @@
]
}
}],
"clientTemplates" : [ ],
"clientTemplates": [
{
"id": "d43ae38d-80a1-471a-9a80-5f9a0d34d7a4",
"name": "Default test template",
"description": "Test client template",
"protocol": "openid-connect",
"fullScopeAllowed": false
}
],
"browserSecurityHeaders" : {
"xContentTypeOptions" : "nosniff",
"xRobotsTag" : "none",
@@ -179,7 +179,8 @@
"grantedRealmRoles": [ "offline_access" ],
"grantedClientRoles": {
"Application": [ "app-admin" ]
}
},
"grantedClientScopes" : [ "roles", "profile", "email", "account", "web-origins" ]
},
{
"clientId": "OtherApp",
@@ -239,6 +240,9 @@
"directAccessGrantsEnabled": false,
"serviceAccountsEnabled": true,
"clientAuthenticatorType": "client-jwt",
"authenticationFlowBindingOverrides": {
"browser": "73dcb1e4-2c7c-4494-825d-f2677cbc114c"
},
"protocolMappers" : [
{
"name" : "gss delegation credential",
@@ -534,7 +538,363 @@
}
]
}
},
"authenticationFlows": [
{
"id": "aed29d4f-aba7-4992-a600-18c0a28c1fc3",
"alias": "Handle Existing Account",
"description": "Handle what to do if there is existing account with same email/username like authenticated identity provider",
"providerId": "basic-flow",
"topLevel": false,
"builtIn": true,
"authenticationExecutions": [
{
"authenticator": "idp-confirm-link",
"requirement": "REQUIRED",
"priority": 10,
"userSetupAllowed": false,
"autheticatorFlow": false
},
{
"authenticator": "idp-email-verification",
"requirement": "ALTERNATIVE",
"priority": 20,
"userSetupAllowed": false,
"autheticatorFlow": false
},
{
"requirement": "ALTERNATIVE",
"priority": 30,
"flowAlias": "Verify Existing Account by Re-authentication",
"userSetupAllowed": false,
"autheticatorFlow": true
}
]
},
{
"id": "d8b8f564-6d56-4171-ba36-a8922c6eae49",
"alias": "Verify Existing Account by Re-authentication",
"description": "Reauthentication of existing account",
"providerId": "basic-flow",
"topLevel": false,
"builtIn": true,
"authenticationExecutions": [
{
"authenticator": "idp-username-password-form",
"requirement": "REQUIRED",
"priority": 10,
"userSetupAllowed": false,
"autheticatorFlow": false
},
{
"authenticator": "auth-otp-form",
"requirement": "OPTIONAL",
"priority": 20,
"userSetupAllowed": false,
"autheticatorFlow": false
}
]
},
{
"id": "73dcb1e4-2c7c-4494-825d-f2677cbc114c",
"alias": "browser",
"description": "browser based authentication",
"providerId": "basic-flow",
"topLevel": true,
"builtIn": true,
"authenticationExecutions": [
{
"authenticator": "auth-cookie",
"requirement": "ALTERNATIVE",
"priority": 10,
"userSetupAllowed": false,
"autheticatorFlow": false
},
{
"authenticator": "auth-spnego",
"requirement": "DISABLED",
"priority": 20,
"userSetupAllowed": false,
"autheticatorFlow": false
},
{
"authenticator": "identity-provider-redirector",
"requirement": "ALTERNATIVE",
"priority": 25,
"userSetupAllowed": false,
"autheticatorFlow": false
},
{
"requirement": "ALTERNATIVE",
"priority": 30,
"flowAlias": "forms",
"userSetupAllowed": false,
"autheticatorFlow": true
}
]
},
{
"id": "a0a80dc3-d473-468e-b6e8-f1d306c21360",
"alias": "clients",
"description": "Base authentication for clients",
"providerId": "client-flow",
"topLevel": true,
"builtIn": true,
"authenticationExecutions": [
{
"authenticator": "client-secret",
"requirement": "ALTERNATIVE",
"priority": 10,
"userSetupAllowed": false,
"autheticatorFlow": false
},
{
"authenticator": "client-jwt",
"requirement": "ALTERNATIVE",
"priority": 20,
"userSetupAllowed": false,
"autheticatorFlow": false
},
{
"authenticator": "client-secret-jwt",
"requirement": "ALTERNATIVE",
"priority": 30,
"userSetupAllowed": false,
"autheticatorFlow": false
}
]
},
{
"id": "91882f46-54be-4738-847a-32e849d53240",
"alias": "direct grant",
"description": "OpenID Connect Resource Owner Grant",
"providerId": "basic-flow",
"topLevel": true,
"builtIn": true,
"authenticationExecutions": [
{
"authenticator": "direct-grant-validate-username",
"requirement": "REQUIRED",
"priority": 10,
"userSetupAllowed": false,
"autheticatorFlow": false
},
{
"authenticator": "direct-grant-validate-password",
"requirement": "REQUIRED",
"priority": 20,
"userSetupAllowed": false,
"autheticatorFlow": false
},
{
"authenticator": "direct-grant-validate-otp",
"requirement": "OPTIONAL",
"priority": 30,
"userSetupAllowed": false,
"autheticatorFlow": false
}
]
},
{
"id": "b727a208-587c-4f27-8f48-ba2a0d4effdd",
"alias": "docker auth",
"description": "Used by Docker clients to authenticate against the IDP",
"providerId": "basic-flow",
"topLevel": true,
"builtIn": true,
"authenticationExecutions": [
{
"authenticator": "docker-http-basic-authenticator",
"requirement": "REQUIRED",
"priority": 10,
"userSetupAllowed": false,
"autheticatorFlow": false
}
]
},
{
"id": "5a6ac775-4000-4ccf-9271-6cb599297d4b",
"alias": "first broker login",
"description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account",
"providerId": "basic-flow",
"topLevel": true,
"builtIn": true,
"authenticationExecutions": [
{
"authenticatorConfig": "review profile config",
"authenticator": "idp-review-profile",
"requirement": "REQUIRED",
"priority": 10,
"userSetupAllowed": false,
"autheticatorFlow": false
},
{
"authenticatorConfig": "create unique user config",
"authenticator": "idp-create-user-if-unique",
"requirement": "ALTERNATIVE",
"priority": 20,
"userSetupAllowed": false,
"autheticatorFlow": false
},
{
"requirement": "ALTERNATIVE",
"priority": 30,
"flowAlias": "Handle Existing Account",
"userSetupAllowed": false,
"autheticatorFlow": true
}
]
},
{
"id": "1a84808d-e0c7-4759-aee8-cf9229542429",
"alias": "forms",
"description": "Username, password, otp and other auth forms.",
"providerId": "basic-flow",
"topLevel": false,
"builtIn": true,
"authenticationExecutions": [
{
"authenticator": "auth-username-password-form",
"requirement": "REQUIRED",
"priority": 10,
"userSetupAllowed": false,
"autheticatorFlow": false
},
{
"authenticator": "auth-otp-form",
"requirement": "OPTIONAL",
"priority": 20,
"userSetupAllowed": false,
"autheticatorFlow": false
}
]
},
{
"id": "717f990a-1c46-464c-9051-5e0ae39d63db",
"alias": "registration",
"description": "registration flow",
"providerId": "basic-flow",
"topLevel": true,
"builtIn": true,
"authenticationExecutions": [
{
"authenticator": "registration-page-form",
"requirement": "REQUIRED",
"priority": 10,
"flowAlias": "registration form",
"userSetupAllowed": false,
"autheticatorFlow": true
}
]
},
{
"id": "166fca50-7b69-4cd4-80eb-a569e87ff8a2",
"alias": "registration form",
"description": "registration form",
"providerId": "form-flow",
"topLevel": false,
"builtIn": true,
"authenticationExecutions": [
{
"authenticator": "registration-user-creation",
"requirement": "REQUIRED",
"priority": 20,
"userSetupAllowed": false,
"autheticatorFlow": false
},
{
"authenticator": "registration-profile-action",
"requirement": "REQUIRED",
"priority": 40,
"userSetupAllowed": false,
"autheticatorFlow": false
},
{
"authenticator": "registration-password-action",
"requirement": "REQUIRED",
"priority": 50,
"userSetupAllowed": false,
"autheticatorFlow": false
},
{
"authenticator": "registration-recaptcha-action",
"requirement": "DISABLED",
"priority": 60,
"userSetupAllowed": false,
"autheticatorFlow": false
}
]
},
{
"id": "a516cb39-8f6d-4d08-ac82-236377be6500",
"alias": "reset credentials",
"description": "Reset credentials for a user if they forgot their password or something",
"providerId": "basic-flow",
"topLevel": true,
"builtIn": true,
"authenticationExecutions": [
{
"authenticator": "reset-credentials-choose-user",
"requirement": "REQUIRED",
"priority": 10,
"userSetupAllowed": false,
"autheticatorFlow": false
},
{
"authenticator": "reset-credential-email",
"requirement": "REQUIRED",
"priority": 20,
"userSetupAllowed": false,
"autheticatorFlow": false
},
{
"authenticator": "reset-password",
"requirement": "REQUIRED",
"priority": 30,
"userSetupAllowed": false,
"autheticatorFlow": false
},
{
"authenticator": "reset-otp",
"requirement": "OPTIONAL",
"priority": 40,
"userSetupAllowed": false,
"autheticatorFlow": false
}
]
},
{
"id": "8b9ae730-11e0-451f-b693-e32f09415e42",
"alias": "saml ecp",
"description": "SAML ECP Profile Authentication Flow",
"providerId": "basic-flow",
"topLevel": true,
"builtIn": true,
"authenticationExecutions": [
{
"authenticator": "http-basic-authenticator",
"requirement": "REQUIRED",
"priority": 10,
"userSetupAllowed": false,
"autheticatorFlow": false
}
]
}
],
"authenticatorConfig": [
{
"id": "a6d38dcd-7b53-4991-b4eb-c866ce3c5e70",
"alias": "create unique user config",
"config": {
"require.password.update.after.registration": "false"
}
},
{
"id": "7408f503-b929-422f-b52b-277cebda44ba",
"alias": "review profile config",
"config": {
"update.profile.on.first.login": "missing"
}
}
]
}
@@ -190,7 +190,7 @@ public class SSSDTest extends AbstractKeycloakTest {
profilePage.updateProfile("New first", "New last", "new@email.com");
Assert.assertEquals("You can't update your account as it is read only.", profilePage.getError());
Assert.assertEquals("You can't update your account as it is read-only.", profilePage.getError());
}
@Test
@@ -122,7 +122,7 @@
if (loadListener) script.addEventListener("load", loadListener);
document.head.appendChild(script);
};
keycloak.init({onLoad: 'login-required'}).success(function(authenticated) {
keycloak.init({onLoad: 'check-sso'}).success(function(authenticated) {
loadjs("/node_modules/react/umd/react.development.js", function() {
loadjs("/node_modules/react-dom/umd/react-dom.development.js", function() {
loadjs("/node_modules/systemjs/dist/system.src.js", function() {
@@ -20,6 +20,7 @@ import {Route, Link} from 'react-router-dom';
import {KeycloakService} from './keycloak-service/keycloak.service';
import {Logout} from './widgets/Logout';
import {Msg} from './widgets/Msg';
import {AccountPage} from './content/account-page/AccountPage';
import {ApplicationsPage} from './content/applications-page/ApplicationsPage';
import {PasswordPage} from './content/password-page/PasswordPage';
@@ -50,9 +51,9 @@ export class App extends React.Component<AppProps> {
return (
<span>
<nav>
<Link to="/app/account" className="btn btn-primary btn-lg btn-sign" type="button">Account</Link>
<Link to="/app/applications" className="btn btn-primary btn-lg btn-sign" type="button">Applications</Link>
<Link to="/app/password" className="btn btn-primary btn-lg btn-sign" type="button">Password</Link>
<Link to="/app/account" className="btn btn-primary btn-lg btn-sign" type="button"><Msg msgKey="account"/></Link>
<Link to="/app/applications" className="btn btn-primary btn-lg btn-sign" type="button"><Msg msgKey="applications"/></Link>
<Link to="/app/password" className="btn btn-primary btn-lg btn-sign" type="button"><Msg msgKey="password"/></Link>
{ExtensionPages.Links}
<Logout/>
<Route path='/app/account' component={AccountPage}/>
@@ -16,7 +16,9 @@
import * as React from 'react';
import {AxiosResponse} from 'axios';
import {AccountServiceClient} from '../../account-service/account.service';
import {Msg} from '../../widgets/Msg';
interface AccountPageProps {
}
@@ -77,11 +79,10 @@ export class AccountPage extends React.Component<AccountPageProps, AccountPageSt
});
}
private makeTextInput(label: string,
name: string,
private makeTextInput(name: string,
disabled = false): React.ReactElement<any> {
return (
<label>{label}
<label><Msg msgKey={name}/>:
<input disabled={disabled} type="text" name={name} value={this.state[name]} onChange={this.handleChange} />
</label>
);
@@ -90,17 +91,17 @@ export class AccountPage extends React.Component<AccountPageProps, AccountPageSt
render() {
return (
<form onSubmit={this.handleSubmit}>
{this.makeTextInput('User Name:', 'username', true)}
{this.makeTextInput('username', true)}
<br/>
{this.makeTextInput('First Name:', 'firstName')}
{this.makeTextInput('firstName')}
<br/>
{this.makeTextInput('Last Name:', 'lastName')}
{this.makeTextInput('lastName')}
<br/>
{this.makeTextInput('Email:', 'email')}
{this.makeTextInput('email')}
<br/>
<button className="btn btn-primary btn-lg btn-sign"
disabled={!this.state.changed}
value="Submit">Submit</button>
value="Submit"><Msg msgKey="doSave"/></button>
</form>
);
}
@@ -16,6 +16,8 @@
import * as React from 'react';
import {Link} from 'react-router-dom';
import {Msg} from './Msg';
import {KeycloakService} from '../keycloak-service/keycloak.service';
declare const baseUrl;
@@ -35,7 +37,7 @@ export class Logout extends React.Component<LogoutProps> {
render() {
return (
<Link to="/" className="btn btn-primary btn-lg btn-sign" type="button" onClick={this.handleLogout}>Logout</Link>
<Link to="/" className="btn btn-primary btn-lg btn-sign" type="button" onClick={this.handleLogout}><Msg msgKey="doSignOut"/></Link>
);
}
}
@@ -0,0 +1,46 @@
/*
* Copyright 2018 Red Hat, Inc. and/or its affiliates.
*
* 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.
*/
import * as React from 'react';
declare const l18n_msg: {[key:string]: string};
export interface MsgProps {
readonly msgKey:string;
readonly params?:Array<string>;
}
export class Msg extends React.Component<MsgProps> {
constructor(props: MsgProps) {
super(props);
}
render() {
let message:string = l18n_msg[this.props.msgKey];
if (message === undefined) message = this.props.msgKey;
if (this.props.params !== undefined) {
this.props.params.forEach((value: string, index: number) => {
message = message.replace('{{param_'+ index + '}}', value);
})
}
return (
<span>{message}</span>
);
}
}