keycloak-memoizeit
Changes
model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoKeycloakSessionFactory.java 4(+3 -1)
model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserSessionAdapter.java 77(+77 -0)
model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoUserSessionEntity.java 58(+58 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java 17(+10 -7)
testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionMultipleActionsTest.java 30(+20 -10)
testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionResetPasswordTest.java 7(+4 -3)
testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionTotpSetupTest.java 23(+12 -11)
testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java 6(+3 -3)
testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/RelativeUriAdapterTest.java 4(+3 -1)
Details
diff --git a/audit/api/src/main/java/org/keycloak/audit/Audit.java b/audit/api/src/main/java/org/keycloak/audit/Audit.java
index 4cbd4f7..1ccdca8 100644
--- a/audit/api/src/main/java/org/keycloak/audit/Audit.java
+++ b/audit/api/src/main/java/org/keycloak/audit/Audit.java
@@ -4,6 +4,7 @@ import org.jboss.logging.Logger;
import org.keycloak.models.ClientModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
+import org.keycloak.models.UserSessionModel;
import org.keycloak.provider.ProviderFactoryLoader;
import java.util.HashMap;
@@ -61,6 +62,16 @@ public class Audit {
return this;
}
+ public Audit session(UserSessionModel session) {
+ event.setSessionId(session.getId());
+ return this;
+ }
+
+ public Audit session(String sessionId) {
+ event.setSessionId(sessionId);
+ return this;
+ }
+
public Audit ipAddress(String ipAddress) {
event.setIpAddress(ipAddress);
return this;
diff --git a/audit/api/src/main/java/org/keycloak/audit/Errors.java b/audit/api/src/main/java/org/keycloak/audit/Errors.java
index 1ab7d4a..4b0d41d 100755
--- a/audit/api/src/main/java/org/keycloak/audit/Errors.java
+++ b/audit/api/src/main/java/org/keycloak/audit/Errors.java
@@ -34,4 +34,8 @@ public interface Errors {
String SOCIAL_PROVIDER_NOT_FOUND = "social_provider_not_found";
String SOCIAL_ID_IN_USE = "social_id_in_use";
+ String USER_NOT_LOGGED_IN = "user_not_logged_in";
+ String USER_SESSION_NOT_FOUND = "user_session_not_found";
+
+
}
diff --git a/audit/api/src/main/java/org/keycloak/audit/Event.java b/audit/api/src/main/java/org/keycloak/audit/Event.java
index 6ab7400..8141acb 100644
--- a/audit/api/src/main/java/org/keycloak/audit/Event.java
+++ b/audit/api/src/main/java/org/keycloak/audit/Event.java
@@ -18,6 +18,8 @@ public class Event {
private String userId;
+ private String sessionId;
+
private String ipAddress;
private String error;
@@ -64,6 +66,14 @@ public class Event {
this.userId = userId;
}
+ public String getSessionId() {
+ return sessionId;
+ }
+
+ public void setSessionId(String sessionId) {
+ this.sessionId = sessionId;
+ }
+
public String getIpAddress() {
return ipAddress;
}
@@ -95,6 +105,7 @@ public class Event {
clone.realmId = realmId;
clone.clientId = clientId;
clone.userId = userId;
+ clone.sessionId = sessionId;
clone.ipAddress = ipAddress;
clone.error = error;
clone.details = details != null ? new HashMap<String, String>(details) : null;
diff --git a/audit/jpa/src/main/java/org/keycloak/audit/jpa/EventEntity.java b/audit/jpa/src/main/java/org/keycloak/audit/jpa/EventEntity.java
index 1227b70..f191813 100644
--- a/audit/jpa/src/main/java/org/keycloak/audit/jpa/EventEntity.java
+++ b/audit/jpa/src/main/java/org/keycloak/audit/jpa/EventEntity.java
@@ -23,6 +23,8 @@ public class EventEntity {
private String userId;
+ private String sessionId;
+
private String ipAddress;
private String error;
@@ -78,6 +80,14 @@ public class EventEntity {
this.userId = userId;
}
+ public String getSessionId() {
+ return sessionId;
+ }
+
+ public void setSessionId(String sessionId) {
+ this.sessionId = sessionId;
+ }
+
public String getIpAddress() {
return ipAddress;
}
diff --git a/audit/jpa/src/main/java/org/keycloak/audit/jpa/JpaAuditProvider.java b/audit/jpa/src/main/java/org/keycloak/audit/jpa/JpaAuditProvider.java
index 5401902..0640086 100644
--- a/audit/jpa/src/main/java/org/keycloak/audit/jpa/JpaAuditProvider.java
+++ b/audit/jpa/src/main/java/org/keycloak/audit/jpa/JpaAuditProvider.java
@@ -83,6 +83,7 @@ public class JpaAuditProvider implements AuditProvider {
e.setRealmId(o.getRealmId());
e.setClientId(o.getClientId());
e.setUserId(o.getUserId());
+ e.setSessionId(o.getSessionId());
e.setIpAddress(o.getIpAddress());
e.setError(o.getError());
try {
@@ -100,6 +101,7 @@ public class JpaAuditProvider implements AuditProvider {
e.setRealmId(o.getRealmId());
e.setClientId(o.getClientId());
e.setUserId(o.getUserId());
+ e.setSessionId(o.getSessionId());
e.setIpAddress(o.getIpAddress());
e.setError(o.getError());
try {
diff --git a/audit/mongo/src/main/java/org/keycloak/audit/mongo/MongoAuditProvider.java b/audit/mongo/src/main/java/org/keycloak/audit/mongo/MongoAuditProvider.java
index 77fbb79..3114628 100644
--- a/audit/mongo/src/main/java/org/keycloak/audit/mongo/MongoAuditProvider.java
+++ b/audit/mongo/src/main/java/org/keycloak/audit/mongo/MongoAuditProvider.java
@@ -60,6 +60,7 @@ public class MongoAuditProvider implements AuditProvider {
e.put("realmId", o.getRealmId());
e.put("clientId", o.getClientId());
e.put("userId", o.getUserId());
+ e.put("sessionId", o.getSessionId());
e.put("ipAddress", o.getIpAddress());
e.put("error", o.getError());
@@ -81,6 +82,7 @@ public class MongoAuditProvider implements AuditProvider {
e.setRealmId(o.getString("realmId"));
e.setClientId(o.getString("clientId"));
e.setUserId(o.getString("userId"));
+ e.setSessionId(o.getString("sessionId"));
e.setIpAddress(o.getString("ipAddress"));
e.setError(o.getString("error"));
diff --git a/bundled-war-example/src/main/resources/META-INF/persistence.xml b/bundled-war-example/src/main/resources/META-INF/persistence.xml
index 3eeed1f..616f8e8 100755
--- a/bundled-war-example/src/main/resources/META-INF/persistence.xml
+++ b/bundled-war-example/src/main/resources/META-INF/persistence.xml
@@ -15,6 +15,7 @@
<class>org.keycloak.models.jpa.entities.SocialLinkEntity</class>
<class>org.keycloak.models.jpa.entities.AuthenticationLinkEntity</class>
<class>org.keycloak.models.jpa.entities.UserEntity</class>
+ <class>org.keycloak.models.jpa.entities.UserSessionEntity</class>
<class>org.keycloak.models.jpa.entities.UsernameLoginFailureEntity</class>
<class>org.keycloak.models.jpa.entities.UserRoleMappingEntity</class>
<class>org.keycloak.models.jpa.entities.ScopeMappingEntity</class>
diff --git a/core/src/main/java/org/keycloak/representations/adapters/action/LogoutAction.java b/core/src/main/java/org/keycloak/representations/adapters/action/LogoutAction.java
index 6b73234..b89b10d 100755
--- a/core/src/main/java/org/keycloak/representations/adapters/action/LogoutAction.java
+++ b/core/src/main/java/org/keycloak/representations/adapters/action/LogoutAction.java
@@ -7,14 +7,16 @@ package org.keycloak.representations.adapters.action;
public class LogoutAction extends AdminAction {
public static final String LOGOUT = "LOGOUT";
protected String user;
+ private String session;
protected int notBefore;
public LogoutAction() {
}
- public LogoutAction(String id, int expiration, String resource, String user, int notBefore) {
+ public LogoutAction(String id, int expiration, String resource, String user, String session, int notBefore) {
super(id, expiration, resource, LOGOUT);
this.user = user;
+ this.session = session;
this.notBefore = notBefore;
}
@@ -26,6 +28,14 @@ public class LogoutAction extends AdminAction {
this.user = user;
}
+ public String getSession() {
+ return session;
+ }
+
+ public void setSession(String session) {
+ this.session = session;
+ }
+
public int getNotBefore() {
return notBefore;
}
diff --git a/core/src/main/java/org/keycloak/representations/IDToken.java b/core/src/main/java/org/keycloak/representations/IDToken.java
index 08514a9..975da61 100755
--- a/core/src/main/java/org/keycloak/representations/IDToken.java
+++ b/core/src/main/java/org/keycloak/representations/IDToken.java
@@ -88,6 +88,9 @@ public class IDToken extends JsonWebToken {
@JsonProperty("claims_locales")
protected String claimsLocales;
+ @JsonProperty("session_state")
+ protected String sessionState;
+
public String getNonce() {
return nonce;
}
@@ -303,4 +306,12 @@ public class IDToken extends JsonWebToken {
public void setClaimsLocales(String claimsLocales) {
this.claimsLocales = claimsLocales;
}
+
+ public String getSessionState() {
+ return sessionState;
+ }
+
+ public void setSessionState(String sessionState) {
+ this.sessionState = sessionState;
+ }
}
diff --git a/core/src/main/java/org/keycloak/representations/RefreshToken.java b/core/src/main/java/org/keycloak/representations/RefreshToken.java
index 7d90931..b536a1c 100755
--- a/core/src/main/java/org/keycloak/representations/RefreshToken.java
+++ b/core/src/main/java/org/keycloak/representations/RefreshToken.java
@@ -13,7 +13,7 @@ public class RefreshToken extends AccessToken {
}
/**
- * Deep copies issuer, subject, issuedFor, realmAccess, and resourceAccess
+ * Deep copies issuer, subject, issuedFor, sessionState, realmAccess, and resourceAccess
* from AccessToken.
*
* @param token
@@ -23,6 +23,7 @@ public class RefreshToken extends AccessToken {
this.issuer = token.issuer;
this.subject = token.subject;
this.issuedFor = token.issuedFor;
+ this.sessionState = token.sessionState;
if (token.realmAccess != null) {
realmAccess = token.realmAccess.clone();
}
diff --git a/model/api/src/main/java/org/keycloak/models/Config.java b/model/api/src/main/java/org/keycloak/models/Config.java
index e50ed9a..f0416d1 100644
--- a/model/api/src/main/java/org/keycloak/models/Config.java
+++ b/model/api/src/main/java/org/keycloak/models/Config.java
@@ -13,9 +13,12 @@ public class Config {
public static final String MODEL_PROVIDER_KEY = "keycloak.model";
+ public static final String USER_EXPIRATION_SCHEDULE_KEY = "keycloak.scheduled.clearExpiredUserSessions";
+ public static final String USER_EXPIRATION_SCHEDULE_DEFAULT = String.valueOf(TimeUnit.MINUTES.toMillis(15));
+
public static final String AUDIT_PROVIDER_KEY = "keycloak.audit";
public static final String AUDIT_PROVIDER_DEFAULT = "jpa";
- public static final String AUDIT_EXPIRATION_SCHEDULE_KEY = "keycloak.audit.expirationSchedule";
+ public static final String AUDIT_EXPIRATION_SCHEDULE_KEY = "keycloak.scheduled.clearExpiredAuditEvents";
public static final String AUDIT_EXPIRATION_SCHEDULE_DEFAULT = String.valueOf(TimeUnit.MINUTES.toMillis(15));
public static final String PICKETLINK_PROVIDER_KEY = "keycloak.picketlink";
@@ -67,6 +70,14 @@ public class Config {
System.setProperty(AUDIT_EXPIRATION_SCHEDULE_KEY, schedule);
}
+ public static String getUserExpirationSchedule() {
+ return System.getProperty(USER_EXPIRATION_SCHEDULE_KEY, USER_EXPIRATION_SCHEDULE_DEFAULT);
+ }
+
+ public static void setUserExpirationSchedule(String schedule) {
+ System.setProperty(USER_EXPIRATION_SCHEDULE_KEY, schedule);
+ }
+
public static String getModelProvider() {
return System.getProperty(MODEL_PROVIDER_KEY);
}
@@ -168,4 +179,5 @@ public class Config {
public static void setExportImportZipPassword(String exportImportZipPassword) {
System.setProperty(EXPORT_IMPORT_ZIP_PASSWORD, exportImportZipPassword);
}
+
}
diff --git a/model/api/src/main/java/org/keycloak/models/RealmModel.java b/model/api/src/main/java/org/keycloak/models/RealmModel.java
index 1c73b2f..fee50d3 100755
--- a/model/api/src/main/java/org/keycloak/models/RealmModel.java
+++ b/model/api/src/main/java/org/keycloak/models/RealmModel.java
@@ -249,4 +249,14 @@ public interface RealmModel extends RoleContainerModel, RoleMapperModel, ScopeMa
void setAdminApp(ApplicationModel app);
+ UserSessionModel createUserSession(UserModel user, String ipAddress);
+
+ UserSessionModel getUserSession(String id);
+
+ void removeUserSession(UserSessionModel session);
+
+ void removeUserSessions(UserModel user);
+
+ void removeExpiredUserSessions();
+
}
diff --git a/model/api/src/main/java/org/keycloak/models/UserSessionModel.java b/model/api/src/main/java/org/keycloak/models/UserSessionModel.java
new file mode 100644
index 0000000..33c33fc
--- /dev/null
+++ b/model/api/src/main/java/org/keycloak/models/UserSessionModel.java
@@ -0,0 +1,28 @@
+package org.keycloak.models;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public interface UserSessionModel {
+
+ String getId();
+
+ void setId(String id);
+
+ UserModel getUser();
+
+ void setUser(UserModel user);
+
+ String getIpAddress();
+
+ void setIpAddress(String ipAddress);
+
+ int getStarted();
+
+ void setStarted(int started);
+
+ int getExpires();
+
+ void setExpires(int expires);
+
+}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserSessionEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserSessionEntity.java
new file mode 100644
index 0000000..5d4fde7
--- /dev/null
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserSessionEntity.java
@@ -0,0 +1,77 @@
+package org.keycloak.models.jpa.entities;
+
+import org.hibernate.annotations.GenericGenerator;
+import org.keycloak.models.UserModel;
+
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.Id;
+import javax.persistence.ManyToOne;
+import javax.persistence.NamedQueries;
+import javax.persistence.NamedQuery;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+@Entity
+@NamedQueries({
+ @NamedQuery(name = "removeUserSessionByUser", query = "delete from UserSessionEntity s where s.user = :user"),
+ @NamedQuery(name = "removeUserSessionExpired", query = "delete from UserSessionEntity s where s.expires < :currentTime")
+})
+public class UserSessionEntity {
+
+ @Id
+ @GenericGenerator(name="uuid_generator", strategy="org.keycloak.models.jpa.utils.JpaIdGenerator")
+ @GeneratedValue(generator = "uuid_generator")
+ private String id;
+
+ @ManyToOne
+ private UserEntity user;
+
+ String ipAddress;
+
+ int started;
+
+ int expires;
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public UserEntity getUser() {
+ return user;
+ }
+
+ public void setUser(UserEntity user) {
+ this.user = user;
+ }
+
+ public String getIpAddress() {
+ return ipAddress;
+ }
+
+ public void setIpAddress(String ipAddress) {
+ this.ipAddress = ipAddress;
+ }
+
+ public int getStarted() {
+ return started;
+ }
+
+ public void setStarted(int started) {
+ this.started = started;
+ }
+
+ public int getExpires() {
+ return expires;
+ }
+
+ public void setExpires(int expires) {
+ this.expires = expires;
+ }
+
+}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaKeycloakSession.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaKeycloakSession.java
index d54cf3c..5614053 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaKeycloakSession.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaKeycloakSession.java
@@ -88,7 +88,7 @@ public class JpaKeycloakSession implements KeycloakSession {
adapter.removeOAuthClient(oauth.getId());
}
- for (UserEntity u : em.createQuery("from UserEntity", UserEntity.class).getResultList()) {
+ for (UserEntity u : em.createQuery("from UserEntity u where u.realm = :realm", UserEntity.class).setParameter("realm", realm).getResultList()) {
adapter.removeUser(u.getLoginName());
}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
index 4b6758d..a243d85 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
@@ -5,6 +5,7 @@ import org.keycloak.models.AuthenticationProviderModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.RoleContainerModel;
import org.keycloak.models.UserCredentialValueModel;
+import org.keycloak.models.UserSessionModel;
import org.keycloak.models.UsernameLoginFailureModel;
import org.keycloak.models.jpa.entities.ApplicationEntity;
import org.keycloak.models.jpa.entities.ApplicationRoleEntity;
@@ -19,6 +20,7 @@ import org.keycloak.models.jpa.entities.RoleEntity;
import org.keycloak.models.jpa.entities.ScopeMappingEntity;
import org.keycloak.models.jpa.entities.SocialLinkEntity;
import org.keycloak.models.jpa.entities.UserEntity;
+import org.keycloak.models.jpa.entities.UserSessionEntity;
import org.keycloak.models.jpa.entities.UserRoleMappingEntity;
import org.keycloak.models.jpa.entities.UsernameLoginFailureEntity;
import org.keycloak.models.utils.KeycloakModelUtils;
@@ -33,6 +35,7 @@ import org.keycloak.models.SocialLinkModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.TimeBasedOTP;
+import org.keycloak.util.Time;
import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
@@ -49,6 +52,7 @@ import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
+
import static org.keycloak.models.utils.Pbkdf2PasswordEncoder.*;
/**
@@ -452,7 +456,7 @@ public class RealmAdapter implements RealmModel {
query.setParameter("email", email);
query.setParameter("realm", realm);
List<UserEntity> results = query.getResultList();
- return results.isEmpty()? null : new UserAdapter(results.get(0));
+ return results.isEmpty() ? null : new UserAdapter(results.get(0));
}
@Override
@@ -504,6 +508,8 @@ public class RealmAdapter implements RealmModel {
}
private void removeUser(UserEntity user) {
+ removeUserSessions(user);
+
em.createQuery("delete from " + UserRoleMappingEntity.class.getSimpleName() + " where user = :user").setParameter("user", user).executeUpdate();
em.createQuery("delete from " + SocialLinkEntity.class.getSimpleName() + " where user = :user").setParameter("user", user).executeUpdate();
if (user.getAuthenticationLink() != null) {
@@ -719,7 +725,7 @@ public class RealmAdapter implements RealmModel {
em.remove(entity);
em.flush();
return true;
- } else {
+ } else {
return false;
}
}
@@ -848,7 +854,7 @@ public class RealmAdapter implements RealmModel {
public boolean removeOAuthClient(String id) {
OAuthClientModel oauth = getOAuthClientById(id);
if (oauth == null) return false;
- OAuthClientEntity client = (OAuthClientEntity)((OAuthClientAdapter)oauth).getEntity();
+ OAuthClientEntity client = (OAuthClientEntity) ((OAuthClientAdapter) oauth).getEntity();
em.createQuery("delete from " + ScopeMappingEntity.class.getSimpleName() + " where client = :client").setParameter("client", client).executeUpdate();
em.remove(client);
return true;
@@ -1001,7 +1007,7 @@ public class RealmAdapter implements RealmModel {
}
if (!role.getContainer().equals(this)) return false;
- RoleEntity roleEntity = ((RoleAdapter)role).getRole();
+ RoleEntity roleEntity = ((RoleAdapter) role).getRole();
realm.getRoles().remove(role);
realm.getDefaultRoles().remove(role);
@@ -1030,10 +1036,10 @@ public class RealmAdapter implements RealmModel {
RoleEntity entity = em.find(RoleEntity.class, id);
if (entity == null) return null;
if (entity instanceof RealmRoleEntity) {
- RealmRoleEntity roleEntity = (RealmRoleEntity)entity;
+ RealmRoleEntity roleEntity = (RealmRoleEntity) entity;
if (!roleEntity.getRealm().getId().equals(getId())) return null;
} else {
- ApplicationRoleEntity roleEntity = (ApplicationRoleEntity)entity;
+ ApplicationRoleEntity roleEntity = (ApplicationRoleEntity) entity;
if (!roleEntity.getApplication().getRealm().getId().equals(getId())) return null;
}
return new RoleAdapter(this, em, entity);
@@ -1069,7 +1075,6 @@ public class RealmAdapter implements RealmModel {
}
-
protected TypedQuery<UserRoleMappingEntity> getUserRoleMappingEntityTypedQuery(UserAdapter user, RoleAdapter role) {
TypedQuery<UserRoleMappingEntity> query = em.createNamedQuery("userHasRole", UserRoleMappingEntity.class);
query.setParameter("user", user.getUser());
@@ -1082,7 +1087,7 @@ public class RealmAdapter implements RealmModel {
if (hasRole(user, role)) return;
UserRoleMappingEntity entity = new UserRoleMappingEntity();
entity.setUser(((UserAdapter) user).getUser());
- entity.setRole(((RoleAdapter)role).getRole());
+ entity.setRole(((RoleAdapter) role).getRole());
em.persist(entity);
em.flush();
}
@@ -1095,7 +1100,7 @@ public class RealmAdapter implements RealmModel {
for (RoleModel role : roleMappings) {
RoleContainerModel container = role.getContainer();
if (container instanceof RealmModel) {
- realmRoles.add(role);
+ realmRoles.add(role);
}
}
return realmRoles;
@@ -1105,7 +1110,7 @@ public class RealmAdapter implements RealmModel {
@Override
public Set<RoleModel> getRoleMappings(UserModel user) {
TypedQuery<UserRoleMappingEntity> query = em.createNamedQuery("userRoleMappings", UserRoleMappingEntity.class);
- query.setParameter("user", ((UserAdapter)user).getUser());
+ query.setParameter("user", ((UserAdapter) user).getUser());
List<UserRoleMappingEntity> entities = query.getResultList();
Set<RoleModel> roles = new HashSet<RoleModel>();
for (UserRoleMappingEntity entity : entities) {
@@ -1135,7 +1140,7 @@ public class RealmAdapter implements RealmModel {
for (RoleModel role : roleMappings) {
RoleContainerModel container = role.getContainer();
if (container instanceof RealmModel) {
- if (((RealmModel)container).getId().equals(getId())) {
+ if (((RealmModel) container).getId().equals(getId())) {
appRoles.add(role);
}
}
@@ -1148,7 +1153,7 @@ public class RealmAdapter implements RealmModel {
@Override
public Set<RoleModel> getScopeMappings(ClientModel client) {
TypedQuery<ScopeMappingEntity> query = em.createNamedQuery("clientScopeMappings", ScopeMappingEntity.class);
- query.setParameter("client", ((ClientAdapter)client).getEntity());
+ query.setParameter("client", ((ClientAdapter) client).getEntity());
List<ScopeMappingEntity> entities = query.getResultList();
Set<RoleModel> roles = new HashSet<RoleModel>();
for (ScopeMappingEntity entity : entities) {
@@ -1162,7 +1167,7 @@ public class RealmAdapter implements RealmModel {
if (hasScope(client, role)) return;
ScopeMappingEntity entity = new ScopeMappingEntity();
entity.setClient(((ClientAdapter) client).getEntity());
- entity.setRole(((RoleAdapter)role).getRole());
+ entity.setRole(((RoleAdapter) role).getRole());
em.persist(entity);
}
@@ -1179,13 +1184,13 @@ public class RealmAdapter implements RealmModel {
protected TypedQuery<ScopeMappingEntity> getRealmScopeMappingQuery(ClientAdapter client, RoleAdapter role) {
TypedQuery<ScopeMappingEntity> query = em.createNamedQuery("hasScope", ScopeMappingEntity.class);
query.setParameter("client", client.getEntity());
- query.setParameter("role", ((RoleAdapter)role).getRole());
+ query.setParameter("role", ((RoleAdapter) role).getRole());
return query;
}
@Override
public boolean validatePassword(UserModel user, String password) {
- for (CredentialEntity cred : ((UserAdapter)user).getUser().getCredentials()) {
+ for (CredentialEntity cred : ((UserAdapter) user).getUser().getCredentials()) {
if (cred.getType().equals(UserCredentialModel.PASSWORD)) {
return new Pbkdf2PasswordEncoder(cred.getSalt()).verify(password, cred.getValue());
}
@@ -1196,7 +1201,7 @@ public class RealmAdapter implements RealmModel {
@Override
public boolean validateTOTP(UserModel user, String password, String token) {
if (!validatePassword(user, password)) return false;
- for (CredentialEntity cred : ((UserAdapter)user).getUser().getCredentials()) {
+ for (CredentialEntity cred : ((UserAdapter) user).getUser().getCredentials()) {
if (cred.getType().equals(UserCredentialModel.TOTP)) {
return new TimeBasedOTP().validate(token, cred.getValue().getBytes());
}
@@ -1297,7 +1302,7 @@ public class RealmAdapter implements RealmModel {
public boolean equals(Object o) {
if (o == null) return false;
if (!(o instanceof RealmAdapter)) return false;
- RealmAdapter r = (RealmAdapter)o;
+ RealmAdapter r = (RealmAdapter) o;
return r.getId().equals(getId());
}
@@ -1367,4 +1372,45 @@ public class RealmAdapter implements RealmModel {
em.flush();
}
+ @Override
+ public UserSessionModel createUserSession(UserModel user, String ipAddress) {
+ UserSessionEntity entity = new UserSessionEntity();
+ entity.setUser(((UserAdapter) user).getUser());
+ entity.setIpAddress(ipAddress);
+
+ int currentTime = Time.currentTime();
+ int expires = currentTime + realm.getCentralLoginLifespan();
+
+ entity.setStarted(currentTime);
+ entity.setExpires(expires);
+
+ em.persist(entity);
+ return new UserSessionAdapter(entity);
+ }
+
+ @Override
+ public UserSessionModel getUserSession(String id) {
+ UserSessionEntity entity = em.find(UserSessionEntity.class, id);
+ return entity != null ? new UserSessionAdapter(entity) : null;
+ }
+
+ @Override
+ public void removeUserSession(UserSessionModel session) {
+ em.remove(((UserSessionAdapter) session).getEntity());
+ }
+
+ @Override
+ public void removeUserSessions(UserModel user) {
+ removeUserSessions(((UserAdapter) user).getUser());
+ }
+
+ private void removeUserSessions(UserEntity user) {
+ em.createNamedQuery("removeUserSessionByUser").setParameter("user", user).executeUpdate();
+ }
+
+ @Override
+ public void removeExpiredUserSessions() {
+ em.createNamedQuery("removeUserSessionExpired").setParameter("currentTime", Time.currentTime()).executeUpdate();
+ }
+
}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/UserSessionAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/UserSessionAdapter.java
new file mode 100644
index 0000000..1f4ba28
--- /dev/null
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/UserSessionAdapter.java
@@ -0,0 +1,72 @@
+package org.keycloak.models.jpa;
+
+import org.keycloak.models.UserSessionModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.jpa.entities.UserSessionEntity;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class UserSessionAdapter implements UserSessionModel {
+
+ private UserSessionEntity entity;
+
+ public UserSessionAdapter(UserSessionEntity entity) {
+ this.entity = entity;
+ }
+
+ public UserSessionEntity getEntity() {
+ return entity;
+ }
+
+ @Override
+ public String getId() {
+ return entity.getId();
+ }
+
+ @Override
+ public void setId(String id) {
+ entity.setId(id);
+ }
+
+ @Override
+ public UserModel getUser() {
+ return new UserAdapter(entity.getUser());
+ }
+
+ @Override
+ public void setUser(UserModel user) {
+ entity.setUser(((UserAdapter) user).getUser());
+ }
+
+ @Override
+ public String getIpAddress() {
+ return entity.getIpAddress();
+ }
+
+ @Override
+ public void setIpAddress(String ipAddress) {
+ entity.setIpAddress(ipAddress);
+ }
+
+ @Override
+ public int getStarted() {
+ return entity.getStarted();
+ }
+
+ @Override
+ public void setStarted(int started) {
+ entity.setStarted(started);
+ }
+
+ @Override
+ public int getExpires() {
+ return entity.getExpires();
+ }
+
+ @Override
+ public void setExpires(int expires) {
+ entity.setExpires(expires);
+ }
+
+}
diff --git a/model/jpa/src/test/resources/META-INF/persistence.xml b/model/jpa/src/test/resources/META-INF/persistence.xml
index 558aa89..900cb5d 100755
--- a/model/jpa/src/test/resources/META-INF/persistence.xml
+++ b/model/jpa/src/test/resources/META-INF/persistence.xml
@@ -16,6 +16,7 @@
<class>org.keycloak.models.jpa.entities.SocialLinkEntity</class>
<class>org.keycloak.models.jpa.entities.AuthenticationLinkEntity</class>
<class>org.keycloak.models.jpa.entities.UserEntity</class>
+ <class>org.keycloak.models.jpa.entities.UserSessionEntity</class>
<class>org.keycloak.models.jpa.entities.UserRoleMappingEntity</class>
<class>org.keycloak.models.jpa.entities.UsernameLoginFailureEntity</class>
<class>org.keycloak.models.jpa.entities.ScopeMappingEntity</class>
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoKeycloakSessionFactory.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoKeycloakSessionFactory.java
index d431803..6feb373 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoKeycloakSessionFactory.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoKeycloakSessionFactory.java
@@ -16,6 +16,7 @@ import org.keycloak.models.mongo.keycloak.entities.MongoOAuthClientEntity;
import org.keycloak.models.mongo.keycloak.entities.MongoRealmEntity;
import org.keycloak.models.mongo.keycloak.entities.MongoRoleEntity;
import org.keycloak.models.mongo.keycloak.entities.MongoUserEntity;
+import org.keycloak.models.mongo.keycloak.entities.MongoUserSessionEntity;
import org.keycloak.models.mongo.keycloak.entities.MongoUsernameLoginFailureEntity;
/**
@@ -37,7 +38,8 @@ public class MongoKeycloakSessionFactory implements KeycloakSessionFactory {
AuthenticationLinkEntity.class,
MongoApplicationEntity.class,
MongoOAuthClientEntity.class,
- MongoUsernameLoginFailureEntity.class
+ MongoUsernameLoginFailureEntity.class,
+ MongoUserSessionEntity.class
};
private final MongoClientProvider mongoClientProvider;
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
index 52308de..82f95c0 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
@@ -1,5 +1,6 @@
package org.keycloak.models.mongo.keycloak.adapters;
+import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
import com.mongodb.QueryBuilder;
import org.jboss.logging.Logger;
@@ -16,6 +17,7 @@ import org.keycloak.models.SocialLinkModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserCredentialValueModel;
import org.keycloak.models.UserModel;
+import org.keycloak.models.UserSessionModel;
import org.keycloak.models.UsernameLoginFailureModel;
import org.keycloak.models.entities.AuthenticationLinkEntity;
import org.keycloak.models.entities.AuthenticationProviderEntity;
@@ -28,11 +30,13 @@ import org.keycloak.models.mongo.keycloak.entities.MongoOAuthClientEntity;
import org.keycloak.models.mongo.keycloak.entities.MongoRealmEntity;
import org.keycloak.models.mongo.keycloak.entities.MongoRoleEntity;
import org.keycloak.models.mongo.keycloak.entities.MongoUserEntity;
+import org.keycloak.models.mongo.keycloak.entities.MongoUserSessionEntity;
import org.keycloak.models.mongo.keycloak.entities.MongoUsernameLoginFailureEntity;
import org.keycloak.models.mongo.utils.MongoModelUtils;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.Pbkdf2PasswordEncoder;
import org.keycloak.models.utils.TimeBasedOTP;
+import org.keycloak.util.Time;
import java.security.PrivateKey;
import java.security.PublicKey;
@@ -1333,4 +1337,51 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
public MongoRealmEntity getMongoEntity() {
return realm;
}
+
+ @Override
+ public UserSessionModel createUserSession(UserModel user, String ipAddress) {
+ MongoUserSessionEntity entity = new MongoUserSessionEntity();
+ entity.setUser(user.getId());
+ entity.setIpAddress(ipAddress);
+
+ int currentTime = Time.currentTime();
+ int expires = currentTime + realm.getCentralLoginLifespan();
+
+ entity.setStarted(currentTime);
+ entity.setExpires(expires);
+
+ getMongoStore().insertEntity(entity, invocationContext);
+ return new UserSessionAdapter(entity, this, invocationContext);
+ }
+
+ @Override
+ public UserSessionModel getUserSession(String id) {
+ MongoUserSessionEntity entity = getMongoStore().loadEntity(MongoUserSessionEntity.class, id, invocationContext);
+ if (entity == null) {
+ return null;
+ } else {
+ return new UserSessionAdapter(entity, this, invocationContext);
+ }
+ }
+
+ @Override
+ public void removeUserSession(UserSessionModel session) {
+ getMongoStore().removeEntity(((UserSessionAdapter) session).getEntity(), invocationContext);
+ }
+
+ @Override
+ public void removeUserSessions(UserModel user) {
+ DBObject query = new BasicDBObject("user", user.getId());
+ getMongoStore().removeEntities(MongoUserSessionEntity.class, query, invocationContext);
+ }
+
+ @Override
+ public void removeExpiredUserSessions() {
+ DBObject query = new QueryBuilder()
+ .and("expires").lessThan(Time.currentTime())
+ .get();
+
+ getMongoStore().removeEntities(MongoUserSessionEntity.class, query, invocationContext);
+ }
+
}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserSessionAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserSessionAdapter.java
new file mode 100644
index 0000000..ab02e1c
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserSessionAdapter.java
@@ -0,0 +1,77 @@
+package org.keycloak.models.mongo.keycloak.adapters;
+
+import org.keycloak.models.UserModel;
+import org.keycloak.models.UserSessionModel;
+import org.keycloak.models.mongo.api.context.MongoStoreInvocationContext;
+import org.keycloak.models.mongo.keycloak.entities.MongoUserSessionEntity;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class UserSessionAdapter implements UserSessionModel {
+
+ private MongoUserSessionEntity entity;
+ private RealmAdapter realm;
+ private MongoStoreInvocationContext invContext;
+
+ public UserSessionAdapter(MongoUserSessionEntity entity, RealmAdapter realm, MongoStoreInvocationContext invContext) {
+ this.entity = entity;
+ this.realm = realm;
+ this.invContext = invContext;
+ }
+
+ public MongoUserSessionEntity getEntity() {
+ return entity;
+ }
+
+ @Override
+ public String getId() {
+ return entity.getId();
+ }
+
+ @Override
+ public void setId(String id) {
+ entity.setId(id);
+ }
+
+ @Override
+ public UserModel getUser() {
+ return realm.getUserById(entity.getUser());
+ }
+
+ @Override
+ public void setUser(UserModel user) {
+ entity.setUser(user.getId());
+ }
+
+ @Override
+ public String getIpAddress() {
+ return entity.getIpAddress();
+ }
+
+ @Override
+ public void setIpAddress(String ipAddress) {
+ entity.setIpAddress(ipAddress);
+ }
+
+ @Override
+ public int getStarted() {
+ return entity.getStarted();
+ }
+
+ @Override
+ public void setStarted(int started) {
+ entity.setStarted(started);
+ }
+
+ @Override
+ public int getExpires() {
+ return entity.getExpires();
+ }
+
+ @Override
+ public void setExpires(int expires) {
+ entity.setExpires(expires);
+ }
+
+}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoUserEntity.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoUserEntity.java
index e9dd851..084a153 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoUserEntity.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoUserEntity.java
@@ -1,5 +1,7 @@
package org.keycloak.models.mongo.keycloak.entities;
+import com.mongodb.DBObject;
+import com.mongodb.QueryBuilder;
import org.keycloak.models.entities.UserEntity;
import org.keycloak.models.mongo.api.MongoCollection;
import org.keycloak.models.mongo.api.MongoIdentifiableEntity;
@@ -27,6 +29,10 @@ public class MongoUserEntity extends UserEntity implements MongoIdentifiableEnti
@Override
public void afterRemove(MongoStoreInvocationContext invocationContext) {
- //To change body of implemented methods use File | Settings | File Templates.
+ DBObject query = new QueryBuilder()
+ .and("userId").is(getId())
+ .get();
+
+ invocationContext.getMongoStore().removeEntities(MongoUserSessionEntity.class, query, invocationContext);
}
}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoUserSessionEntity.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoUserSessionEntity.java
new file mode 100644
index 0000000..de2a6d0
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoUserSessionEntity.java
@@ -0,0 +1,58 @@
+package org.keycloak.models.mongo.keycloak.entities;
+
+import org.keycloak.models.entities.AbstractIdentifiableEntity;
+import org.keycloak.models.mongo.api.MongoCollection;
+import org.keycloak.models.mongo.api.MongoIdentifiableEntity;
+import org.keycloak.models.mongo.api.context.MongoStoreInvocationContext;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+@MongoCollection(collectionName = "sessions")
+public class MongoUserSessionEntity extends AbstractIdentifiableEntity implements MongoIdentifiableEntity {
+
+ private String user;
+
+ private String ipAddress;
+
+ private int started;
+
+ private int expires;
+
+ public String getUser() {
+ return user;
+ }
+
+ public void setUser(String user) {
+ this.user = user;
+ }
+
+ public String getIpAddress() {
+ return ipAddress;
+ }
+
+ public void setIpAddress(String ipAddress) {
+ this.ipAddress = ipAddress;
+ }
+
+ public int getStarted() {
+ return started;
+ }
+
+ public void setStarted(int started) {
+ this.started = started;
+ }
+
+ public int getExpires() {
+ return expires;
+ }
+
+ public void setExpires(int expires) {
+ this.expires = expires;
+ }
+
+ @Override
+ public void afterRemove(MongoStoreInvocationContext invocationContext) {
+ }
+
+}
diff --git a/model/tests/src/test/java/org/keycloak/model/test/AdapterTest.java b/model/tests/src/test/java/org/keycloak/model/test/AdapterTest.java
index 4270d29..b15de5d 100755
--- a/model/tests/src/test/java/org/keycloak/model/test/AdapterTest.java
+++ b/model/tests/src/test/java/org/keycloak/model/test/AdapterTest.java
@@ -14,6 +14,7 @@ import org.keycloak.models.RoleModel;
import org.keycloak.models.SocialLinkModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
+import org.keycloak.models.UserSessionModel;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.services.managers.OAuthClientManager;
import org.keycloak.services.managers.RealmManager;
@@ -24,6 +25,9 @@ import java.util.HashSet;
import java.util.List;
import java.util.Set;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
@@ -46,7 +50,7 @@ public class AdapterTest extends AbstractModelTest {
realmModel.addDefaultRole("foo");
realmModel = realmManager.getRealm(realmModel.getId());
- Assert.assertNotNull(realmModel);
+ assertNotNull(realmModel);
Assert.assertEquals(realmModel.getAccessCodeLifespan(), 100);
Assert.assertEquals(600, realmModel.getAccessCodeLifespanUserAction());
Assert.assertEquals(realmModel.getAccessTokenLifespan(), 1000);
@@ -73,7 +77,7 @@ public class AdapterTest extends AbstractModelTest {
realmModel.addDefaultRole("foo");
realmModel = realmManager.getRealm(realmModel.getId());
- Assert.assertNotNull(realmModel);
+ assertNotNull(realmModel);
Assert.assertEquals(realmModel.getAccessCodeLifespan(), 100);
Assert.assertEquals(600, realmModel.getAccessCodeLifespanUserAction());
Assert.assertEquals(realmModel.getAccessTokenLifespan(), 1000);
@@ -169,7 +173,7 @@ public class AdapterTest extends AbstractModelTest {
realmModel = identitySession.getRealm("JUGGLER");
Assert.assertTrue(realmModel.removeUser("bburke"));
Assert.assertFalse(realmModel.removeUser("bburke"));
- Assert.assertNull(realmModel.getUser("bburke"));
+ assertNull(realmModel.getUser("bburke"));
}
@Test
@@ -191,7 +195,7 @@ public class AdapterTest extends AbstractModelTest {
Assert.assertTrue(realmModel.removeApplication(app.getId()));
Assert.assertFalse(realmModel.removeApplication(app.getId()));
- Assert.assertNull(realmModel.getApplicationById(app.getId()));
+ assertNull(realmModel.getApplicationById(app.getId()));
}
@@ -226,7 +230,7 @@ public class AdapterTest extends AbstractModelTest {
Assert.assertTrue(realmManager.removeRealm(realmModel));
Assert.assertFalse(realmManager.removeRealm(realmModel));
- Assert.assertNull(realmManager.getRealm(realmModel.getId()));
+ assertNull(realmManager.getRealm(realmModel.getId()));
}
@@ -253,11 +257,11 @@ public class AdapterTest extends AbstractModelTest {
Assert.assertTrue(realmModel.removeRoleById(realmRole.getId()));
Assert.assertFalse(realmModel.removeRoleById(realmRole.getId()));
- Assert.assertNull(realmModel.getRole(realmRole.getName()));
+ assertNull(realmModel.getRole(realmRole.getName()));
Assert.assertTrue(realmModel.removeRoleById(appRole.getId()));
Assert.assertFalse(realmModel.removeRoleById(appRole.getId()));
- Assert.assertNull(app.getRole(appRole.getName()));
+ assertNull(app.getRole(appRole.getName()));
}
@Test
@@ -435,7 +439,7 @@ public class AdapterTest extends AbstractModelTest {
realmModel.grantRole(user, realmUserRole);
Assert.assertTrue(realmModel.hasRole(user, realmUserRole));
RoleModel found = realmModel.getRoleById(realmUserRole.getId());
- Assert.assertNotNull(found);
+ assertNotNull(found);
assertRolesEquals(found, realmUserRole);
// Test app roles
@@ -445,10 +449,10 @@ public class AdapterTest extends AbstractModelTest {
Set<RoleModel> appRoles = application.getRoles();
Assert.assertEquals(2, appRoles.size());
RoleModel appBarRole = application.getRole("bar");
- Assert.assertNotNull(appBarRole);
+ assertNotNull(appBarRole);
found = realmModel.getRoleById(appBarRole.getId());
- Assert.assertNotNull(found);
+ assertNotNull(found);
assertRolesEquals(found, appBarRole);
realmModel.grantRole(user, appBarRole);
@@ -719,4 +723,43 @@ public class AdapterTest extends AbstractModelTest {
resetSession();
}
+ @Test
+ public void userSessions() throws InterruptedException {
+ realmManager.createRealm("userSessions");
+ realmManager.getRealmByName("userSessions").setCentralLoginLifespan(5);
+
+ UserModel user = realmManager.getRealmByName("userSessions").addUser("userSessions1");
+
+ UserSessionModel userSession = realmManager.getRealmByName("userSessions").createUserSession(user, "127.0.0.1");
+ commit();
+
+ assertNotNull(realmManager.getRealmByName("userSessions").getUserSession(userSession.getId()));
+ commit();
+
+ realmManager.getRealmByName("userSessions").removeUserSession(realmManager.getRealmByName("userSessions").getUserSession(userSession.getId()));
+ commit();
+
+ assertNull(realmManager.getRealmByName("userSessions").getUserSession(userSession.getId()));
+
+ userSession = realmManager.getRealmByName("userSessions").createUserSession(user, "127.0.0.1");
+ commit();
+
+ realmManager.getRealmByName("userSessions").removeUserSessions(user);
+ commit();
+
+ assertNull(realmManager.getRealmByName("userSessions").getUserSession(userSession.getId()));
+
+ realmManager.getRealmByName("userSessions").setCentralLoginLifespan(1);
+
+ userSession = realmManager.getRealmByName("userSessions").createUserSession(user, "127.0.0.1");
+ commit();
+
+ Thread.sleep(2000);
+
+ realmManager.getRealmByName("userSessions").removeExpiredUserSessions();
+ commit();
+
+ assertNull(realmManager.getRealmByName("userSessions").getUserSession(userSession.getId()));
+ }
+
}
diff --git a/project-integrations/aerogear-ups/auth-server/src/main/resources/META-INF/persistence.xml b/project-integrations/aerogear-ups/auth-server/src/main/resources/META-INF/persistence.xml
index 3eeed1f..616f8e8 100755
--- a/project-integrations/aerogear-ups/auth-server/src/main/resources/META-INF/persistence.xml
+++ b/project-integrations/aerogear-ups/auth-server/src/main/resources/META-INF/persistence.xml
@@ -15,6 +15,7 @@
<class>org.keycloak.models.jpa.entities.SocialLinkEntity</class>
<class>org.keycloak.models.jpa.entities.AuthenticationLinkEntity</class>
<class>org.keycloak.models.jpa.entities.UserEntity</class>
+ <class>org.keycloak.models.jpa.entities.UserSessionEntity</class>
<class>org.keycloak.models.jpa.entities.UsernameLoginFailureEntity</class>
<class>org.keycloak.models.jpa.entities.UserRoleMappingEntity</class>
<class>org.keycloak.models.jpa.entities.ScopeMappingEntity</class>
diff --git a/server/src/main/resources/META-INF/persistence.xml b/server/src/main/resources/META-INF/persistence.xml
index 3eeed1f..616f8e8 100755
--- a/server/src/main/resources/META-INF/persistence.xml
+++ b/server/src/main/resources/META-INF/persistence.xml
@@ -15,6 +15,7 @@
<class>org.keycloak.models.jpa.entities.SocialLinkEntity</class>
<class>org.keycloak.models.jpa.entities.AuthenticationLinkEntity</class>
<class>org.keycloak.models.jpa.entities.UserEntity</class>
+ <class>org.keycloak.models.jpa.entities.UserSessionEntity</class>
<class>org.keycloak.models.jpa.entities.UsernameLoginFailureEntity</class>
<class>org.keycloak.models.jpa.entities.UserRoleMappingEntity</class>
<class>org.keycloak.models.jpa.entities.ScopeMappingEntity</class>
diff --git a/services/src/main/java/org/keycloak/services/managers/AccessCodeEntry.java b/services/src/main/java/org/keycloak/services/managers/AccessCodeEntry.java
index abbc793..93f9f6c 100755
--- a/services/src/main/java/org/keycloak/services/managers/AccessCodeEntry.java
+++ b/services/src/main/java/org/keycloak/services/managers/AccessCodeEntry.java
@@ -6,6 +6,7 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserModel.RequiredAction;
+import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.representations.AccessToken;
import org.keycloak.util.Time;
@@ -23,6 +24,7 @@ public class AccessCodeEntry {
protected String id = UUID.randomUUID().toString() + System.currentTimeMillis();
protected String code;
protected String state;
+ protected String sessionState;
protected String redirectUri;
protected boolean rememberMe;
protected String authMethod;
@@ -117,6 +119,14 @@ public class AccessCodeEntry {
this.state = state;
}
+ public String getSessionState() {
+ return sessionState;
+ }
+
+ public void setSessionState(String sessionState) {
+ this.sessionState = sessionState;
+ }
+
public String getRedirectUri() {
return redirectUri;
}
diff --git a/services/src/main/java/org/keycloak/services/managers/AppAuthManager.java b/services/src/main/java/org/keycloak/services/managers/AppAuthManager.java
index 0a3c305..41586d6 100755
--- a/services/src/main/java/org/keycloak/services/managers/AppAuthManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/AppAuthManager.java
@@ -31,9 +31,12 @@ public class AppAuthManager extends AuthenticationManager {
}
public UserModel authenticateRequest(RealmModel realm, UriInfo uriInfo, HttpHeaders headers) {
- UserModel user = authenticateIdentityCookie(realm, uriInfo, headers);
- if (user != null) return user;
- return authenticateBearerToken(realm, uriInfo, headers);
+ AuthResult authResult = authenticateIdentityCookie(realm, uriInfo, headers);
+ if (authResult != null) {
+ return authResult.getUser();
+ } else {
+ return authenticateBearerToken(realm, uriInfo, headers);
+ }
}
public String extractAuthorizationHeaderToken(HttpHeaders headers) {
@@ -51,7 +54,8 @@ public class AppAuthManager extends AuthenticationManager {
public UserModel authenticateBearerToken(RealmModel realm, UriInfo uriInfo, HttpHeaders headers) {
String tokenString = extractAuthorizationHeaderToken(headers);
if (tokenString == null) return null;
- return verifyIdentityToken(realm, uriInfo, true, tokenString);
+ AuthResult authResult = verifyIdentityToken(realm, uriInfo, true, tokenString);
+ return authResult != null ? authResult.getUser() : null;
}
}
diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
index 2fb333a..c8df10c 100755
--- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
@@ -11,6 +11,7 @@ import org.keycloak.models.ClientModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RequiredCredentialModel;
import org.keycloak.models.UserModel;
+import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.provider.ProviderSession;
import org.keycloak.representations.AccessToken;
@@ -55,28 +56,31 @@ public class AuthenticationManager {
this.protector = protector;
}
- public AccessToken createIdentityToken(RealmModel realm, UserModel user) {
+ public AccessToken createIdentityToken(RealmModel realm, UserModel user, UserSessionModel session) {
logger.info("createIdentityToken");
AccessToken token = new AccessToken();
token.id(KeycloakModelUtils.generateId());
token.issuedNow();
token.subject(user.getId());
token.audience(realm.getName());
+ if (session != null) {
+ token.setSessionState(session.getId());
+ }
if (realm.getCentralLoginLifespan() > 0) {
token.expiration(Time.currentTime() + realm.getCentralLoginLifespan());
}
return token;
}
- public NewCookie createLoginCookie(RealmModel realm, UserModel user, UriInfo uriInfo, boolean rememberMe) {
+ public NewCookie createLoginCookie(RealmModel realm, UserModel user, UserSessionModel session, UriInfo uriInfo, boolean rememberMe) {
logger.info("createLoginCookie");
String cookieName = KEYCLOAK_IDENTITY_COOKIE;
String cookiePath = getIdentityCookiePath(realm, uriInfo);
- return createLoginCookie(realm, user, null, cookieName, cookiePath, rememberMe);
+ return createLoginCookie(realm, user, session, null, cookieName, cookiePath, rememberMe);
}
- protected NewCookie createLoginCookie(RealmModel realm, UserModel user, ClientModel client, String cookieName, String cookiePath, boolean rememberMe) {
- AccessToken identityToken = createIdentityToken(realm, user);
+ protected NewCookie createLoginCookie(RealmModel realm, UserModel user, UserSessionModel session, ClientModel client, String cookieName, String cookiePath, boolean rememberMe) {
+ AccessToken identityToken = createIdentityToken(realm, user, session);
if (client != null) {
identityToken.issuedFor(client.getClientId());
}
@@ -136,17 +140,17 @@ public class AuthenticationManager {
response.addNewCookie(expireIt);
}
- public UserModel authenticateIdentityCookie(RealmModel realm, UriInfo uriInfo, HttpHeaders headers) {
+ public AuthResult authenticateIdentityCookie(RealmModel realm, UriInfo uriInfo, HttpHeaders headers) {
return authenticateIdentityCookie(realm, uriInfo, headers, true);
}
- public UserModel authenticateIdentityCookie(RealmModel realm, UriInfo uriInfo, HttpHeaders headers, boolean checkActive) {
+ public AuthResult authenticateIdentityCookie(RealmModel realm, UriInfo uriInfo, HttpHeaders headers, boolean checkActive) {
logger.info("authenticateIdentityCookie");
String cookieName = KEYCLOAK_IDENTITY_COOKIE;
return authenticateIdentityCookie(realm, uriInfo, headers, cookieName, checkActive);
}
- protected UserModel authenticateIdentityCookie(RealmModel realm, UriInfo uriInfo, HttpHeaders headers, String cookieName, boolean checkActive) {
+ private AuthResult authenticateIdentityCookie(RealmModel realm, UriInfo uriInfo, HttpHeaders headers, String cookieName, boolean checkActive) {
logger.info("authenticateIdentityCookie");
Cookie cookie = headers.getCookies().get(cookieName);
if (cookie == null) {
@@ -155,14 +159,14 @@ public class AuthenticationManager {
}
String tokenString = cookie.getValue();
- UserModel user = verifyIdentityToken(realm, uriInfo, checkActive, tokenString);
- if (user == null) {
+ AuthResult authResult = verifyIdentityToken(realm, uriInfo, checkActive, tokenString);
+ if (authResult == null) {
expireIdentityCookie(realm, uriInfo);
}
- return user;
+ return authResult;
}
- protected UserModel verifyIdentityToken(RealmModel realm, UriInfo uriInfo, boolean checkActive, String tokenString) {
+ protected AuthResult verifyIdentityToken(RealmModel realm, UriInfo uriInfo, boolean checkActive, String tokenString) {
try {
AccessToken token = RSATokenVerifier.verifyToken(tokenString, realm.getPublicKey(), realm.getName(), checkActive);
logger.info("identity token verified");
@@ -188,10 +192,16 @@ public class AuthenticationManager {
if (token.getIssuedAt() < user.getNotBefore()) {
logger.info("Stale cookie");
return null;
+ }
+ UserSessionModel session = realm.getUserSession(token.getSessionState());
+ if (session == null || session.getExpires() < Time.currentTime()) {
+ logger.info("User session not active");
+ expireIdentityCookie(realm, uriInfo);
+ return null;
}
- return user;
+ return new AuthResult(user, session);
} catch (VerificationException e) {
logger.info("Failed to verify identity token", e);
}
@@ -328,4 +338,22 @@ public class AuthenticationManager {
SUCCESS, ACCOUNT_TEMPORARILY_DISABLED, ACCOUNT_DISABLED, ACTIONS_REQUIRED, INVALID_USER, INVALID_CREDENTIALS, MISSING_PASSWORD, MISSING_TOTP, FAILED
}
+ public class AuthResult {
+ private final UserModel user;
+ private final UserSessionModel session;
+
+ public AuthResult(UserModel user, UserSessionModel session) {
+ this.user = user;
+ this.session = session;
+ }
+
+ public UserSessionModel getSession() {
+ return session;
+ }
+
+ public UserModel getUser() {
+ return user;
+ }
+ }
+
}
diff --git a/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java b/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java
index 48600fd..ce06542 100755
--- a/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java
@@ -1,10 +1,10 @@
package org.keycloak.services.managers;
import org.apache.http.client.HttpClient;
+import org.jboss.logging.Logger;
import org.jboss.resteasy.client.ClientRequest;
import org.jboss.resteasy.client.ClientResponse;
import org.jboss.resteasy.client.core.executors.ApacheHttpClient4Executor;
-import org.jboss.logging.Logger;
import org.keycloak.TokenIdGenerator;
import org.keycloak.adapters.AdapterConstants;
import org.keycloak.models.ApplicationModel;
@@ -146,7 +146,7 @@ public class ResourceAdminManager {
}
- public void logoutUser(URI requestUri, RealmModel realm, UserModel user) {
+ public void logoutUser(URI requestUri, RealmModel realm, String user, String session) {
ApacheHttpClient4Executor executor = createExecutor();
try {
@@ -154,7 +154,7 @@ public class ResourceAdminManager {
List<ApplicationModel> resources = realm.getApplications();
logger.debugv("logging out {0} resources ", resources.size());
for (ApplicationModel resource : resources) {
- logoutApplication(requestUri, realm, resource, user.getId(), executor, 0);
+ logoutApplication(requestUri, realm, resource, user, session, executor, 0);
}
} finally {
executor.getHttpClient().getConnectionManager().shutdown();
@@ -168,19 +168,19 @@ public class ResourceAdminManager {
List<ApplicationModel> resources = realm.getApplications();
logger.debugv("logging out {0} resources ", resources.size());
for (ApplicationModel resource : resources) {
- logoutApplication(requestUri, realm, resource, null, executor, realm.getNotBefore());
+ logoutApplication(requestUri, realm, resource, null, null, executor, realm.getNotBefore());
}
} finally {
executor.getHttpClient().getConnectionManager().shutdown();
}
}
- public void logoutApplication(URI requestUri, RealmModel realm, ApplicationModel resource, String user) {
+ public void logoutApplication(URI requestUri, RealmModel realm, ApplicationModel resource, String user, String session) {
ApacheHttpClient4Executor executor = createExecutor();
try {
resource.setNotBefore(Time.currentTime());
- logoutApplication(requestUri, realm, resource, user, executor, resource.getNotBefore());
+ logoutApplication(requestUri, realm, resource, user, session, executor, resource.getNotBefore());
} finally {
executor.getHttpClient().getConnectionManager().shutdown();
}
@@ -188,10 +188,10 @@ public class ResourceAdminManager {
}
- protected boolean logoutApplication(URI requestUri, RealmModel realm, ApplicationModel resource, String user, ApacheHttpClient4Executor client, int notBefore) {
+ protected boolean logoutApplication(URI requestUri, RealmModel realm, ApplicationModel resource, String user, String session, ApacheHttpClient4Executor client, int notBefore) {
String managementUrl = getManagementUrl(requestUri, resource);
if (managementUrl != null) {
- LogoutAction adminAction = new LogoutAction(TokenIdGenerator.generateId(), Time.currentTime() + 30, resource.getName(), user, notBefore);
+ LogoutAction adminAction = new LogoutAction(TokenIdGenerator.generateId(), Time.currentTime() + 30, resource.getName(), user, session, notBefore);
String token = new TokenManager().encodeToken(realm, adminAction);
logger.infov("logout user: {0} resource: {1} url: {2}", user, resource.getName(), managementUrl);
ClientRequest request = client.createRequest(UriBuilder.fromUri(managementUrl).path(AdapterConstants.K_LOGOUT).build().toString());
@@ -209,7 +209,7 @@ public class ResourceAdminManager {
response.releaseConnection();
}
} else {
- logger.info("Can't logout" + resource.getName() + " no mgmt url.");
+ logger.info("Can't logout " + resource.getName() + " no mgmt url.");
return false;
}
}
diff --git a/services/src/main/java/org/keycloak/services/managers/TokenManager.java b/services/src/main/java/org/keycloak/services/managers/TokenManager.java
index 0816c0e..4995e81 100755
--- a/services/src/main/java/org/keycloak/services/managers/TokenManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/TokenManager.java
@@ -14,6 +14,7 @@ import org.keycloak.models.ClientModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
+import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.AccessTokenResponse;
@@ -70,18 +71,23 @@ public class TokenManager {
- public AccessCodeEntry createAccessCode(String scopeParam, String state, String redirect, RealmModel realm, ClientModel client, UserModel user) {
- AccessCodeEntry code = createAccessCodeEntry(scopeParam, state, redirect, realm, client, user);
+ public AccessCodeEntry createAccessCode(String scopeParam, String state, String redirect, RealmModel realm, ClientModel client, UserModel user, UserSessionModel session) {
+ AccessCodeEntry code = createAccessCodeEntry(scopeParam, state, redirect, realm, client, user, session);
accessCodeMap.put(code.getId(), code);
return code;
}
- private AccessCodeEntry createAccessCodeEntry(String scopeParam, String state, String redirect, RealmModel realm, ClientModel client, UserModel user) {
+ private AccessCodeEntry createAccessCodeEntry(String scopeParam, String state, String redirect, RealmModel realm, ClientModel client, UserModel user, UserSessionModel session) {
AccessCodeEntry code = new AccessCodeEntry();
+ if (session != null) {
+ code.setSessionState(session.getId());
+ }
+
List<RoleModel> realmRolesRequested = code.getRealmRolesRequested();
MultivaluedMap<String, RoleModel> resourceRolesRequested = code.getResourceRolesRequested();
- AccessToken token = createClientAccessToken(scopeParam, realm, client, user, realmRolesRequested, resourceRolesRequested);
+ AccessToken token = createClientAccessToken(scopeParam, realm, client, user, session, realmRolesRequested, resourceRolesRequested);
+ token.setSessionState(code.getSessionState());
code.setToken(token);
code.setRealm(realm);
@@ -119,7 +125,7 @@ public class TokenManager {
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Stale refresh token");
}
- audit.user(refreshToken.getSubject()).detail(Details.REFRESH_TOKEN_ID, refreshToken.getId());
+ audit.user(refreshToken.getSubject()).session(refreshToken.getSessionState()).detail(Details.REFRESH_TOKEN_ID, refreshToken.getId());
UserModel user = realm.getUserById(refreshToken.getSubject());
if (user == null) {
@@ -128,12 +134,15 @@ public class TokenManager {
if (!user.isEnabled()) {
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "User disabled", "User disabled");
+ }
+ UserSessionModel session = realm.getUserSession(refreshToken.getSessionState());
+ if (session == null || session.getExpires() < Time.currentTime()) {
+ throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Session not active", "Session not active");
}
if (!client.getClientId().equals(refreshToken.getIssuedFor())) {
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Unmatching clients", "Unmatching clients");
-
}
if (refreshToken.getIssuedAt() < client.getNotBefore() || refreshToken.getIssuedAt() < user.getNotBefore()) {
@@ -179,17 +188,17 @@ public class TokenManager {
}
}
- AccessToken accessToken = initToken(realm, client, user);
+ AccessToken accessToken = initToken(realm, client, user, session);
accessToken.setRealmAccess(refreshToken.getRealmAccess());
accessToken.setResourceAccess(refreshToken.getResourceAccess());
return accessToken;
}
- public AccessToken createClientAccessToken(String scopeParam, RealmModel realm, ClientModel client, UserModel user) {
- return createClientAccessToken(scopeParam, realm, client, user, new LinkedList<RoleModel>(), new MultivaluedMapImpl<String, RoleModel>());
+ public AccessToken createClientAccessToken(String scopeParam, RealmModel realm, ClientModel client, UserModel user, UserSessionModel session) {
+ return createClientAccessToken(scopeParam, realm, client, user, session, new LinkedList<RoleModel>(), new MultivaluedMapImpl<String, RoleModel>());
}
- public AccessToken createClientAccessToken(String scopeParam, RealmModel realm, ClientModel client, UserModel user, List<RoleModel> realmRolesRequested, MultivaluedMap<String, RoleModel> resourceRolesRequested) {
+ public AccessToken createClientAccessToken(String scopeParam, RealmModel realm, ClientModel client, UserModel user, UserSessionModel session, List<RoleModel> realmRolesRequested, MultivaluedMap<String, RoleModel> resourceRolesRequested) {
// todo scopeParam is ignored until we figure out a scheme that fits with openid connect
Set<RoleModel> roleMappings = realm.getRoleMappings(user);
@@ -217,7 +226,7 @@ public class TokenManager {
}
}
- AccessToken token = initToken(realm, client, user);
+ AccessToken token = initToken(realm, client, user, session);
if (realmRolesRequested.size() > 0) {
for (RoleModel role : realmRolesRequested) {
@@ -270,7 +279,7 @@ public class TokenManager {
- protected AccessToken initToken(RealmModel realm, ClientModel client, UserModel user) {
+ protected AccessToken initToken(RealmModel realm, ClientModel client, UserModel user, UserSessionModel session) {
AccessToken token = new AccessToken();
token.id(KeycloakModelUtils.generateId());
token.subject(user.getId());
@@ -278,6 +287,9 @@ public class TokenManager {
token.issuedNow();
token.issuedFor(client.getClientId());
token.issuer(realm.getName());
+ if (session != null) {
+ token.setSessionState(session.getId());
+ }
if (realm.getAccessTokenLifespan() > 0) {
token.expiration(Time.currentTime() + realm.getAccessTokenLifespan());
}
@@ -351,8 +363,8 @@ public class TokenManager {
return this;
}
- public AccessTokenResponseBuilder generateAccessToken(String scopeParam, ClientModel client, UserModel user) {
- accessToken = createClientAccessToken(scopeParam, realm, client, user);
+ public AccessTokenResponseBuilder generateAccessToken(String scopeParam, ClientModel client, UserModel user, UserSessionModel session) {
+ accessToken = createClientAccessToken(scopeParam, realm, client, user, session);
return this;
}
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ApplicationResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ApplicationResource.java
index fcf2e0d..d1c3706 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/ApplicationResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/ApplicationResource.java
@@ -234,7 +234,7 @@ public class ApplicationResource {
@POST
public void logoutAll() {
auth.requireManage();
- new ResourceAdminManager().logoutApplication(uriInfo.getRequestUri(), realm, application, null);
+ new ResourceAdminManager().logoutApplication(uriInfo.getRequestUri(), realm, application, null, null);
}
@Path("logout-user/{username}")
@@ -245,7 +245,7 @@ public class ApplicationResource {
if (user == null) {
throw new NotFoundException("User not found");
}
- new ResourceAdminManager().logoutApplication(uriInfo.getRequestUri(), realm, application, user.getId());
+ new ResourceAdminManager().logoutApplication(uriInfo.getRequestUri(), realm, application, user.getId(), null);
}
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
index 10ce72d..56f5e3b 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
@@ -210,7 +210,7 @@ public class UsersResource {
}
// set notBefore so that user will be forced to log in.
user.setNotBefore(Time.currentTime());
- new ResourceAdminManager().logoutUser(uriInfo.getRequestUri(), realm, user);
+ new ResourceAdminManager().logoutUser(uriInfo.getRequestUri(), realm, user.getId(), null);
}
@@ -540,7 +540,7 @@ public class UsersResource {
Set<UserModel.RequiredAction> requiredActions = new HashSet<UserModel.RequiredAction>(user.getRequiredActions());
requiredActions.add(UserModel.RequiredAction.UPDATE_PASSWORD);
- AccessCodeEntry accessCode = tokenManager.createAccessCode(scope, state, redirect, realm, client, user);
+ AccessCodeEntry accessCode = tokenManager.createAccessCode(scope, state, redirect, realm, client, user, null);
accessCode.setRequiredActions(requiredActions);
accessCode.setExpiration(Time.currentTime() + realm.getAccessCodeLifespanUserAction());
diff --git a/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java b/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java
index 569ff93..a5c0abe 100755
--- a/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java
+++ b/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java
@@ -34,6 +34,7 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.RequiredCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserModel.RequiredAction;
+import org.keycloak.models.UserSessionModel;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.services.managers.AccessCodeEntry;
import org.keycloak.services.managers.AuthenticationManager;
@@ -74,12 +75,12 @@ public class OAuthFlows {
this.tokenManager = tokenManager;
}
- public Response redirectAccessCode(AccessCodeEntry accessCode, String state, String redirect) {
- return redirectAccessCode(accessCode, state, redirect, false);
+ public Response redirectAccessCode(AccessCodeEntry accessCode, UserSessionModel session, String state, String redirect) {
+ return redirectAccessCode(accessCode, session, state, redirect, false);
}
- public Response redirectAccessCode(AccessCodeEntry accessCode, String state, String redirect, boolean rememberMe) {
+ public Response redirectAccessCode(AccessCodeEntry accessCode, UserSessionModel session, String state, String redirect, boolean rememberMe) {
String code = accessCode.getCode();
if (Constants.INSTALLED_APP_URN.equals(redirect)) {
@@ -92,7 +93,7 @@ public class OAuthFlows {
Response.ResponseBuilder location = Response.status(302).location(redirectUri.build());
Cookie remember = request.getHttpHeaders().getCookies().get(AuthenticationManager.KEYCLOAK_REMEMBER_ME);
rememberMe = rememberMe || remember != null;
- location.cookie(authManager.createLoginCookie(realm, accessCode.getUser(), uriInfo, rememberMe));
+ location.cookie(authManager.createLoginCookie(realm, accessCode.getUser(), session, uriInfo, rememberMe));
return location.build();
}
}
@@ -109,17 +110,12 @@ public class OAuthFlows {
}
}
- public Response processAccessCode(String scopeParam, String state, String redirect, ClientModel client, UserModel user, Audit audit) {
- return processAccessCode(scopeParam, state, redirect, client, user, null, false, "form", audit);
- }
-
-
- public Response processAccessCode(String scopeParam, String state, String redirect, ClientModel client, UserModel user, String username, boolean rememberMe, String authMethod, Audit audit) {
+ public Response processAccessCode(String scopeParam, String state, String redirect, ClientModel client, UserModel user, UserSessionModel session, String username, boolean rememberMe, String authMethod, Audit audit) {
isTotpConfigurationRequired(user);
isEmailVerificationRequired(user);
boolean isResource = client instanceof ApplicationModel;
- AccessCodeEntry accessCode = tokenManager.createAccessCode(scopeParam, state, redirect, realm, client, user);
+ AccessCodeEntry accessCode = tokenManager.createAccessCode(scopeParam, state, redirect, realm, client, user, session);
accessCode.setUsername(username);
accessCode.setRememberMe(rememberMe);
accessCode.setAuthMethod(authMethod);
@@ -155,7 +151,7 @@ public class OAuthFlows {
if (redirect != null) {
audit.success();
- return redirectAccessCode(accessCode, state, redirect, rememberMe);
+ return redirectAccessCode(accessCode, session, state, redirect, rememberMe);
} else {
return null;
}
diff --git a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
index a89ae12..c18304a 100755
--- a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
+++ b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
@@ -29,6 +29,9 @@ import org.keycloak.services.managers.SocialRequestManager;
import org.keycloak.services.managers.TokenManager;
import org.keycloak.services.resources.admin.AdminRoot;
import org.keycloak.models.utils.ModelProviderUtils;
+import org.keycloak.services.scheduled.ClearExpiredAuditEvents;
+import org.keycloak.services.scheduled.ClearExpiredUserSessions;
+import org.keycloak.services.scheduled.ScheduledTaskRunner;
import org.keycloak.timer.TimerProvider;
import org.keycloak.timer.TimerProviderFactory;
import org.keycloak.util.JsonSerialization;
@@ -155,32 +158,8 @@ public class KeycloakApplication extends Application {
return;
}
TimerProvider timer = timerFactory.create(null);
-
- final ProviderFactory<AuditProvider> auditFactory = providerSessionFactory.getProviderFactory(AuditProvider.class);
- if (auditFactory != null) {
- timer.schedule(new Runnable() {
- @Override
- public void run() {
- KeycloakSession keycloakSession = keycloakSessionFactory.createSession();
- ProviderSession providerSession = providerSessionFactory.createSession();
- AuditProvider audit = providerSession.getProvider(AuditProvider.class);
- try {
- for (RealmModel realm : keycloakSession.getRealms()) {
- if (realm.isAuditEnabled() && realm.getAuditExpiration() > 0) {
- long olderThan = System.currentTimeMillis() - realm.getAuditExpiration() * 1000;
- log.info("Expiring audit events for " + realm.getName() + " older than " + new Date(olderThan));
- audit.clear(realm.getId(), olderThan);
- }
- }
- } finally {
- keycloakSession.close();
- audit.close();
- }
- }
- }, Config.getAuditExpirationSchedule());
- } else {
- log.info("Not scheduling audit expiration, no audit provider found");
- }
+ timer.schedule(new ScheduledTaskRunner(keycloakSessionFactory, providerSessionFactory, new ClearExpiredAuditEvents()), Config.getAuditExpirationSchedule());
+ timer.schedule(new ScheduledTaskRunner(keycloakSessionFactory, providerSessionFactory, new ClearExpiredUserSessions()), Config.getUserExpirationSchedule());
}
public KeycloakSessionFactory getFactory() {
@@ -204,7 +183,6 @@ public class KeycloakApplication extends Application {
public void importRealms(ServletContext context) {
importRealmFile();
importRealmResources(context);
-
}
public void importRealmResources(ServletContext context) {
diff --git a/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java b/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java
index 1343ec4..e760263 100755
--- a/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java
+++ b/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java
@@ -36,9 +36,11 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserModel.RequiredAction;
+import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.TimeBasedOTP;
import org.keycloak.provider.ProviderSession;
import org.keycloak.representations.idm.CredentialRepresentation;
+import org.keycloak.services.ClientConnection;
import org.keycloak.services.email.EmailException;
import org.keycloak.services.email.EmailSender;
import org.keycloak.services.managers.AccessCodeEntry;
@@ -62,7 +64,9 @@ import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import javax.ws.rs.ext.Providers;
+import java.util.HashMap;
import java.util.HashSet;
+import java.util.Map;
import java.util.Set;
/**
@@ -83,6 +87,9 @@ public class RequiredActionsService {
private UriInfo uriInfo;
@Context
+ private ClientConnection clientConnection;
+
+ @Context
protected Providers providers;
@Context
@@ -317,7 +324,10 @@ public class RequiredActionsService {
Set<RequiredAction> requiredActions = new HashSet<RequiredAction>(user.getRequiredActions());
requiredActions.add(RequiredAction.UPDATE_PASSWORD);
- AccessCodeEntry accessCode = tokenManager.createAccessCode(scopeParam, state, redirect, realm, client, user);
+ UserSessionModel session = realm.createUserSession(user, clientConnection.getRemoteAddr());
+ audit.session(session);
+
+ AccessCodeEntry accessCode = tokenManager.createAccessCode(scopeParam, state, redirect, realm, client, user, session);
accessCode.setRequiredActions(requiredActions);
accessCode.setExpiration(Time.currentTime() + realm.getAccessCodeLifespanUserAction());
accessCode.setAuthMethod("form");
@@ -395,18 +405,25 @@ public class RequiredActionsService {
logger.debugv("redirectOauth: redirecting to: {0}", accessCode.getRedirectUri());
accessCode.setExpiration(Time.currentTime() + realm.getAccessCodeLifespan());
- audit.success();
-
AuthenticationManager authManager = new AuthenticationManager(providerSession);
+ UserSessionModel session = realm.getUserSession(accessCode.getSessionState());
+ if (session == null || session.getExpires() < Time.currentTime()) {
+ return Flows.oauth(realm, request, uriInfo, authManager, tokenManager).redirectError(accessCode.getClient(), "access_denied", accessCode.getState(), accessCode.getRedirectUri());
+ }
+ audit.session(session);
+
+ audit.success();
+
return Flows.oauth(realm, request, uriInfo, authManager, tokenManager).redirectAccessCode(accessCode,
- accessCode.getState(), accessCode.getRedirectUri());
+ session, accessCode.getState(), accessCode.getRedirectUri());
}
}
private void initAudit(AccessCodeEntry accessCode) {
audit.event(Events.LOGIN).client(accessCode.getClient())
.user(accessCode.getUser())
+ .session(accessCode.getSessionState())
.detail(Details.CODE_ID, accessCode.getId())
.detail(Details.REDIRECT_URI, accessCode.getRedirectUri())
.detail(Details.RESPONSE_TYPE, "code")
diff --git a/services/src/main/java/org/keycloak/services/resources/SocialResource.java b/services/src/main/java/org/keycloak/services/resources/SocialResource.java
index 43f8435..06f0ae6 100755
--- a/services/src/main/java/org/keycloak/services/resources/SocialResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/SocialResource.java
@@ -36,6 +36,7 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.SocialLinkModel;
import org.keycloak.models.UserModel;
+import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.services.ClientConnection;
import org.keycloak.provider.ProviderSession;
@@ -254,7 +255,10 @@ public class SocialResource {
return oauth.forwardToSecurityFailure("Your account is not enabled.");
}
- return oauth.processAccessCode(scope, state, redirectUri, client, user, socialLink.getSocialUserId() + "@" + socialLink.getSocialProvider(), false, "social@" + provider.getId(), audit);
+ UserSessionModel session = realm.createUserSession(user, clientConnection.getRemoteAddr());
+ audit.session(session);
+
+ return oauth.processAccessCode(scope, state, redirectUri, client, user, session, socialLink.getSocialUserId() + "@" + socialLink.getSocialProvider(), false, "social@" + provider.getId(), audit);
}
@GET
diff --git a/services/src/main/java/org/keycloak/services/resources/TokenService.java b/services/src/main/java/org/keycloak/services/resources/TokenService.java
index 95d8367..4054953 100755
--- a/services/src/main/java/org/keycloak/services/resources/TokenService.java
+++ b/services/src/main/java/org/keycloak/services/resources/TokenService.java
@@ -26,6 +26,7 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.RequiredCredentialModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
+import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.provider.ProviderSession;
import org.keycloak.representations.AccessToken;
@@ -210,6 +211,16 @@ public class TokenService {
audit.event(Events.LOGIN).detail(Details.AUTH_METHOD, "oauth_credentials").detail(Details.RESPONSE_TYPE, "token");
+ String username = form.getFirst(AuthenticationManager.FORM_USERNAME);
+ if (username == null) {
+ audit.error(Errors.USERNAME_MISSING);
+ throw new UnauthorizedException("No username");
+ }
+ audit.detail(Details.USERNAME, username);
+
+ UserModel user = realm.getUser(username);
+ audit.user(user);
+
ClientModel client = authorizeClient(authorizationHeader, form, audit);
if (client.isPublicClient()) {
@@ -218,38 +229,49 @@ public class TokenService {
throw new ForbiddenException("Public clients are not allowed to invoke grants/access");
}
- String username = form.getFirst(AuthenticationManager.FORM_USERNAME);
- if (username == null) {
- audit.error(Errors.USERNAME_MISSING);
- throw new UnauthorizedException("No username");
- }
- audit.detail(Details.USERNAME, username);
if (!realm.isEnabled()) {
audit.error(Errors.REALM_DISABLED);
throw new UnauthorizedException("Disabled realm");
}
AuthenticationStatus authenticationStatus = authManager.authenticateForm(clientConnection, realm, form);
+ Map<String, String> err;
switch (authenticationStatus) {
case SUCCESS:
break;
case ACCOUNT_TEMPORARILY_DISABLED:
case ACTIONS_REQUIRED:
+ err = new HashMap<String, String>();
+ err.put(OAuth2Constants.ERROR, "invalid_grant");
+ err.put(OAuth2Constants.ERROR_DESCRIPTION, "Account temporarily disabled");
audit.error(Errors.USER_TEMPORARILY_DISABLED);
- return Response.status(503).type(MediaType.TEXT_PLAIN).entity("Account temporarily disabled").build();
+ return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(err)
+ .build();
case ACCOUNT_DISABLED:
- return Response.status(403).type(MediaType.TEXT_PLAIN).entity("Account disabled").build();
+ err = new HashMap<String, String>();
+ err.put(OAuth2Constants.ERROR, "invalid_grant");
+ err.put(OAuth2Constants.ERROR_DESCRIPTION, "Account disabled");
+ audit.error(Errors.USER_DISABLED);
+ return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(err)
+ .build();
default:
+ err = new HashMap<String, String>();
+ err.put(OAuth2Constants.ERROR, "invalid_grant");
+ err.put(OAuth2Constants.ERROR_DESCRIPTION, "Invalid user credentials");
audit.error(Errors.INVALID_USER_CREDENTIALS);
- throw new UnauthorizedException("Auth failed");
+ return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(err)
+ .build();
}
- UserModel user = realm.getUser(form.getFirst(AuthenticationManager.FORM_USERNAME));
String scope = form.getFirst(OAuth2Constants.SCOPE);
+ UserSessionModel session = realm.createUserSession(user, clientConnection.getRemoteAddr());
+ audit.session(session);
+
AccessTokenResponse res = tokenManager.responseBuilder(realm, client, audit)
- .generateAccessToken(scope, client, user)
+ .generateAccessToken(scope, client, user, session)
+ .generateRefreshToken()
.generateIDToken()
.build();
@@ -362,8 +384,12 @@ public class TokenService {
case SUCCESS:
case ACTIONS_REQUIRED:
UserModel user = KeycloakModelUtils.findUserByNameOrEmail(realm, username);
- audit.user(user);
- return oauth.processAccessCode(scopeParam, state, redirect, client, user, username, remember, "form", audit);
+ audit.user(user);
+
+ UserSessionModel session = realm.createUserSession(user, clientConnection.getRemoteAddr());
+ audit.session(session);
+
+ return oauth.processAccessCode(scopeParam, state, redirect, client, user, session, username, remember, "form", audit);
case ACCOUNT_TEMPORARILY_DISABLED:
audit.error(Errors.USER_TEMPORARILY_DISABLED);
return Flows.forms(realm, uriInfo).setError(Messages.ACCOUNT_TEMPORARILY_DISABLED).setFormData(formData).createLogin();
@@ -561,6 +587,7 @@ public class TokenService {
}
audit.user(accessCode.getUser());
+ audit.session(accessCode.getSessionState());
ClientModel client = authorizeClient(authorizationHeader, formData, audit);
@@ -589,6 +616,35 @@ public class TokenService {
.build();
}
+ UserModel user = realm.getUserById(accessCode.getUser().getId());
+ if (user == null) {
+ Map<String, String> res = new HashMap<String, String>();
+ res.put(OAuth2Constants.ERROR, "invalid_grant");
+ res.put(OAuth2Constants.ERROR_DESCRIPTION, "User not found");
+ audit.error(Errors.INVALID_CODE);
+ return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(res)
+ .build();
+ }
+
+ if (!user.isEnabled()) {
+ Map<String, String> res = new HashMap<String, String>();
+ res.put(OAuth2Constants.ERROR, "invalid_grant");
+ res.put(OAuth2Constants.ERROR_DESCRIPTION, "User disabled");
+ audit.error(Errors.INVALID_CODE);
+ return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(res)
+ .build();
+ }
+
+ UserSessionModel session = realm.getUserSession(accessCode.getSessionState());
+ if (session == null || session.getExpires() < Time.currentTime()) {
+ Map<String, String> res = new HashMap<String, String>();
+ res.put(OAuth2Constants.ERROR, "invalid_grant");
+ res.put(OAuth2Constants.ERROR_DESCRIPTION, "Session not active");
+ audit.error(Errors.INVALID_CODE);
+ return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(res)
+ .build();
+ }
+
logger.debug("accessRequest SUCCESS");
AccessTokenResponse res = tokenManager.responseBuilder(realm, client, audit)
@@ -693,11 +749,14 @@ public class TokenService {
}
logger.info("Checking cookie...");
- UserModel user = authManager.authenticateIdentityCookie(realm, uriInfo, headers);
- if (user != null) {
+ AuthenticationManager.AuthResult authResult = authManager.authenticateIdentityCookie(realm, uriInfo, headers);
+ if (authResult != null) {
+ UserModel user = authResult.getUser();
+ UserSessionModel session = authResult.getSession();
+
logger.debug(user.getLoginName() + " already logged in.");
- audit.user(user).detail(Details.AUTH_METHOD, "sso");
- return oauth.processAccessCode(scopeParam, state, redirect, client, user, null, false, "sso", audit);
+ audit.user(user).session(session).detail(Details.AUTH_METHOD, "sso");
+ return oauth.processAccessCode(scopeParam, state, redirect, client, user, session, null, false, "sso", audit);
}
if (prompt != null && prompt.equals("none")) {
@@ -760,25 +819,52 @@ public class TokenService {
@Path("logout")
@GET
@NoCache
- public Response logout(final @QueryParam("redirect_uri") String redirectUri) {
+ public Response logout(final @QueryParam("session_state") String sessionState, final @QueryParam("redirect_uri") String redirectUri) {
// todo do we care if anybody can trigger this?
- audit.event(Events.LOGOUT).detail(Details.REDIRECT_URI, redirectUri);
+ audit.event(Events.LOGOUT);
+ if (redirectUri != null) {
+ audit.detail(Details.REDIRECT_URI, redirectUri);
+ }
+ if (sessionState != null) {
+ audit.session(sessionState);
+ }
// authenticate identity cookie, but ignore an access token timeout as we're logging out anyways.
- UserModel user = authManager.authenticateIdentityCookie(realm, uriInfo, headers, false);
- if (user != null) {
- logger.infov("Logging out: {0}", user.getLoginName());
- authManager.expireIdentityCookie(realm, uriInfo);
- authManager.expireRememberMeCookie(realm, uriInfo);
- resourceAdminManager.logoutUser(uriInfo.getRequestUri(), realm, user);
+ AuthenticationManager.AuthResult authResult = authManager.authenticateIdentityCookie(realm, uriInfo, headers, false);
+ if (authResult != null) {
+ logout(authResult.getSession());
+ } else if (sessionState != null) {
+ UserSessionModel userSession = realm.getUserSession(sessionState);
+ if (userSession != null) {
+ logout(userSession);
+ } else {
+ audit.error(Errors.USER_SESSION_NOT_FOUND);
+ }
+ } else {
+ audit.error(Errors.USER_NOT_LOGGED_IN);
+ }
- audit.user(user).success();
+ if (redirectUri != null) {
+ // todo manage legal redirects
+ return Response.status(302).location(UriBuilder.fromUri(redirectUri).build()).build();
} else {
- logger.info("No user logged in for logout");
+ return Response.ok().build();
}
- // todo manage legal redirects
- return Response.status(302).location(UriBuilder.fromUri(redirectUri).build()).build();
+ }
+
+ private void logout(UserSessionModel session) {
+ UserModel user = session.getUser();
+
+ logger.infov("Logging out: {0} ({1})", user.getLoginName(), session.getId());
+
+ realm.removeUserSession(session);
+ authManager.expireIdentityCookie(realm, uriInfo);
+ authManager.expireRememberMeCookie(realm, uriInfo);
+
+ resourceAdminManager.logoutUser(uriInfo.getRequestUri(), realm, user.getId(), session.getId());
+
+ audit.user(user).session(session).success();
}
@Path("oauth/grant")
@@ -828,6 +914,13 @@ public class TokenService {
audit.detail(Details.REMEMBER_ME, "true");
}
+ UserSessionModel session = realm.getUserSession(accessCodeEntry.getSessionState());
+ if (session == null || session.getExpires() < Time.currentTime()) {
+ audit.error(Errors.INVALID_CODE);
+ return oauth.forwardToSecurityFailure("Session not active");
+ }
+ audit.session(session);
+
if (formData.containsKey("cancel")) {
audit.error(Errors.REJECTED_BY_USER);
return redirectAccessDenied(redirect, state);
@@ -836,7 +929,7 @@ public class TokenService {
audit.success();
accessCodeEntry.setExpiration(Time.currentTime() + realm.getAccessCodeLifespan());
- return oauth.redirectAccessCode(accessCodeEntry, state, redirect);
+ return oauth.redirectAccessCode(accessCodeEntry, session, state, redirect);
}
protected Response redirectAccessDenied(String redirect, String state) {
diff --git a/services/src/main/java/org/keycloak/services/scheduled/ClearExpiredAuditEvents.java b/services/src/main/java/org/keycloak/services/scheduled/ClearExpiredAuditEvents.java
new file mode 100644
index 0000000..c12ea04
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/scheduled/ClearExpiredAuditEvents.java
@@ -0,0 +1,26 @@
+package org.keycloak.services.scheduled;
+
+import org.keycloak.audit.AuditProvider;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.provider.ProviderSession;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class ClearExpiredAuditEvents implements ScheduledTask {
+
+ @Override
+ public void run(KeycloakSession keycloakSession, ProviderSession providerSession) {
+ AuditProvider audit = providerSession.getProvider(AuditProvider.class);
+ if (audit != null) {
+ for (RealmModel realm : keycloakSession.getRealms()) {
+ if (realm.isAuditEnabled() && realm.getAuditExpiration() > 0) {
+ long olderThan = System.currentTimeMillis() - realm.getAuditExpiration() * 1000;
+ audit.clear(realm.getId(), olderThan);
+ }
+ }
+ }
+ }
+
+}
diff --git a/services/src/main/java/org/keycloak/services/scheduled/ClearExpiredUserSessions.java b/services/src/main/java/org/keycloak/services/scheduled/ClearExpiredUserSessions.java
new file mode 100644
index 0000000..cd770b6
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/scheduled/ClearExpiredUserSessions.java
@@ -0,0 +1,19 @@
+package org.keycloak.services.scheduled;
+
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.provider.ProviderSession;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class ClearExpiredUserSessions implements ScheduledTask {
+
+ @Override
+ public void run(KeycloakSession keycloakSession, ProviderSession providerSession) {
+ for (RealmModel realm : keycloakSession.getRealms()) {
+ realm.removeExpiredUserSessions();
+ }
+ }
+
+}
diff --git a/services/src/main/java/org/keycloak/services/scheduled/ScheduledTask.java b/services/src/main/java/org/keycloak/services/scheduled/ScheduledTask.java
new file mode 100644
index 0000000..e47f8b9
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/scheduled/ScheduledTask.java
@@ -0,0 +1,13 @@
+package org.keycloak.services.scheduled;
+
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.provider.ProviderSession;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public interface ScheduledTask {
+
+ public void run(KeycloakSession keycloakSession, ProviderSession providerSession);
+
+}
diff --git a/services/src/main/java/org/keycloak/services/scheduled/ScheduledTaskRunner.java b/services/src/main/java/org/keycloak/services/scheduled/ScheduledTaskRunner.java
new file mode 100644
index 0000000..1caa290
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/scheduled/ScheduledTaskRunner.java
@@ -0,0 +1,54 @@
+package org.keycloak.services.scheduled;
+
+import org.jboss.resteasy.logging.Logger;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.provider.ProviderSession;
+import org.keycloak.provider.ProviderSessionFactory;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class ScheduledTaskRunner implements Runnable {
+
+ private static final Logger logger = Logger.getLogger(ScheduledTaskRunner.class);
+
+ private final KeycloakSessionFactory keycloakSessionFactory;
+ private final ProviderSessionFactory providerSessionFactory;
+ private final ScheduledTask task;
+
+ public ScheduledTaskRunner(KeycloakSessionFactory keycloakSessionFactory, ProviderSessionFactory providerSessionFactory, ScheduledTask task) {
+ this.keycloakSessionFactory = keycloakSessionFactory;
+ this.providerSessionFactory = providerSessionFactory;
+ this.task = task;
+ }
+
+ @Override
+ public void run() {
+ KeycloakSession keycloakSession = keycloakSessionFactory.createSession();
+ ProviderSession providerSession = providerSessionFactory.createSession();
+ try {
+ keycloakSession.getTransaction().begin();
+ task.run(keycloakSession, providerSession);
+ keycloakSession.getTransaction().commit();
+
+ logger.debug("Executed scheduled task " + task.getClass().getSimpleName());
+ } catch (Throwable t) {
+ logger.error("Failed to run scheduled task " + task.getClass().getSimpleName(), t);
+
+ keycloakSession.getTransaction().rollback();
+ } finally {
+ try {
+ keycloakSession.close();
+ } catch (Throwable t) {
+ logger.error("Failed to close KeycloakSession", t);
+ }
+ try {
+ providerSession.close();
+ } catch (Throwable t) {
+ logger.error("Failed to close ProviderSession", t);
+ }
+ }
+ }
+
+}
diff --git a/testsuite/integration/src/main/resources/META-INF/persistence.xml b/testsuite/integration/src/main/resources/META-INF/persistence.xml
index 1d2f21a..a5a7849 100755
--- a/testsuite/integration/src/main/resources/META-INF/persistence.xml
+++ b/testsuite/integration/src/main/resources/META-INF/persistence.xml
@@ -16,6 +16,7 @@
<class>org.keycloak.models.jpa.entities.SocialLinkEntity</class>
<class>org.keycloak.models.jpa.entities.AuthenticationLinkEntity</class>
<class>org.keycloak.models.jpa.entities.UserEntity</class>
+ <class>org.keycloak.models.jpa.entities.UserSessionEntity</class>
<class>org.keycloak.models.jpa.entities.UsernameLoginFailureEntity</class>
<class>org.keycloak.models.jpa.entities.UserRoleMappingEntity</class>
<class>org.keycloak.models.jpa.entities.ScopeMappingEntity</class>
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java
index 955474e..d3c5859 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java
@@ -194,7 +194,7 @@ public class AccountTest {
changePasswordPage.open();
loginPage.login("test-user@localhost", "password");
- events.expectLogin().client("account").detail(Details.REDIRECT_URI, ACCOUNT_REDIRECT + "?path=password").assertEvent();
+ String sessionId = events.expectLogin().client("account").detail(Details.REDIRECT_URI, ACCOUNT_REDIRECT + "?path=password").assertEvent().getSessionId();
changePasswordPage.changePassword("", "new-password", "new-password");
@@ -212,14 +212,14 @@ public class AccountTest {
changePasswordPage.logout();
- events.expectLogout().detail(Details.REDIRECT_URI, ACCOUNT_URL).assertEvent();
+ events.expectLogout(sessionId).detail(Details.REDIRECT_URI, ACCOUNT_URL).assertEvent();
loginPage.open();
loginPage.login("test-user@localhost", "password");
Assert.assertEquals("Invalid username or password.", loginPage.getError());
- events.expectLogin().user((String) null).error("invalid_user_credentials").removeDetail(Details.CODE_ID).assertEvent();
+ events.expectLogin().user((String) null).session((String) null).error("invalid_user_credentials").removeDetail(Details.CODE_ID).assertEvent();
loginPage.open();
loginPage.login("test-user@localhost", "new-password");
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java
index db4c3ea..974ae88 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java
@@ -121,6 +121,7 @@ public class RequiredActionEmailVerificationTest {
String verificationUrl = m.group(1);
Event sendEvent = events.expectRequiredAction("send_verify_email").detail("email", "test-user@localhost").assertEvent();
+ String sessionId = sendEvent.getSessionId();
String mailCodeId = sendEvent.getDetails().get(Details.CODE_ID);
@@ -128,11 +129,11 @@ public class RequiredActionEmailVerificationTest {
driver.navigate().to(verificationUrl.trim());
- events.expectRequiredAction("verify_email").detail("email", "test-user@localhost").detail(Details.CODE_ID, mailCodeId).assertEvent();
+ events.expectRequiredAction("verify_email").session(sessionId).detail("email", "test-user@localhost").detail(Details.CODE_ID, mailCodeId).assertEvent();
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
- events.expectLogin().detail(Details.CODE_ID, mailCodeId).assertEvent();
+ events.expectLogin().session(sessionId).detail(Details.CODE_ID, mailCodeId).assertEvent();
}
@Test
@@ -156,6 +157,7 @@ public class RequiredActionEmailVerificationTest {
m.matches();
Event sendEvent = events.expectRequiredAction("send_verify_email").user(userId).detail("username", "verifyEmail").detail("email", "email").assertEvent();
+ String sessionId = sendEvent.getSessionId();
String mailCodeId = sendEvent.getDetails().get(Details.CODE_ID);
@@ -165,9 +167,9 @@ public class RequiredActionEmailVerificationTest {
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
- events.expectRequiredAction("verify_email").user(userId).detail("username", "verifyEmail").detail("email", "email").detail(Details.CODE_ID, mailCodeId).assertEvent();
+ events.expectRequiredAction("verify_email").user(userId).session(sessionId).detail("username", "verifyEmail").detail("email", "email").detail(Details.CODE_ID, mailCodeId).assertEvent();
- events.expectLogin().user(userId).detail("username", "verifyEmail").detail(Details.CODE_ID, mailCodeId).assertEvent();
+ events.expectLogin().user(userId).session(sessionId).detail("username", "verifyEmail").detail(Details.CODE_ID, mailCodeId).assertEvent();
}
@Test
@@ -180,6 +182,7 @@ public class RequiredActionEmailVerificationTest {
Assert.assertEquals(1, greenMail.getReceivedMessages().length);
Event sendEvent = events.expectRequiredAction("send_verify_email").detail("email", "test-user@localhost").assertEvent();
+ String sessionId = sendEvent.getSessionId();
String mailCodeId = sendEvent.getDetails().get(Details.CODE_ID);
@@ -195,7 +198,7 @@ public class RequiredActionEmailVerificationTest {
Matcher m = p.matcher(body);
m.matches();
- events.expectRequiredAction("send_verify_email").detail("email", "test-user@localhost").assertEvent(sendEvent);
+ events.expectRequiredAction("send_verify_email").session(sessionId).detail("email", "test-user@localhost").assertEvent(sendEvent);
String verificationUrl = m.group(1);
@@ -203,9 +206,9 @@ public class RequiredActionEmailVerificationTest {
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
- events.expectRequiredAction("verify_email").detail("email", "test-user@localhost").detail(Details.CODE_ID, mailCodeId).assertEvent();
+ events.expectRequiredAction("verify_email").session(sessionId).detail("email", "test-user@localhost").detail(Details.CODE_ID, mailCodeId).assertEvent();
- events.expectLogin().assertEvent();
+ events.expectLogin().session(sessionId).assertEvent();
}
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionMultipleActionsTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionMultipleActionsTest.java
index c2044ef..12a58b2 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionMultipleActionsTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionMultipleActionsTest.java
@@ -89,36 +89,46 @@ public class RequiredActionMultipleActionsTest {
loginPage.open();
loginPage.login("test-user@localhost", "password");
+ String sessionId = null;
if (changePasswordPage.isCurrent()) {
- updatePassword();
+ sessionId = updatePassword(sessionId);
updateProfilePage.assertCurrent();
- updateProfile();
+ updateProfile(sessionId);
} else if (updateProfilePage.isCurrent()) {
- updateProfile();
+ sessionId = updateProfile(sessionId);
changePasswordPage.assertCurrent();
- updatePassword();
+ updatePassword(sessionId);
} else {
Assert.fail("Expected to update password and profile before login");
}
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
- events.expectLogin().assertEvent();
+ events.expectLogin().session(sessionId).assertEvent();
}
- public void updatePassword() {
+ public String updatePassword(String sessionId) {
changePasswordPage.changePassword("new-password", "new-password");
- events.expectRequiredAction("update_password").assertEvent();
+ AssertEvents.ExpectedEvent expectedEvent = events.expectRequiredAction("update_password");
+ if (sessionId != null) {
+ expectedEvent.session(sessionId);
+ }
+ return expectedEvent.assertEvent().getSessionId();
}
- public void updateProfile() {
+ public String updateProfile(String sessionId) {
updateProfilePage.update("New first", "New last", "new@email.com");
- events.expectRequiredAction("update_profile").assertEvent();
- events.expectRequiredAction("update_email").detail(Details.PREVIOUS_EMAIL, "test-user@localhost").detail(Details.UPDATED_EMAIL, "new@email.com").assertEvent();
+ AssertEvents.ExpectedEvent expectedEvent = events.expectRequiredAction("update_profile");
+ if (sessionId != null) {
+ expectedEvent.session(sessionId);
+ }
+ sessionId = expectedEvent.assertEvent().getSessionId();
+ events.expectRequiredAction("update_email").session(sessionId).detail(Details.PREVIOUS_EMAIL, "test-user@localhost").detail(Details.UPDATED_EMAIL, "new@email.com").assertEvent();
+ return sessionId;
}
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionResetPasswordTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionResetPasswordTest.java
index 00fe91e..ad5f74b 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionResetPasswordTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionResetPasswordTest.java
@@ -25,6 +25,7 @@ import org.junit.Assert;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
+import org.keycloak.audit.Event;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserModel.RequiredAction;
@@ -92,15 +93,15 @@ public class RequiredActionResetPasswordTest {
changePasswordPage.assertCurrent();
changePasswordPage.changePassword("new-password", "new-password");
- events.expectRequiredAction("update_password").assertEvent();
+ String sessionId = events.expectRequiredAction("update_password").assertEvent().getSessionId();
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
- events.expectLogin().assertEvent();
+ Event loginEvent = events.expectLogin().session(sessionId).assertEvent();
oauth.openLogout();
- events.expectLogout().assertEvent();
+ events.expectLogout(loginEvent.getSessionId()).assertEvent();
loginPage.open();
loginPage.login("test-user@localhost", "new-password");
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionTotpSetupTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionTotpSetupTest.java
index 43bec78..f69aef9 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionTotpSetupTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionTotpSetupTest.java
@@ -26,6 +26,7 @@ import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.audit.Details;
+import org.keycloak.audit.Event;
import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.TimeBasedOTP;
import org.keycloak.representations.idm.CredentialRepresentation;
@@ -108,11 +109,11 @@ public class RequiredActionTotpSetupTest {
totpPage.configure(totp.generate(totpPage.getTotpSecret()));
- events.expectRequiredAction("update_totp").user(userId).detail(Details.USERNAME, "setupTotp").assertEvent();
+ String sessionId = events.expectRequiredAction("update_totp").user(userId).detail(Details.USERNAME, "setupTotp").assertEvent().getSessionId();
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
- events.expectLogin().user(userId).detail(Details.USERNAME, "setupTotp").assertEvent();
+ events.expectLogin().user(userId).session(sessionId).detail(Details.USERNAME, "setupTotp").assertEvent();
}
@Test
@@ -126,15 +127,15 @@ public class RequiredActionTotpSetupTest {
totpPage.configure(totp.generate(totpSecret));
- events.expectRequiredAction("update_totp").assertEvent();
+ String sessionId = events.expectRequiredAction("update_totp").assertEvent().getSessionId();
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
- events.expectLogin().assertEvent();
+ Event loginEvent = events.expectLogin().session(sessionId).assertEvent();
oauth.openLogout();
- events.expectLogout().assertEvent();
+ events.expectLogout(loginEvent.getSessionId()).assertEvent();
loginPage.open();
loginPage.login("test-user@localhost", "password");
@@ -165,11 +166,11 @@ public class RequiredActionTotpSetupTest {
events.expectRequiredAction("update_totp").user(userId).detail(Details.USERNAME, "setupTotp2").assertEvent();
- events.expectLogin().user(userId).detail(Details.USERNAME, "setupTotp2").assertEvent();
+ Event loginEvent = events.expectLogin().user(userId).detail(Details.USERNAME, "setupTotp2").assertEvent();
// Logout
oauth.openLogout();
- events.expectLogout().user(userId).assertEvent();
+ events.expectLogout(loginEvent.getSessionId()).user(userId).assertEvent();
// Try to login after logout
loginPage.open();
@@ -182,7 +183,7 @@ public class RequiredActionTotpSetupTest {
// Login with one-time password
loginTotpPage.login(totp.generate(totpCode));
- events.expectLogin().user(userId).detail(Details.USERNAME, "setupTotp2").assertEvent();
+ loginEvent = events.expectLogin().user(userId).detail(Details.USERNAME, "setupTotp2").assertEvent();
// Open account page
accountTotpPage.open();
@@ -195,7 +196,7 @@ public class RequiredActionTotpSetupTest {
// Logout
oauth.openLogout();
- events.expectLogout().user(userId).assertEvent();
+ events.expectLogout(loginEvent.getSessionId()).user(userId).assertEvent();
// Try to login
loginPage.open();
@@ -205,11 +206,11 @@ public class RequiredActionTotpSetupTest {
totpPage.assertCurrent();
totpPage.configure(totp.generate(totpPage.getTotpSecret()));
- events.expectRequiredAction("update_totp").user(userId).detail(Details.USERNAME, "setupTotp2").assertEvent();
+ String sessionId = events.expectRequiredAction("update_totp").user(userId).detail(Details.USERNAME, "setupTotp2").assertEvent().getSessionId();
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
- events.expectLogin().user(userId).detail(Details.USERNAME, "setupTotp2").assertEvent();
+ events.expectLogin().user(userId).session(sessionId).detail(Details.USERNAME, "setupTotp2").assertEvent();
}
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java
index 3c317b1..373b487 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java
@@ -87,12 +87,12 @@ public class RequiredActionUpdateProfileTest {
updateProfilePage.update("New first", "New last", "new@email.com");
- events.expectRequiredAction("update_profile").assertEvent();
- events.expectRequiredAction("update_email").detail(Details.PREVIOUS_EMAIL, "test-user@localhost").detail(Details.UPDATED_EMAIL, "new@email.com").assertEvent();
+ String sessionId = events.expectRequiredAction("update_profile").assertEvent().getSessionId();
+ events.expectRequiredAction("update_email").session(sessionId).detail(Details.PREVIOUS_EMAIL, "test-user@localhost").detail(Details.UPDATED_EMAIL, "new@email.com").assertEvent();
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
- events.expectLogin().assertEvent();
+ events.expectLogin().session(sessionId).assertEvent();
}
@Test
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTest.java
index 0e93bd8..46c7bb2 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTest.java
@@ -30,6 +30,7 @@ import org.keycloak.models.ApplicationModel;
import org.keycloak.models.Constants;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
+import org.keycloak.models.UserSessionModel;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.adapters.action.SessionStats;
import org.keycloak.representations.idm.RealmRepresentation;
@@ -83,7 +84,8 @@ public class AdapterTest {
ApplicationModel adminConsole = adminRealm.getApplicationByName(Constants.ADMIN_CONSOLE_APPLICATION);
TokenManager tm = new TokenManager();
UserModel admin = adminRealm.getUser("admin");
- AccessToken token = tm.createClientAccessToken(null, adminRealm, adminConsole, admin);
+ UserSessionModel session = adminRealm.createUserSession(admin, null);
+ AccessToken token = tm.createClientAccessToken(null, adminRealm, adminConsole, admin, session);
adminToken = tm.encodeToken(adminRealm, token);
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/RelativeUriAdapterTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/RelativeUriAdapterTest.java
index 75e8a23..96af4c1 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/RelativeUriAdapterTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/RelativeUriAdapterTest.java
@@ -30,6 +30,7 @@ import org.keycloak.models.ApplicationModel;
import org.keycloak.models.Constants;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
+import org.keycloak.models.UserSessionModel;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.adapters.action.SessionStats;
import org.keycloak.representations.idm.RealmRepresentation;
@@ -85,7 +86,8 @@ public class RelativeUriAdapterTest {
ApplicationModel adminConsole = adminRealm.getApplicationByName(Constants.ADMIN_CONSOLE_APPLICATION);
TokenManager tm = new TokenManager();
UserModel admin = adminRealm.getUser("admin");
- AccessToken token = tm.createClientAccessToken(null, adminRealm, adminConsole, admin);
+ UserSessionModel session = adminRealm.createUserSession(admin, null);
+ AccessToken token = tm.createClientAccessToken(null, adminRealm, adminConsole, admin, session);
adminToken = tm.encodeToken(adminRealm, token);
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/AssertEvents.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/AssertEvents.java
index e94b4a9..a57aff1 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/AssertEvents.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/AssertEvents.java
@@ -15,6 +15,7 @@ import org.keycloak.audit.Event;
import org.keycloak.models.ClientModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
+import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.provider.ProviderSession;
import org.keycloak.representations.idm.UserRepresentation;
@@ -115,7 +116,7 @@ public class AssertEvents implements TestRule, AuditListenerFactory {
}
public ExpectedEvent expectRequiredAction(String event) {
- return expectLogin().event(event);
+ return expectLogin().event(event).session(isUUID());
}
public ExpectedEvent expectLogin() {
@@ -124,26 +125,30 @@ public class AssertEvents implements TestRule, AuditListenerFactory {
.detail(Details.USERNAME, DEFAULT_USERNAME)
.detail(Details.RESPONSE_TYPE, "code")
.detail(Details.AUTH_METHOD, "form")
- .detail(Details.REDIRECT_URI, DEFAULT_REDIRECT_URI);
+ .detail(Details.REDIRECT_URI, DEFAULT_REDIRECT_URI)
+ .session(isUUID());
}
- public ExpectedEvent expectCodeToToken(String codeId) {
+ public ExpectedEvent expectCodeToToken(String codeId, String sessionId) {
return expect("code_to_token")
.detail(Details.CODE_ID, codeId)
.detail(Details.TOKEN_ID, isUUID())
- .detail(Details.REFRESH_TOKEN_ID, isUUID());
+ .detail(Details.REFRESH_TOKEN_ID, isUUID())
+ .session(sessionId);
}
- public ExpectedEvent expectRefresh(String refreshTokenId) {
+ public ExpectedEvent expectRefresh(String refreshTokenId, String sessionId) {
return expect("refresh_token")
.detail(Details.TOKEN_ID, isUUID())
.detail(Details.REFRESH_TOKEN_ID, refreshTokenId)
- .detail(Details.UPDATED_REFRESH_TOKEN_ID, isUUID());
+ .detail(Details.UPDATED_REFRESH_TOKEN_ID, isUUID())
+ .session(sessionId);
}
- public ExpectedEvent expectLogout() {
+ public ExpectedEvent expectLogout(String sessionId) {
return expect("logout").client((String) null)
- .detail(Details.REDIRECT_URI, DEFAULT_REDIRECT_URI);
+ .detail(Details.REDIRECT_URI, DEFAULT_REDIRECT_URI)
+ .session(sessionId);
}
public ExpectedEvent expectRegister(String username, String email) {
@@ -162,7 +167,13 @@ public class AssertEvents implements TestRule, AuditListenerFactory {
}
public ExpectedEvent expect(String event) {
- return new ExpectedEvent().realm(DEFAULT_REALM).client(DEFAULT_CLIENT_ID).user(keycloak.getUser(DEFAULT_REALM, DEFAULT_USERNAME).getId()).ipAddress(DEFAULT_IP_ADDRESS).event(event);
+ return new ExpectedEvent()
+ .realm(DEFAULT_REALM)
+ .client(DEFAULT_CLIENT_ID)
+ .user(keycloak.getUser(DEFAULT_REALM, DEFAULT_USERNAME).getId())
+ .ipAddress(DEFAULT_IP_ADDRESS)
+ .session((String) null)
+ .event(event);
}
@Override
@@ -193,6 +204,7 @@ public class AssertEvents implements TestRule, AuditListenerFactory {
public static class ExpectedEvent {
private Event expected = new Event();
private Matcher<String> userId;
+ private Matcher<String> sessionId;
private HashMap<String, Matcher<String>> details;
public ExpectedEvent realm(RealmModel realm) {
@@ -216,7 +228,7 @@ public class AssertEvents implements TestRule, AuditListenerFactory {
}
public ExpectedEvent user(UserModel user) {
- return user(CoreMatchers.equalTo(user.getId()));
+ return user(user.getId());
}
public ExpectedEvent user(String userId) {
@@ -228,6 +240,19 @@ public class AssertEvents implements TestRule, AuditListenerFactory {
return this;
}
+ public ExpectedEvent session(UserSessionModel session) {
+ return session(session.getId());
+ }
+
+ public ExpectedEvent session(String sessionId) {
+ return session(CoreMatchers.equalTo(sessionId));
+ }
+
+ public ExpectedEvent session(Matcher<String> sessionId) {
+ this.sessionId = sessionId;
+ return this;
+ }
+
public ExpectedEvent ipAddress(String ipAddress) {
expected.setIpAddress(ipAddress);
return this;
@@ -277,8 +302,9 @@ public class AssertEvents implements TestRule, AuditListenerFactory {
Assert.assertEquals(expected.getError(), actual.getError());
Assert.assertEquals(expected.getIpAddress(), actual.getIpAddress());
Assert.assertThat(actual.getUserId(), userId);
+ Assert.assertThat(actual.getSessionId(), sessionId);
- if (details == null) {
+ if (details == null || details.isEmpty()) {
Assert.assertNull(actual.getDetails());
} else {
Assert.assertNotNull(actual.getDetails());
@@ -288,9 +314,7 @@ public class AssertEvents implements TestRule, AuditListenerFactory {
Assert.fail(d.getKey() + " missing");
}
- if (!d.getValue().matches(actualValue)) {
- Assert.fail(d.getKey() + " doesn't match");
- }
+ Assert.assertThat("Unexpected value for " + d.getKey(), actualValue, d.getValue());
}
for (String k : actual.getDetails().keySet()) {
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTest.java
index b4dc34c..aa51050 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTest.java
@@ -94,7 +94,7 @@ public class LoginTest {
Assert.assertEquals("Invalid username or password.", loginPage.getError());
- events.expectLogin().user((String) null).error("invalid_user_credentials").detail(Details.USERNAME, "login-test").removeDetail(Details.CODE_ID).assertEvent();
+ events.expectLogin().user((String) null).session((String) null).error("invalid_user_credentials").detail(Details.USERNAME, "login-test").removeDetail(Details.CODE_ID).assertEvent();
}
@Test
@@ -106,7 +106,7 @@ public class LoginTest {
Assert.assertEquals("Invalid username or password.", loginPage.getError());
- events.expectLogin().user((String) null).error("user_not_found").detail(Details.USERNAME, "invalid").removeDetail(Details.CODE_ID).assertEvent();
+ events.expectLogin().user((String) null).session((String) null).error("user_not_found").detail(Details.USERNAME, "invalid").removeDetail(Details.CODE_ID).assertEvent();
}
@Test
@@ -139,7 +139,7 @@ public class LoginTest {
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
Assert.assertEquals("access_denied", oauth.getCurrentQuery().get(OAuth2Constants.ERROR));
- events.expectLogin().error("rejected_by_user").user((String) null).removeDetail(Details.USERNAME).removeDetail(Details.CODE_ID).assertEvent();
+ events.expectLogin().error("rejected_by_user").user((String) null).session((String) null).removeDetail(Details.USERNAME).removeDetail(Details.CODE_ID).assertEvent();
}
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTotpTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTotpTest.java
index 783f07a..9502251 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTotpTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTotpTest.java
@@ -111,7 +111,7 @@ public class LoginTotpTest {
loginPage.assertCurrent();
Assert.assertEquals("Invalid username or password.", loginPage.getError());
- events.expectLogin().error("invalid_user_credentials").removeDetail(Details.CODE_ID).user((String) null).assertEvent();
+ events.expectLogin().error("invalid_user_credentials").removeDetail(Details.CODE_ID).user((String) null).session((String) null).assertEvent();
}
@Test
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LogoutTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LogoutTest.java
new file mode 100644
index 0000000..4ec9193
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LogoutTest.java
@@ -0,0 +1,175 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2012, Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.keycloak.testsuite.forms;
+
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.keycloak.audit.Details;
+import org.keycloak.testsuite.AssertEvents;
+import org.keycloak.testsuite.OAuthClient;
+import org.keycloak.testsuite.pages.AppPage;
+import org.keycloak.testsuite.pages.LoginPage;
+import org.keycloak.testsuite.rule.KeycloakRule;
+import org.keycloak.testsuite.rule.WebResource;
+import org.keycloak.testsuite.rule.WebRule;
+import org.openqa.selenium.Cookie;
+import org.openqa.selenium.WebDriver;
+
+import java.io.IOException;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class LogoutTest {
+
+ @ClassRule
+ public static KeycloakRule keycloakRule = new KeycloakRule();
+
+ @Rule
+ public AssertEvents events = new AssertEvents(keycloakRule);
+
+ @Rule
+ public WebRule webRule = new WebRule(this);
+
+ @WebResource
+ protected OAuthClient oauth;
+
+ @WebResource
+ protected WebDriver driver;
+
+ @WebResource
+ protected AppPage appPage;
+
+ @WebResource
+ protected LoginPage loginPage;
+
+ @Test
+ public void logoutRedirect() {
+ loginPage.open();
+ loginPage.login("test-user@localhost", "password");
+ assertTrue(appPage.isCurrent());
+
+ String sessionId = events.expectLogin().assertEvent().getSessionId();
+
+ String redirectUri = AppPage.baseUrl + "?logout";
+
+ String logoutUrl = oauth.getLogoutUrl(redirectUri, null);
+ driver.navigate().to(logoutUrl);
+
+ events.expectLogout(sessionId).detail(Details.REDIRECT_URI, redirectUri).assertEvent();
+
+ assertEquals(redirectUri, driver.getCurrentUrl());
+
+ loginPage.open();
+ loginPage.login("test-user@localhost", "password");
+ assertTrue(appPage.isCurrent());
+
+ String sessionId2 = events.expectLogin().assertEvent().getSessionId();
+ assertNotEquals(sessionId, sessionId2);
+ }
+
+ @Test
+ public void logoutSession() {
+ loginPage.open();
+ loginPage.login("test-user@localhost", "password");
+ assertTrue(appPage.isCurrent());
+
+ String sessionId = events.expectLogin().assertEvent().getSessionId();
+
+ String logoutUrl = oauth.getLogoutUrl(null, sessionId);
+ driver.navigate().to(logoutUrl);
+
+ events.expectLogout(sessionId).removeDetail(Details.REDIRECT_URI).assertEvent();
+
+ assertEquals(logoutUrl, driver.getCurrentUrl());
+
+ loginPage.open();
+ loginPage.login("test-user@localhost", "password");
+ assertTrue(appPage.isCurrent());
+
+ String sessionId2 = events.expectLogin().assertEvent().getSessionId();
+ assertNotEquals(sessionId, sessionId2);
+ }
+
+ @Test
+ public void logoutMultipleSessions() throws IOException {
+ // Login session 1
+ loginPage.open();
+ loginPage.login("test-user@localhost", "password");
+ assertTrue(appPage.isCurrent());
+
+ String sessionId = events.expectLogin().assertEvent().getSessionId();
+
+ // Login session 2
+ WebDriver driver2 = WebRule.createWebDriver();
+
+ OAuthClient oauth2 = new OAuthClient(driver2);
+ oauth2.doLogin("test-user@localhost", "password");
+
+ String sessionId2 = events.expectLogin().assertEvent().getSessionId();
+ assertNotEquals(sessionId, sessionId2);
+
+ // Check session 1 logged-in
+ oauth.openLoginForm();
+ events.expectLogin().session(sessionId).detail(Details.AUTH_METHOD, "sso").removeDetail(Details.USERNAME).assertEvent();
+
+ // Check session 2 logged-in
+ oauth2.openLoginForm();
+ events.expectLogin().session(sessionId2).detail(Details.AUTH_METHOD, "sso").removeDetail(Details.USERNAME).assertEvent();
+
+ // Logout session 1 by redirect
+ driver.navigate().to(oauth.getLogoutUrl(AppPage.baseUrl, null));
+ events.expectLogout(sessionId).detail(Details.REDIRECT_URI, AppPage.baseUrl).assertEvent();
+
+ // Check session 2 logged-in
+ oauth2.openLoginForm();
+ events.expectLogin().session(sessionId2).detail(Details.AUTH_METHOD, "sso").removeDetail(Details.USERNAME).assertEvent();
+
+ // Check session 1 not logged-in
+ oauth.openLoginForm();
+ assertEquals(oauth.getLoginFormUrl(), driver.getCurrentUrl());
+
+ // Login session 3
+ oauth.doLogin("test-user@localhost", "password");
+ String sessionId3 = events.expectLogin().assertEvent().getSessionId();
+ assertNotEquals(sessionId, sessionId3);
+ assertNotEquals(sessionId2, sessionId3);
+
+ // Logout session 2 by session_state
+ oauth2.doLogout(null, sessionId2);
+ events.expectLogout(sessionId2).removeDetail(Details.REDIRECT_URI).assertEvent();
+
+ // Check session 3 logged-in
+ oauth.openLoginForm();
+ events.expectLogin().session(sessionId3).detail(Details.AUTH_METHOD, "sso").removeDetail(Details.USERNAME).assertEvent();
+
+ // Check session 2 not logged-in
+ oauth2.openLoginForm();
+ assertEquals(oauth2.getLoginFormUrl(), driver2.getCurrentUrl());
+ }
+
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java
index 0863c1a..e2259f9 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java
@@ -26,6 +26,7 @@ import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.audit.Details;
+import org.keycloak.audit.Event;
import org.keycloak.models.PasswordPolicy;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserCredentialModel;
@@ -123,7 +124,7 @@ public class ResetPasswordTest {
resetPasswordPage.assertCurrent();
- events.expectRequiredAction("send_reset_password").user(userId).detail(Details.USERNAME, username).detail(Details.EMAIL, "login@test.com").assertEvent();
+ String sessionId = events.expectRequiredAction("send_reset_password").user(userId).detail(Details.USERNAME, username).detail(Details.EMAIL, "login@test.com").assertEvent().getSessionId();
Assert.assertEquals("You should receive an email shortly with further instructions.", resetPasswordPage.getSuccessMessage());
@@ -140,15 +141,15 @@ public class ResetPasswordTest {
updatePasswordPage.changePassword("resetPassword", "resetPassword");
- events.expectRequiredAction("update_password").user(userId).detail(Details.USERNAME, username).assertEvent();
+ events.expectRequiredAction("update_password").user(userId).session(sessionId).detail(Details.USERNAME, username).assertEvent();
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
- events.expectLogin().user(userId).detail(Details.USERNAME, username).assertEvent();
+ Event loginEvent = events.expectLogin().user(userId).session(sessionId).detail(Details.USERNAME, username).assertEvent();
oauth.openLogout();
- events.expectLogout().user(userId).assertEvent();
+ events.expectLogout(loginEvent.getSessionId()).user(userId).session(sessionId).assertEvent();
loginPage.open();
@@ -176,7 +177,7 @@ public class ResetPasswordTest {
Assert.assertEquals(0, greenMail.getReceivedMessages().length);
- events.expectRequiredAction("send_reset_password").user((String) null).detail(Details.USERNAME, "invalid").removeDetail(Details.EMAIL).removeDetail(Details.CODE_ID).error("user_not_found").assertEvent();
+ events.expectRequiredAction("send_reset_password").user((String) null).session((String) null).detail(Details.USERNAME, "invalid").removeDetail(Details.EMAIL).removeDetail(Details.CODE_ID).error("user_not_found").assertEvent();
}
@Test
@@ -206,7 +207,7 @@ public class ResetPasswordTest {
String body = (String) message.getContent();
String changePasswordUrl = body.split("\n")[3];
- events.expectRequiredAction("send_reset_password").user(userId).detail(Details.USERNAME, "login-test").detail(Details.EMAIL, "login@test.com").assertEvent();
+ String sessionId = events.expectRequiredAction("send_reset_password").user(userId).detail(Details.USERNAME, "login-test").detail(Details.EMAIL, "login@test.com").assertEvent().getSessionId();
driver.navigate().to(changePasswordUrl.trim());
@@ -218,15 +219,15 @@ public class ResetPasswordTest {
updatePasswordPage.changePassword("resetPasswordWithPasswordPolicy", "resetPasswordWithPasswordPolicy");
- events.expectRequiredAction("update_password").user(userId).detail(Details.USERNAME, "login-test").assertEvent();
+ events.expectRequiredAction("update_password").user(userId).session(sessionId).detail(Details.USERNAME, "login-test").assertEvent();
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
- events.expectLogin().user(userId).detail(Details.USERNAME, "login-test").assertEvent();
+ Event loginEvent = events.expectLogin().user(userId).session(sessionId).detail(Details.USERNAME, "login-test").assertEvent();
oauth.openLogout();
- events.expectLogout().user(userId).assertEvent();
+ events.expectLogout(loginEvent.getSessionId()).user(userId).session(sessionId).assertEvent();
loginPage.open();
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/SSOTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/SSOTest.java
index 3dc81fc..16c9d36 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/SSOTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/SSOTest.java
@@ -41,6 +41,9 @@ import org.openqa.selenium.WebDriver;
import javax.ws.rs.core.UriBuilder;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@@ -76,22 +79,34 @@ public class SSOTest {
loginPage.open();
loginPage.login("test-user@localhost", "password");
- Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
+ assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
- events.expectLogin().assertEvent();
+ String sessionId = events.expectLogin().assertEvent().getSessionId();
appPage.open();
oauth.openLoginForm();
- Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
+ assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
profilePage.open();
Assert.assertTrue(profilePage.isCurrent());
- events.expectLogin().detail(Details.AUTH_METHOD, "sso").removeDetail(Details.USERNAME).client("test-app").assertEvent();
+ String sessionId2 = events.expectLogin().detail(Details.AUTH_METHOD, "sso").removeDetail(Details.USERNAME).client("test-app").assertEvent().getSessionId();
+
+ assertEquals(sessionId, sessionId2);
+
+ // Expire session
+ keycloakRule.removeUserSession(sessionId);
+
+ oauth.doLogin("test-user@localhost", "password");
+
+ String sessionId4 = events.expectLogin().assertEvent().getSessionId();
+ assertNotEquals(sessionId, sessionId4);
+
+ events.clear();
}
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java
index 4829497..a8ae436 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java
@@ -27,6 +27,7 @@ import org.junit.Rule;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.audit.Details;
+import org.keycloak.audit.Errors;
import org.keycloak.audit.Event;
import org.keycloak.representations.AccessToken;
import org.keycloak.testsuite.AssertEvents;
@@ -41,6 +42,8 @@ import org.openqa.selenium.WebDriver;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.lessThanOrEqualTo;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -69,7 +72,10 @@ public class AccessTokenTest {
public void accessTokenRequest() throws Exception {
oauth.doLogin("test-user@localhost", "password");
- String codeId = events.expectLogin().assertEvent().getDetails().get(Details.CODE_ID);
+ Event loginEvent = events.expectLogin().assertEvent();
+
+ String sessionId = loginEvent.getSessionId();
+ String codeId = loginEvent.getDetails().get(Details.CODE_ID);
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password");
@@ -85,33 +91,61 @@ public class AccessTokenTest {
Assert.assertEquals(keycloakRule.getUser("test", "test-user@localhost").getId(), token.getSubject());
Assert.assertNotEquals("test-user@localhost", token.getSubject());
+ Assert.assertEquals(sessionId, token.getSessionState());
+
Assert.assertEquals(1, token.getRealmAccess().getRoles().size());
Assert.assertTrue(token.getRealmAccess().isUserInRole("user"));
Assert.assertEquals(1, token.getResourceAccess(oauth.getClientId()).getRoles().size());
Assert.assertTrue(token.getResourceAccess(oauth.getClientId()).isUserInRole("customer-user"));
- Event event = events.expectCodeToToken(codeId).assertEvent();
+ Event event = events.expectCodeToToken(codeId, sessionId).assertEvent();
Assert.assertEquals(token.getId(), event.getDetails().get(Details.TOKEN_ID));
Assert.assertEquals(oauth.verifyRefreshToken(response.getRefreshToken()).getId(), event.getDetails().get(Details.REFRESH_TOKEN_ID));
+ Assert.assertEquals(sessionId, token.getSessionState());
response = oauth.doAccessTokenRequest(code, "password");
Assert.assertEquals(400, response.getStatusCode());
- events.expectCodeToToken(codeId).error("invalid_code").removeDetail(Details.TOKEN_ID).removeDetail(Details.REFRESH_TOKEN_ID).client((String) null).user((String) null).assertEvent();
+ events.expectCodeToToken(codeId, null).error("invalid_code").removeDetail(Details.TOKEN_ID).removeDetail(Details.REFRESH_TOKEN_ID).client((String) null).user((String) null).assertEvent();
}
@Test
public void accessTokenInvalidClientCredentials() throws Exception {
oauth.doLogin("test-user@localhost", "password");
- String codeId = events.expectLogin().assertEvent().getDetails().get(Details.CODE_ID);
+ Event loginEvent = events.expectLogin().assertEvent();
+ String codeId = loginEvent.getDetails().get(Details.CODE_ID);
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
AccessTokenResponse response = oauth.doAccessTokenRequest(code, "invalid");
Assert.assertEquals(400, response.getStatusCode());
- events.expectCodeToToken(codeId).error("invalid_client_credentials").removeDetail(Details.TOKEN_ID).removeDetail(Details.REFRESH_TOKEN_ID).assertEvent();
+ events.expectCodeToToken(codeId, loginEvent.getSessionId()).error("invalid_client_credentials").removeDetail(Details.TOKEN_ID).removeDetail(Details.REFRESH_TOKEN_ID).assertEvent();
}
+ @Test
+ public void accessTokenUserSessionExpired() {
+ oauth.doLogin("test-user@localhost", "password");
+
+ Event loginEvent = events.expectLogin().assertEvent();
+
+ String codeId = loginEvent.getDetails().get(Details.CODE_ID);
+ String sessionId = loginEvent.getSessionId();
+
+ keycloakRule.removeUserSession(sessionId);
+
+ String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+
+ OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
+ assertEquals(400, tokenResponse.getStatusCode());
+ assertNull(tokenResponse.getAccessToken());
+ assertNull(tokenResponse.getRefreshToken());
+
+ events.expectCodeToToken(codeId, sessionId).removeDetail(Details.TOKEN_ID).removeDetail(Details.REFRESH_TOKEN_ID).error(Errors.INVALID_CODE).assertEvent();
+
+ events.clear();
+ }
+
+
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OAuthGrantTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OAuthGrantTest.java
index 2072e6e..03b9086 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OAuthGrantTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OAuthGrantTest.java
@@ -27,6 +27,7 @@ import org.junit.Rule;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.audit.Details;
+import org.keycloak.audit.Event;
import org.keycloak.representations.AccessToken;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.OAuthClient;
@@ -40,6 +41,8 @@ import org.openqa.selenium.WebDriver;
import java.io.IOException;
import java.util.Map;
+import static org.junit.Assert.assertEquals;
+
/**
* @author <a href="mailto:vrockai@redhat.com">Viliam Rockai</a>
*/
@@ -82,22 +85,25 @@ public class OAuthGrantTest {
Assert.assertTrue(oauth.getCurrentQuery().containsKey(OAuth2Constants.CODE));
- String codeId = events.expectLogin().client("third-party").assertEvent().getDetails().get(Details.CODE_ID);
+ Event loginEvent = events.expectLogin().client("third-party").assertEvent();
+ String codeId = loginEvent.getDetails().get(Details.CODE_ID);
+ String sessionId = loginEvent.getSessionId();
OAuthClient.AccessTokenResponse accessToken = oauth.doAccessTokenRequest(oauth.getCurrentQuery().get(OAuth2Constants.CODE), "password");
AccessToken token = oauth.verifyToken(accessToken.getAccessToken());
+ assertEquals(sessionId, token.getSessionState());
AccessToken.Access realmAccess = token.getRealmAccess();
- Assert.assertEquals(1, realmAccess.getRoles().size());
+ assertEquals(1, realmAccess.getRoles().size());
Assert.assertTrue(realmAccess.isUserInRole("user"));
Map<String,AccessToken.Access> resourceAccess = token.getResourceAccess();
- Assert.assertEquals(1, resourceAccess.size());
- Assert.assertEquals(1, resourceAccess.get("test-app").getRoles().size());
+ assertEquals(1, resourceAccess.size());
+ assertEquals(1, resourceAccess.get("test-app").getRoles().size());
Assert.assertTrue(resourceAccess.get("test-app").isUserInRole("customer-user"));
- events.expectCodeToToken(codeId).client("third-party").assertEvent();
+ events.expectCodeToToken(codeId, loginEvent.getSessionId()).client("third-party").assertEvent();
}
@Test
@@ -112,7 +118,7 @@ public class OAuthGrantTest {
grantPage.cancel();
Assert.assertTrue(oauth.getCurrentQuery().containsKey(OAuth2Constants.ERROR));
- Assert.assertEquals("access_denied", oauth.getCurrentQuery().get(OAuth2Constants.ERROR));
+ assertEquals("access_denied", oauth.getCurrentQuery().get(OAuth2Constants.ERROR));
events.expectLogin().client("third-party").error("rejected_by_user").assertEvent();
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java
index beb8e77..030bec7 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java
@@ -27,6 +27,7 @@ import org.junit.Rule;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.audit.Details;
+import org.keycloak.audit.Errors;
import org.keycloak.audit.Event;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.RefreshToken;
@@ -43,6 +44,8 @@ import org.openqa.selenium.WebDriver;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.lessThanOrEqualTo;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -71,7 +74,10 @@ public class RefreshTokenTest {
public void refreshTokenRequest() throws Exception {
oauth.doLogin("test-user@localhost", "password");
- String codeId = events.expectLogin().assertEvent().getDetails().get(Details.CODE_ID);
+ Event loginEvent = events.expectLogin().assertEvent();
+
+ String sessionId = loginEvent.getSessionId();
+ String codeId = loginEvent.getDetails().get(Details.CODE_ID);
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
@@ -80,7 +86,7 @@ public class RefreshTokenTest {
String refreshTokenString = tokenResponse.getRefreshToken();
RefreshToken refreshToken = oauth.verifyRefreshToken(refreshTokenString);
- Event tokenEvent = events.expectCodeToToken(codeId).assertEvent();
+ Event tokenEvent = events.expectCodeToToken(codeId, sessionId).assertEvent();
Assert.assertNotNull(refreshTokenString);
@@ -89,6 +95,8 @@ public class RefreshTokenTest {
Assert.assertThat(token.getExpiration() - Time.currentTime(), allOf(greaterThanOrEqualTo(250), lessThanOrEqualTo(300)));
Assert.assertThat(refreshToken.getExpiration() - Time.currentTime(), allOf(greaterThanOrEqualTo(35950), lessThanOrEqualTo(36000)));
+ Assert.assertEquals(sessionId, refreshToken.getSessionState());
+
Thread.sleep(2000);
AccessTokenResponse response = oauth.doRefreshTokenRequest(refreshTokenString, "password");
@@ -97,6 +105,9 @@ public class RefreshTokenTest {
Assert.assertEquals(200, response.getStatusCode());
+ Assert.assertEquals(sessionId, refreshedToken.getSessionState());
+ Assert.assertEquals(sessionId, refreshedRefreshToken.getSessionState());
+
Assert.assertThat(response.getExpiresIn(), allOf(greaterThanOrEqualTo(250), lessThanOrEqualTo(300)));
Assert.assertThat(refreshedToken.getExpiration() - Time.currentTime(), allOf(greaterThanOrEqualTo(250), lessThanOrEqualTo(300)));
@@ -117,9 +128,37 @@ public class RefreshTokenTest {
Assert.assertEquals(1, refreshedToken.getResourceAccess(oauth.getClientId()).getRoles().size());
Assert.assertTrue(refreshedToken.getResourceAccess(oauth.getClientId()).isUserInRole("customer-user"));
- Event refreshEvent = events.expectRefresh(tokenEvent.getDetails().get(Details.REFRESH_TOKEN_ID)).assertEvent();
+ Event refreshEvent = events.expectRefresh(tokenEvent.getDetails().get(Details.REFRESH_TOKEN_ID), sessionId).assertEvent();
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));
}
+ @Test
+ public void refreshTokenUserSessionExpired() {
+ oauth.doLogin("test-user@localhost", "password");
+
+ Event loginEvent = events.expectLogin().assertEvent();
+
+ String sessionId = loginEvent.getSessionId();
+
+ String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+ OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
+
+ events.poll();
+
+ String refreshId = oauth.verifyRefreshToken(tokenResponse.getRefreshToken()).getId();
+
+ keycloakRule.removeUserSession(sessionId);
+
+ tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), "password");
+
+ assertEquals(400, tokenResponse.getStatusCode());
+ assertNull(tokenResponse.getAccessToken());
+ assertNull(tokenResponse.getRefreshToken());
+
+ events.expectRefresh(refreshId, sessionId).error(Errors.INVALID_TOKEN);
+
+ events.clear();
+ }
+
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ResourceOwnerPasswordCredentialsGrantTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ResourceOwnerPasswordCredentialsGrantTest.java
new file mode 100644
index 0000000..15a3f23
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ResourceOwnerPasswordCredentialsGrantTest.java
@@ -0,0 +1,181 @@
+package org.keycloak.testsuite.oauth;
+
+import net.iharder.Base64;
+import org.apache.http.HttpResponse;
+import org.apache.http.NameValuePair;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.message.BasicNameValuePair;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.audit.Details;
+import org.keycloak.audit.Errors;
+import org.keycloak.audit.Event;
+import org.keycloak.models.ApplicationModel;
+import org.keycloak.models.RealmModel;
+import org.keycloak.representations.AccessToken;
+import org.keycloak.representations.RefreshToken;
+import org.keycloak.services.managers.RealmManager;
+import org.keycloak.testsuite.AssertEvents;
+import org.keycloak.testsuite.OAuthClient;
+import org.keycloak.testsuite.rule.KeycloakRule;
+import org.keycloak.testsuite.rule.WebResource;
+import org.keycloak.testsuite.rule.WebRule;
+import org.openqa.selenium.WebDriver;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.util.LinkedList;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class ResourceOwnerPasswordCredentialsGrantTest {
+
+ @ClassRule
+ public static KeycloakRule keycloakRule = new KeycloakRule(new KeycloakRule.KeycloakSetup() {
+ @Override
+ public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+ ApplicationModel app = appRealm.addApplication("resource-owner");
+ app.setSecret("secret");
+ }
+ });
+
+ @Rule
+ public AssertEvents events = new AssertEvents(keycloakRule);
+
+ @Rule
+ public WebRule webRule = new WebRule(this);
+
+ @WebResource
+ protected WebDriver driver;
+
+ @WebResource
+ protected OAuthClient oauth;
+
+ @Test
+ public void grantAccessToken() throws Exception {
+ oauth.clientId("resource-owner");
+
+ OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("secret", "test-user@localhost", "password");
+
+ assertEquals(200, response.getStatusCode());
+
+ AccessToken accessToken = oauth.verifyToken(response.getAccessToken());
+ RefreshToken refreshToken = oauth.verifyRefreshToken(response.getRefreshToken());
+
+ events.expectLogin()
+ .client("resource-owner")
+ .session(accessToken.getSessionState())
+ .detail(Details.AUTH_METHOD, "oauth_credentials")
+ .detail(Details.RESPONSE_TYPE, "token")
+ .detail(Details.TOKEN_ID, accessToken.getId())
+ .detail(Details.REFRESH_TOKEN_ID, refreshToken.getId())
+ .removeDetail(Details.CODE_ID)
+ .removeDetail(Details.REDIRECT_URI)
+ .assertEvent();
+
+ assertEquals(accessToken.getSessionState(), refreshToken.getSessionState());
+
+ OAuthClient.AccessTokenResponse refreshedResponse = oauth.doRefreshTokenRequest(response.getRefreshToken(), "secret");
+
+ AccessToken refreshedAccessToken = oauth.verifyToken(refreshedResponse.getAccessToken());
+ RefreshToken refreshedRefreshToken = oauth.verifyRefreshToken(refreshedResponse.getRefreshToken());
+
+ assertEquals(accessToken.getSessionState(), refreshedAccessToken.getSessionState());
+ assertEquals(accessToken.getSessionState(), refreshedRefreshToken.getSessionState());
+
+ events.expectRefresh(refreshToken.getId(), refreshToken.getSessionState()).client("resource-owner").assertEvent();
+ }
+
+ @Test
+ public void grantAccessTokenLogout() throws Exception {
+ oauth.clientId("resource-owner");
+
+ OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("secret", "test-user@localhost", "password");
+
+ assertEquals(200, response.getStatusCode());
+
+ AccessToken accessToken = oauth.verifyToken(response.getAccessToken());
+ RefreshToken refreshToken = oauth.verifyRefreshToken(response.getRefreshToken());
+
+ events.expectLogin()
+ .client("resource-owner")
+ .session(accessToken.getSessionState())
+ .detail(Details.AUTH_METHOD, "oauth_credentials")
+ .detail(Details.RESPONSE_TYPE, "token")
+ .detail(Details.TOKEN_ID, accessToken.getId())
+ .detail(Details.REFRESH_TOKEN_ID, refreshToken.getId())
+ .removeDetail(Details.CODE_ID)
+ .removeDetail(Details.REDIRECT_URI)
+ .assertEvent();
+
+ HttpResponse logoutResponse = oauth.doLogout(null, accessToken.getSessionState());
+ assertEquals(200, logoutResponse.getStatusLine().getStatusCode());
+ events.expectLogout(accessToken.getSessionState()).removeDetail(Details.REDIRECT_URI).assertEvent();
+
+ logoutResponse = oauth.doLogout(null, accessToken.getSessionState());
+ assertEquals(200, logoutResponse.getStatusLine().getStatusCode());
+ events.expectLogout(accessToken.getSessionState()).user((String) null).removeDetail(Details.REDIRECT_URI).error(Errors.USER_SESSION_NOT_FOUND).assertEvent();
+
+ response = oauth.doRefreshTokenRequest(response.getRefreshToken(), "secret");
+ assertEquals(400, response.getStatusCode());
+ assertEquals("invalid_grant", response.getError());
+
+ events.expectRefresh(refreshToken.getId(), refreshToken.getSessionState()).client("resource-owner")
+ .removeDetail(Details.TOKEN_ID)
+ .removeDetail(Details.UPDATED_REFRESH_TOKEN_ID)
+ .error(Errors.INVALID_TOKEN).assertEvent();
+ }
+
+ @Test
+ public void grantAccessTokenInvalidClientCredentials() throws Exception {
+ oauth.clientId("resource-owner");
+
+ OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("invalid", "test-user@localhost", "password");
+
+ assertEquals(400, response.getStatusCode());
+
+ assertEquals("unauthorized_client", response.getError());
+
+ events.expectLogin()
+ .client("resource-owner")
+ .session((String) null)
+ .detail(Details.AUTH_METHOD, "oauth_credentials")
+ .detail(Details.RESPONSE_TYPE, "token")
+ .removeDetail(Details.CODE_ID)
+ .removeDetail(Details.REDIRECT_URI)
+ .error(Errors.INVALID_CLIENT_CREDENTIALS)
+ .assertEvent();
+ }
+
+ @Test
+ public void grantAccessTokenInvalidUserCredentials() throws Exception {
+ oauth.clientId("resource-owner");
+
+ OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("secret", "test-user@localhost", "invalid");
+
+ assertEquals(400, response.getStatusCode());
+
+ assertEquals("invalid_grant", response.getError());
+
+ events.expectLogin()
+ .client("resource-owner")
+ .session((String) null)
+ .detail(Details.AUTH_METHOD, "oauth_credentials")
+ .detail(Details.RESPONSE_TYPE, "token")
+ .removeDetail(Details.CODE_ID)
+ .removeDetail(Details.REDIRECT_URI)
+ .error(Errors.INVALID_USER_CREDENTIALS)
+ .assertEvent();
+ }
+
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java
index 2b8ea8f..77bd97e 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java
@@ -21,11 +21,13 @@
*/
package org.keycloak.testsuite;
+import net.iharder.Base64;
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.impl.client.DefaultHttpClient;
@@ -36,9 +38,12 @@ import org.junit.Assert;
import org.keycloak.OAuth2Constants;
import org.keycloak.RSATokenVerifier;
import org.keycloak.VerificationException;
+import org.keycloak.audit.Details;
+import org.keycloak.audit.Event;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.crypto.RSAProvider;
import org.keycloak.representations.AccessToken;
+import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.RefreshToken;
import org.keycloak.services.resources.TokenService;
import org.keycloak.util.BasicAuthHelper;
@@ -46,6 +51,7 @@ import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import javax.ws.rs.core.UriBuilder;
+import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
@@ -144,6 +150,35 @@ public class OAuthClient {
}
}
+ public AccessTokenResponse doGrantAccessTokenRequest(String clientSecret, String username, String password) throws Exception {
+ HttpClient client = new DefaultHttpClient();
+ HttpPost post = new HttpPost(getResourceOwnerPasswordCredentialGrantUrl());
+
+ String authorization = BasicAuthHelper.createHeader(clientId, clientSecret);
+ post.setHeader("Authorization", authorization);
+
+ List<NameValuePair> parameters = new LinkedList<NameValuePair>();
+ parameters.add(new BasicNameValuePair("username", username));
+ parameters.add(new BasicNameValuePair("password", password));
+
+ UrlEncodedFormEntity formEntity;
+ try {
+ formEntity = new UrlEncodedFormEntity(parameters, "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException(e);
+ }
+ post.setEntity(formEntity);
+
+ return new AccessTokenResponse(client.execute(post));
+ }
+
+ public HttpResponse doLogout(String redirectUri, String sessionState) throws IOException {
+ HttpClient client = new DefaultHttpClient();
+ HttpGet get = new HttpGet(getLogoutUrl(redirectUri, sessionState));
+
+ return client.execute(get);
+ }
+
public AccessTokenResponse doRefreshTokenRequest(String refreshToken, String password) {
HttpClient client = new DefaultHttpClient();
HttpPost post = new HttpPost(getRefreshTokenUrl());
@@ -163,7 +198,7 @@ public class OAuthClient {
parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ID, clientId));
}
- UrlEncodedFormEntity formEntity = null;
+ UrlEncodedFormEntity formEntity;
try {
formEntity = new UrlEncodedFormEntity(parameters, "UTF-8");
} catch (UnsupportedEncodingException e) {
@@ -267,6 +302,22 @@ public class OAuthClient {
return b.build(realm).toString();
}
+ public String getLogoutUrl(String redirectUri, String sessionState) {
+ UriBuilder b = TokenService.logoutUrl(UriBuilder.fromUri(baseUrl));
+ if (redirectUri != null) {
+ b.queryParam(OAuth2Constants.REDIRECT_URI, redirectUri);
+ }
+ if (sessionState != null) {
+ b.queryParam("session_state", sessionState);
+ }
+ return b.build(realm).toString();
+ }
+
+ public String getResourceOwnerPasswordCredentialGrantUrl() {
+ UriBuilder b = TokenService.grantAccessTokenUrl(UriBuilder.fromUri(baseUrl));
+ return b.build(realm).toString();
+ }
+
public String getRefreshTokenUrl() {
UriBuilder b = TokenService.refreshUrl(UriBuilder.fromUri(baseUrl));
return b.build(realm).toString();
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/KeycloakRule.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/KeycloakRule.java
index 4cd2b68..d04cc21 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/KeycloakRule.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/KeycloakRule.java
@@ -24,10 +24,13 @@ package org.keycloak.testsuite.rule;
import org.keycloak.models.Config;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserSessionModel;
import org.keycloak.provider.ProviderSession;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.ApplicationServlet;
+import static org.junit.Assert.assertNotNull;
+
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@@ -78,6 +81,28 @@ public class KeycloakRule extends AbstractKeycloakRule {
}
}
+ public KeycloakSession startSession() {
+ KeycloakSession session = server.getKeycloakSessionFactory().createSession();
+ session.getTransaction().begin();
+ return session;
+ }
+
+ public void stopSession(KeycloakSession session, boolean commit) {
+ if (commit) {
+ session.getTransaction().commit();
+ }
+ session.close();
+ }
+
+ public void removeUserSession(String sessionId) {
+ KeycloakSession keycloakSession = startSession();
+ RealmModel realm = keycloakSession.getRealm("test");
+ UserSessionModel session = realm.getUserSession(sessionId);
+ assertNotNull(session);
+ realm.removeUserSession(session);
+ stopSession(keycloakSession, true);
+ }
+
public abstract static class KeycloakSetup {
protected ProviderSession providerSession;
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/WebRule.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/WebRule.java
index ef37e21..35454dc 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/WebRule.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/WebRule.java
@@ -47,6 +47,13 @@ public class WebRule extends ExternalResource {
@Override
protected void before() throws Throwable {
+ driver = createWebDriver();
+ oauth = new OAuthClient(driver);
+ initWebResources(test);
+ }
+
+ public static WebDriver createWebDriver() {
+ WebDriver driver;
String browser = "htmlunit";
if (System.getProperty("browser") != null) {
browser = System.getProperty("browser");
@@ -64,10 +71,7 @@ public class WebRule extends ExternalResource {
} else {
throw new RuntimeException("Unsupported browser " + browser);
}
-
- oauth = new OAuthClient(driver);
-
- initWebResources(test);
+ return driver;
}
protected void initWebResources(Object o) {
@@ -122,7 +126,7 @@ public class WebRule extends ExternalResource {
driver.close();
}
- public class HtmlUnitDriver extends org.openqa.selenium.htmlunit.HtmlUnitDriver {
+ public static class HtmlUnitDriver extends org.openqa.selenium.htmlunit.HtmlUnitDriver {
@Override
public WebClient getWebClient() {
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/social/SocialLoginTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/social/SocialLoginTest.java
index 6ed3294..f7693bf 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/social/SocialLoginTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/social/SocialLoginTest.java
@@ -28,6 +28,7 @@ import org.junit.Rule;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.audit.Details;
+import org.keycloak.audit.Event;
import org.keycloak.models.RealmModel;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.idm.UserRepresentation;
@@ -116,16 +117,21 @@ public class SocialLoginTest {
.detail(Details.REGISTER_METHOD, "social@dummy")
.detail(Details.REDIRECT_URI, AssertEvents.DEFAULT_REDIRECT_URI)
.detail(Details.USERNAME, "1@dummy")
+ .session((String) null)
.assertEvent().getUserId();
- String codeId = events.expectLogin().user(userId).detail(Details.USERNAME, "1@dummy").detail(Details.AUTH_METHOD, "social@dummy").assertEvent().getDetails().get(Details.CODE_ID);
+ Event loginEvent = events.expectLogin().user(userId).detail(Details.USERNAME, "1@dummy").detail(Details.AUTH_METHOD, "social@dummy").assertEvent();
+
+ String sessionId = loginEvent.getSessionId();
+ String codeId = loginEvent.getDetails().get(Details.CODE_ID);
AccessTokenResponse response = oauth.doAccessTokenRequest(oauth.getCurrentQuery().get(OAuth2Constants.CODE), "password");
- events.expectCodeToToken(codeId).user(userId).assertEvent();
+ events.expectCodeToToken(codeId, sessionId).user(userId).assertEvent();
AccessToken token = oauth.verifyToken(response.getAccessToken());
Assert.assertEquals(36, token.getSubject().length());
+ Assert.assertEquals(sessionId, token.getSessionState());
UserRepresentation profile = keycloakRule.getUserById("test", token.getSubject());
Assert.assertEquals(36, profile.getUsername().length());
@@ -136,7 +142,7 @@ public class SocialLoginTest {
oauth.openLogout();
- events.expectLogout().user(userId).assertEvent();
+ events.expectLogout(sessionId).user(userId).assertEvent();
loginPage.open();
@@ -160,7 +166,7 @@ public class SocialLoginTest {
Assert.assertTrue(loginPage.isCurrent());
Assert.assertEquals("Access denied", loginPage.getWarning());
- events.expectLogin().error("rejected_by_user").user((String) null).detail(Details.AUTH_METHOD, "social@dummy").removeDetail(Details.USERNAME).removeDetail(Details.CODE_ID).assertEvent();
+ events.expectLogin().error("rejected_by_user").user((String) null).session((String) null).detail(Details.AUTH_METHOD, "social@dummy").removeDetail(Details.USERNAME).removeDetail(Details.CODE_ID).assertEvent();
loginPage.login("test-user@localhost", "password");
@@ -212,12 +218,13 @@ public class SocialLoginTest {
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
- String codeId = events.expectLogin().user(userId).removeDetail(Details.USERNAME).detail(Details.AUTH_METHOD, "social@dummy").detail(Details.USERNAME, "2@dummy").assertEvent().getDetails().get(Details.CODE_ID);
+ Event loginEvent = events.expectLogin().user(userId).removeDetail(Details.USERNAME).detail(Details.AUTH_METHOD, "social@dummy").detail(Details.USERNAME, "2@dummy").assertEvent();
+ String codeId = loginEvent.getDetails().get(Details.CODE_ID);
AccessTokenResponse response = oauth.doAccessTokenRequest(oauth.getCurrentQuery().get(OAuth2Constants.CODE), "password");
AccessToken token = oauth.verifyToken(response.getAccessToken());
- events.expectCodeToToken(codeId).user(userId).assertEvent();
+ events.expectCodeToToken(codeId, loginEvent.getSessionId()).user(userId).assertEvent();
UserRepresentation profile = keycloakRule.getUserById("test", token.getSubject());
diff --git a/testsuite/performance/src/test/resources/META-INF/persistence.xml b/testsuite/performance/src/test/resources/META-INF/persistence.xml
index 29af038..e5ae89a 100755
--- a/testsuite/performance/src/test/resources/META-INF/persistence.xml
+++ b/testsuite/performance/src/test/resources/META-INF/persistence.xml
@@ -16,6 +16,7 @@
<class>org.keycloak.models.jpa.entities.SocialLinkEntity</class>
<class>org.keycloak.models.jpa.entities.AuthenticationLinkEntity</class>
<class>org.keycloak.models.jpa.entities.UserEntity</class>
+ <class>org.keycloak.models.jpa.entities.UserSessionEntity</class>
<class>org.keycloak.models.jpa.entities.UsernameLoginFailureEntity</class>
<class>org.keycloak.models.jpa.entities.UserRoleMappingEntity</class>
<class>org.keycloak.models.jpa.entities.ScopeMappingEntity</class>