Merge pull request #372 from stianst/login-session

KEYCLOAK-432 Added user sessions
This commit is contained in:
Stian Thorgersen 2014-05-09 11:53:23 +01:00
commit 59747500aa
66 changed files with 1634 additions and 249 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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;
/**
@ -82,6 +86,9 @@ public class RequiredActionsService {
@Context
private UriInfo uriInfo;
@Context
private ClientConnection clientConnection;
@Context
protected Providers providers;
@ -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")

View file

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

View file

@ -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);
audit.user(user).success();
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 {
logger.info("No user logged in for logout");
audit.error(Errors.USER_NOT_LOGGED_IN);
}
// todo manage legal redirects
return Response.status(302).location(UriBuilder.fromUri(redirectUri).build()).build();
if (redirectUri != null) {
// todo manage legal redirects
return Response.status(302).location(UriBuilder.fromUri(redirectUri).build()).build();
} else {
return Response.ok().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) {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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