Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bfafe65814 | ||
|
|
1b8dc04459 | ||
|
|
4355c89b9d | ||
|
|
1b37394276 | ||
|
|
ded82fff3d | ||
|
|
4fbbaf18aa | ||
|
|
5ad929b9ee | ||
|
|
2b9b1ba45f | ||
|
|
8b122de425 | ||
|
|
7d75377813 | ||
|
|
3c8dddf33e | ||
|
|
311e848460 | ||
|
|
607bb1b995 | ||
|
|
91637120ee | ||
|
|
d90a5d1367 | ||
|
|
d395043fc7 | ||
|
|
c9cd060417 | ||
|
|
abd6e560ac | ||
|
|
6e93ca36af | ||
|
|
2d727fc54c | ||
|
|
6db1f60e27 | ||
|
|
8af1ca8fc3 |
No files matched your search
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+9
-8
@@ -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();
|
||||
|
||||
|
||||
+25
-9
@@ -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() {
|
||||
|
||||
|
||||
+187
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+64
-26
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+68
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+140
@@ -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>
|
||||
|
||||
+1
-1
@@ -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);
|
||||
}
|
||||
+61
-11
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+5
-1
@@ -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();
|
||||
|
||||
+20
-6
@@ -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
|
||||
|
||||
|
||||
|
||||
+17
@@ -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>
|
||||
|
||||
+15
@@ -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");
|
||||
|
||||
+28
@@ -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": []
|
||||
|
||||
+8
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+2
-1
@@ -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();
|
||||
|
||||
+56
-9
@@ -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()
|
||||
|
||||
+4
@@ -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) {
|
||||
|
||||
+35
@@ -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);
|
||||
}
|
||||
|
||||
+3
@@ -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) {
|
||||
|
||||
+13
@@ -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();
|
||||
|
||||
+18
-1
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
+3
@@ -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();
|
||||
|
||||
|
||||
+3
-1
@@ -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
|
||||
|
||||
+62
-25
@@ -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
|
||||
));
|
||||
}
|
||||
}
|
||||
+34
-2
@@ -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());
|
||||
|
||||
+14
-2
@@ -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());
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+7
@@ -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());
|
||||
|
||||
|
||||
-1
@@ -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;
|
||||
|
||||
+6
@@ -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());
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+26
-3
@@ -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());
|
||||
|
||||
|
||||
+53
@@ -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")));
|
||||
}
|
||||
}
|
||||
+45
-2
@@ -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",
|
||||
|
||||
+46
-1
@@ -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",
|
||||
|
||||
+364
-4
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+1
-1
@@ -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}/>
|
||||
|
||||
+9
-8
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
+3
-1
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
+46
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user