KEYCLOAK-431 View open sessions, and logout all sessions, through account management
This commit is contained in:
parent
f7c3373f75
commit
f4f9b1e323
20 changed files with 298 additions and 26 deletions
|
@ -1,5 +1,7 @@
|
||||||
package org.keycloak.util;
|
package org.keycloak.util;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
*/
|
*/
|
||||||
|
@ -9,4 +11,8 @@ public class Time {
|
||||||
return (int) (System.currentTimeMillis() / 1000);
|
return (int) (System.currentTimeMillis() / 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Date toDate(int time) {
|
||||||
|
return new Date(((long) time ) * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import org.codehaus.jackson.map.SerializationConfig;
|
||||||
import org.codehaus.jackson.map.annotate.JsonSerialize;
|
import org.codehaus.jackson.map.annotate.JsonSerialize;
|
||||||
import org.keycloak.adapters.ServerRequest;
|
import org.keycloak.adapters.ServerRequest;
|
||||||
import org.keycloak.adapters.installed.KeycloakInstalled;
|
import org.keycloak.adapters.installed.KeycloakInstalled;
|
||||||
|
import org.keycloak.util.Time;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
@ -65,7 +66,7 @@ public class CustomerCli {
|
||||||
System.out.println(mapper.writeValueAsString(keycloak.getIdToken()));
|
System.out.println(mapper.writeValueAsString(keycloak.getIdToken()));
|
||||||
} else if (s.equals("refresh")) {
|
} else if (s.equals("refresh")) {
|
||||||
keycloak.refreshToken();
|
keycloak.refreshToken();
|
||||||
System.out.println("Token refreshed: expires at " + new Date(keycloak.getToken().getExpiration() * 1000));
|
System.out.println("Token refreshed: expires at " + Time.toDate(keycloak.getToken().getExpiration()));
|
||||||
} else if (s.equals("exit")) {
|
} else if (s.equals("exit")) {
|
||||||
System.exit(0);
|
System.exit(0);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -3,6 +3,7 @@ package org.keycloak.account;
|
||||||
import org.keycloak.audit.Event;
|
import org.keycloak.audit.Event;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
|
import org.keycloak.models.UserSessionModel;
|
||||||
|
|
||||||
import javax.ws.rs.core.MultivaluedMap;
|
import javax.ws.rs.core.MultivaluedMap;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
|
@ -31,5 +32,7 @@ public interface Account {
|
||||||
|
|
||||||
Account setEvents(List<Event> events);
|
Account setEvents(List<Event> events);
|
||||||
|
|
||||||
|
Account setSessions(List<UserSessionModel> sessions);
|
||||||
|
|
||||||
Account setFeatures(boolean social, boolean audit, boolean passwordUpdateSupported);
|
Account setFeatures(boolean social, boolean audit, boolean passwordUpdateSupported);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,6 @@ package org.keycloak.account;
|
||||||
*/
|
*/
|
||||||
public enum AccountPages {
|
public enum AccountPages {
|
||||||
|
|
||||||
ACCOUNT, PASSWORD, TOTP, SOCIAL, LOG;
|
ACCOUNT, PASSWORD, TOTP, SOCIAL, LOG, SESSIONS;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import org.keycloak.account.freemarker.model.FeaturesBean;
|
||||||
import org.keycloak.account.freemarker.model.LogBean;
|
import org.keycloak.account.freemarker.model.LogBean;
|
||||||
import org.keycloak.account.freemarker.model.MessageBean;
|
import org.keycloak.account.freemarker.model.MessageBean;
|
||||||
import org.keycloak.account.freemarker.model.ReferrerBean;
|
import org.keycloak.account.freemarker.model.ReferrerBean;
|
||||||
|
import org.keycloak.account.freemarker.model.SessionsBean;
|
||||||
import org.keycloak.account.freemarker.model.TotpBean;
|
import org.keycloak.account.freemarker.model.TotpBean;
|
||||||
import org.keycloak.account.freemarker.model.UrlBean;
|
import org.keycloak.account.freemarker.model.UrlBean;
|
||||||
import org.keycloak.audit.Event;
|
import org.keycloak.audit.Event;
|
||||||
|
@ -19,6 +20,7 @@ import org.keycloak.freemarker.ThemeLoader;
|
||||||
import org.keycloak.models.ApplicationModel;
|
import org.keycloak.models.ApplicationModel;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
|
import org.keycloak.models.UserSessionModel;
|
||||||
|
|
||||||
import javax.ws.rs.core.MediaType;
|
import javax.ws.rs.core.MediaType;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
|
@ -43,6 +45,7 @@ public class FreeMarkerAccount implements Account {
|
||||||
private RealmModel realm;
|
private RealmModel realm;
|
||||||
private String[] referrer;
|
private String[] referrer;
|
||||||
private List<Event> events;
|
private List<Event> events;
|
||||||
|
private List<UserSessionModel> sessions;
|
||||||
private boolean social;
|
private boolean social;
|
||||||
private boolean audit;
|
private boolean audit;
|
||||||
private boolean passwordUpdateSupported;
|
private boolean passwordUpdateSupported;
|
||||||
|
@ -100,7 +103,7 @@ public class FreeMarkerAccount implements Account {
|
||||||
attributes.put("referrer", new ReferrerBean(referrer));
|
attributes.put("referrer", new ReferrerBean(referrer));
|
||||||
}
|
}
|
||||||
|
|
||||||
attributes.put("url", new UrlBean(realm, theme, baseUri, baseQueryUri));
|
attributes.put("url", new UrlBean(realm, theme, baseUri, baseQueryUri, uriInfo.getRequestUri()));
|
||||||
|
|
||||||
attributes.put("features", new FeaturesBean(social, audit, passwordUpdateSupported));
|
attributes.put("features", new FeaturesBean(social, audit, passwordUpdateSupported));
|
||||||
|
|
||||||
|
@ -116,6 +119,10 @@ public class FreeMarkerAccount implements Account {
|
||||||
break;
|
break;
|
||||||
case LOG:
|
case LOG:
|
||||||
attributes.put("log", new LogBean(events));
|
attributes.put("log", new LogBean(events));
|
||||||
|
break;
|
||||||
|
case SESSIONS:
|
||||||
|
attributes.put("sessions", new SessionsBean(sessions));
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -178,6 +185,12 @@ public class FreeMarkerAccount implements Account {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Account setSessions(List<UserSessionModel> sessions) {
|
||||||
|
this.sessions = sessions;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Account setFeatures(boolean social, boolean audit, boolean passwordUpdateSupported) {
|
public Account setFeatures(boolean social, boolean audit, boolean passwordUpdateSupported) {
|
||||||
this.social = social;
|
this.social = social;
|
||||||
|
|
|
@ -19,6 +19,8 @@ public class Templates {
|
||||||
return "social.ftl";
|
return "social.ftl";
|
||||||
case LOG:
|
case LOG:
|
||||||
return "log.ftl";
|
return "log.ftl";
|
||||||
|
case SESSIONS:
|
||||||
|
return "sessions.ftl";
|
||||||
default:
|
default:
|
||||||
throw new IllegalArgumentException();
|
throw new IllegalArgumentException();
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
package org.keycloak.account.freemarker.model;
|
||||||
|
|
||||||
|
import org.keycloak.models.UserSessionModel;
|
||||||
|
import org.keycloak.util.Time;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
|
*/
|
||||||
|
public class SessionsBean {
|
||||||
|
|
||||||
|
private List<UserSessionBean> events;
|
||||||
|
|
||||||
|
public SessionsBean(List<UserSessionModel> sessions) {
|
||||||
|
this.events = new LinkedList<UserSessionBean>();
|
||||||
|
for (UserSessionModel session : sessions) {
|
||||||
|
this.events.add(new UserSessionBean(session));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<UserSessionBean> getSessions() {
|
||||||
|
return events;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class UserSessionBean {
|
||||||
|
|
||||||
|
private UserSessionModel session;
|
||||||
|
|
||||||
|
public UserSessionBean(UserSessionModel session) {
|
||||||
|
this.session = session;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getIpAddress() {
|
||||||
|
return session.getIpAddress();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Date getStarted() {
|
||||||
|
return Time.toDate(session.getStarted());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Date getExpires() {
|
||||||
|
return Time.toDate(session.getExpires());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ package org.keycloak.account.freemarker.model;
|
||||||
|
|
||||||
import org.keycloak.freemarker.Theme;
|
import org.keycloak.freemarker.Theme;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.services.resources.TokenService;
|
||||||
import org.keycloak.services.resources.flows.Urls;
|
import org.keycloak.services.resources.flows.Urls;
|
||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
@ -15,12 +16,14 @@ public class UrlBean {
|
||||||
private Theme theme;
|
private Theme theme;
|
||||||
private URI baseURI;
|
private URI baseURI;
|
||||||
private URI baseQueryURI;
|
private URI baseQueryURI;
|
||||||
|
private URI currentURI;
|
||||||
|
|
||||||
public UrlBean(RealmModel realm, Theme theme, URI baseURI, URI baseQueryURI) {
|
public UrlBean(RealmModel realm, Theme theme, URI baseURI, URI baseQueryURI, URI currentURI) {
|
||||||
this.realm = realm.getName();
|
this.realm = realm.getName();
|
||||||
this.theme = theme;
|
this.theme = theme;
|
||||||
this.baseURI = baseURI;
|
this.baseURI = baseURI;
|
||||||
this.baseQueryURI = baseQueryURI;
|
this.baseQueryURI = baseQueryURI;
|
||||||
|
this.currentURI = currentURI;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getAccessUrl() {
|
public String getAccessUrl() {
|
||||||
|
@ -47,12 +50,20 @@ public class UrlBean {
|
||||||
return Urls.accountLogPage(baseQueryURI, realm).toString();
|
return Urls.accountLogPage(baseQueryURI, realm).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getSessionsUrl() {
|
||||||
|
return Urls.accountSessionsPage(baseQueryURI, realm).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSessionsLogoutUrl() {
|
||||||
|
return Urls.accountSessionsLogoutPage(baseQueryURI, realm).toString();
|
||||||
|
}
|
||||||
|
|
||||||
public String getTotpRemoveUrl() {
|
public String getTotpRemoveUrl() {
|
||||||
return Urls.accountTotpRemove(baseQueryURI, realm).toString();
|
return Urls.accountTotpRemove(baseQueryURI, realm).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getLogoutUrl() {
|
public String getLogoutUrl() {
|
||||||
return Urls.accountLogout(baseQueryURI, realm).toString();
|
return Urls.accountLogout(baseQueryURI, currentURI, realm).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getResourcesPath() {
|
public String getResourcesPath() {
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
<#import "template.ftl" as layout>
|
||||||
|
<@layout.mainLayout active='sessions' bodyClass='sessions'; section>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-10">
|
||||||
|
<h2>Sessions</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<td>IP</td>
|
||||||
|
<td>Started</td>
|
||||||
|
<td>Expires</td>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
<#list sessions.sessions as session>
|
||||||
|
<tr>
|
||||||
|
<td>${session.ipAddress}</td>
|
||||||
|
<td>${session.started?datetime}</td>
|
||||||
|
<td>${session.expires?datetime}</td>
|
||||||
|
</tr>
|
||||||
|
</#list>
|
||||||
|
</tbody>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<a id="logout-all-sessions" href="${url.sessionsLogoutUrl}">Logout all sessions</a>
|
||||||
|
|
||||||
|
</@layout.mainLayout>
|
|
@ -43,6 +43,7 @@
|
||||||
<#if features.passwordUpdateSupported><li class="<#if active=='password'>active</#if>"><a href="${url.passwordUrl}">Password</a></li></#if>
|
<#if features.passwordUpdateSupported><li class="<#if active=='password'>active</#if>"><a href="${url.passwordUrl}">Password</a></li></#if>
|
||||||
<li class="<#if active=='totp'>active</#if>"><a href="${url.totpUrl}">Authenticator</a></li>
|
<li class="<#if active=='totp'>active</#if>"><a href="${url.totpUrl}">Authenticator</a></li>
|
||||||
<#if features.social><li class="<#if active=='social'>active</#if>"><a href="${url.socialUrl}">Social</a></li></#if>
|
<#if features.social><li class="<#if active=='social'>active</#if>"><a href="${url.socialUrl}">Social</a></li></#if>
|
||||||
|
<li class="<#if active=='sessions'>active</#if>"><a href="${url.sessionsUrl}">Sessions</a></li>
|
||||||
<#if features.log><li class="<#if active=='log'>active</#if>"><a href="${url.logUrl}">Log</a></li></#if>
|
<#if features.log><li class="<#if active=='log'>active</#if>"><a href="${url.logUrl}">Log</a></li></#if>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -257,6 +257,8 @@ public interface RealmModel extends RoleContainerModel, RoleMapperModel, ScopeMa
|
||||||
|
|
||||||
UserSessionModel getUserSession(String id);
|
UserSessionModel getUserSession(String id);
|
||||||
|
|
||||||
|
List<UserSessionModel> getUserSessions(UserModel user);
|
||||||
|
|
||||||
void removeUserSession(UserSessionModel session);
|
void removeUserSession(UserSessionModel session);
|
||||||
|
|
||||||
void removeUserSessions(UserModel user);
|
void removeUserSessions(UserModel user);
|
||||||
|
|
|
@ -49,6 +49,7 @@ import java.util.Comparator;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
@ -1405,6 +1406,15 @@ public class RealmAdapter implements RealmModel {
|
||||||
return entity != null ? new UserSessionAdapter(entity) : null;
|
return entity != null ? new UserSessionAdapter(entity) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<UserSessionModel> getUserSessions(UserModel user) {
|
||||||
|
List<UserSessionModel> sessions = new LinkedList<UserSessionModel>();
|
||||||
|
for (UserSessionEntity e : em.createNamedQuery("getUserSessionByUser", UserSessionEntity.class).setParameter("user", ((UserAdapter) user).getUser()).getResultList()) {
|
||||||
|
sessions.add(new UserSessionAdapter(e));
|
||||||
|
}
|
||||||
|
return sessions;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void removeUserSession(UserSessionModel session) {
|
public void removeUserSession(UserSessionModel session) {
|
||||||
em.remove(((UserSessionAdapter) session).getEntity());
|
em.remove(((UserSessionAdapter) session).getEntity());
|
||||||
|
|
|
@ -15,6 +15,7 @@ import javax.persistence.NamedQuery;
|
||||||
*/
|
*/
|
||||||
@Entity
|
@Entity
|
||||||
@NamedQueries({
|
@NamedQueries({
|
||||||
|
@NamedQuery(name = "getUserSessionByUser", query = "select s from UserSessionEntity s where s.user = :user"),
|
||||||
@NamedQuery(name = "removeUserSessionByUser", query = "delete from UserSessionEntity s where s.user = :user"),
|
@NamedQuery(name = "removeUserSessionByUser", query = "delete from UserSessionEntity s where s.user = :user"),
|
||||||
@NamedQuery(name = "removeUserSessionExpired", query = "delete from UserSessionEntity s where s.expires < :currentTime")
|
@NamedQuery(name = "removeUserSessionExpired", query = "delete from UserSessionEntity s where s.expires < :currentTime")
|
||||||
})
|
})
|
||||||
|
|
|
@ -45,6 +45,7 @@ import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
@ -1375,6 +1376,16 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<UserSessionModel> getUserSessions(UserModel user) {
|
||||||
|
DBObject query = new BasicDBObject("user", user.getId());
|
||||||
|
List<UserSessionModel> sessions = new LinkedList<UserSessionModel>();
|
||||||
|
for (MongoUserSessionEntity e : getMongoStore().loadEntities(MongoUserSessionEntity.class, query, invocationContext)) {
|
||||||
|
sessions.add(new UserSessionAdapter(e, this, invocationContext));
|
||||||
|
}
|
||||||
|
return sessions;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void removeUserSession(UserSessionModel session) {
|
public void removeUserSession(UserSessionModel session) {
|
||||||
getMongoStore().removeEntity(((UserSessionAdapter) session).getEntity(), invocationContext);
|
getMongoStore().removeEntity(((UserSessionAdapter) session).getEntity(), invocationContext);
|
||||||
|
|
|
@ -241,10 +241,6 @@ public class AccountService {
|
||||||
return forwardToPage("social", AccountPages.SOCIAL);
|
return forwardToPage("social", AccountPages.SOCIAL);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static UriBuilder logUrl(UriBuilder base) {
|
|
||||||
return RealmsResource.accountUrl(base).path(AccountService.class, "logPage");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Path("log")
|
@Path("log")
|
||||||
@GET
|
@GET
|
||||||
public Response logPage() {
|
public Response logPage() {
|
||||||
|
@ -269,6 +265,15 @@ public class AccountService {
|
||||||
return forwardToPage("log", AccountPages.LOG);
|
return forwardToPage("log", AccountPages.LOG);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Path("sessions")
|
||||||
|
@GET
|
||||||
|
public Response sessionsPage() {
|
||||||
|
if (auth != null) {
|
||||||
|
account.setSessions(realm.getUserSessions(auth.getUser()));
|
||||||
|
}
|
||||||
|
return forwardToPage("sessions", AccountPages.SESSIONS);
|
||||||
|
}
|
||||||
|
|
||||||
@Path("/")
|
@Path("/")
|
||||||
@POST
|
@POST
|
||||||
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
||||||
|
@ -314,6 +319,18 @@ public class AccountService {
|
||||||
return account.setSuccess("successTotpRemoved").createResponse(AccountPages.TOTP);
|
return account.setSuccess("successTotpRemoved").createResponse(AccountPages.TOTP);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Path("sessions-logout")
|
||||||
|
@GET
|
||||||
|
public Response processSessionsLogout() {
|
||||||
|
require(AccountRoles.MANAGE_ACCOUNT);
|
||||||
|
|
||||||
|
UserModel user = auth.getUser();
|
||||||
|
realm.removeUserSessions(user);
|
||||||
|
|
||||||
|
return Response.seeOther(Urls.accountSessionsPage(uriInfo.getBaseUri(), realm.getName())).build();
|
||||||
|
}
|
||||||
|
|
||||||
@Path("totp")
|
@Path("totp")
|
||||||
@POST
|
@POST
|
||||||
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
||||||
|
@ -493,16 +510,6 @@ public class AccountService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Path("logout")
|
|
||||||
@GET
|
|
||||||
public Response logout() {
|
|
||||||
URI redirect = Urls.accountBase(uriInfo.getBaseUri()).build(realm.getName());
|
|
||||||
|
|
||||||
return Response.status(302).location(
|
|
||||||
TokenService.logoutUrl(uriInfo).queryParam("redirect_uri", redirect.toString()).build(realm.getName())
|
|
||||||
).build();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Response login(String path) {
|
private Response login(String path) {
|
||||||
OAuthRedirect oauth = new OAuthRedirect();
|
OAuthRedirect oauth = new OAuthRedirect();
|
||||||
String authUrl = Urls.realmLoginPage(uriInfo.getBaseUri(), realm.getName()).toString();
|
String authUrl = Urls.realmLoginPage(uriInfo.getBaseUri(), realm.getName()).toString();
|
||||||
|
|
|
@ -76,8 +76,16 @@ public class Urls {
|
||||||
return accountBase(baseUri).path(AccountService.class, "logPage").build(realmId);
|
return accountBase(baseUri).path(AccountService.class, "logPage").build(realmId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static URI accountLogout(URI baseUri, String realmId) {
|
public static URI accountSessionsPage(URI baseUri, String realmId) {
|
||||||
return accountBase(baseUri).path(AccountService.class, "logout").build(realmId);
|
return accountBase(baseUri).path(AccountService.class, "sessionsPage").build(realmId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static URI accountSessionsLogoutPage(URI baseUri, String realmId) {
|
||||||
|
return accountBase(baseUri).path(AccountService.class, "processSessionsLogout").build(realmId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static URI accountLogout(URI baseUri, URI redirectUri, String realmId) {
|
||||||
|
return realmLogout(baseUri).queryParam("redirect_uri", redirectUri).build(realmId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static URI loginActionUpdatePassword(URI baseUri, String realmId) {
|
public static URI loginActionUpdatePassword(URI baseUri, String realmId) {
|
||||||
|
@ -128,6 +136,10 @@ public class Urls {
|
||||||
return tokenBase(baseUri).path(TokenService.class, "loginPage").build(realmId);
|
return tokenBase(baseUri).path(TokenService.class, "loginPage").build(realmId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static UriBuilder realmLogout(URI baseUri) {
|
||||||
|
return tokenBase(baseUri).path(TokenService.class, "logout");
|
||||||
|
}
|
||||||
|
|
||||||
public static URI realmRegisterAction(URI baseUri, String realmId) {
|
public static URI realmRegisterAction(URI baseUri, String realmId) {
|
||||||
return tokenBase(baseUri).path(TokenService.class, "processRegister").build(realmId);
|
return tokenBase(baseUri).path(TokenService.class, "processRegister").build(realmId);
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,6 +48,7 @@ import org.keycloak.testsuite.OAuthClient;
|
||||||
import org.keycloak.testsuite.Retry;
|
import org.keycloak.testsuite.Retry;
|
||||||
import org.keycloak.testsuite.pages.AccountLogPage;
|
import org.keycloak.testsuite.pages.AccountLogPage;
|
||||||
import org.keycloak.testsuite.pages.AccountPasswordPage;
|
import org.keycloak.testsuite.pages.AccountPasswordPage;
|
||||||
|
import org.keycloak.testsuite.pages.AccountSessionsPage;
|
||||||
import org.keycloak.testsuite.pages.AccountTotpPage;
|
import org.keycloak.testsuite.pages.AccountTotpPage;
|
||||||
import org.keycloak.testsuite.pages.AccountUpdateProfilePage;
|
import org.keycloak.testsuite.pages.AccountUpdateProfilePage;
|
||||||
import org.keycloak.testsuite.pages.AppPage;
|
import org.keycloak.testsuite.pages.AppPage;
|
||||||
|
@ -130,6 +131,9 @@ public class AccountTest {
|
||||||
@WebResource
|
@WebResource
|
||||||
protected AccountLogPage logPage;
|
protected AccountLogPage logPage;
|
||||||
|
|
||||||
|
@WebResource
|
||||||
|
protected AccountSessionsPage sessionsPage;
|
||||||
|
|
||||||
@WebResource
|
@WebResource
|
||||||
protected ErrorPage errorPage;
|
protected ErrorPage errorPage;
|
||||||
|
|
||||||
|
@ -212,7 +216,7 @@ public class AccountTest {
|
||||||
|
|
||||||
changePasswordPage.logout();
|
changePasswordPage.logout();
|
||||||
|
|
||||||
events.expectLogout(sessionId).detail(Details.REDIRECT_URI, ACCOUNT_URL).assertEvent();
|
events.expectLogout(sessionId).detail(Details.REDIRECT_URI, AccountPasswordPage.PATH).assertEvent();
|
||||||
|
|
||||||
loginPage.open();
|
loginPage.open();
|
||||||
loginPage.login("test-user@localhost", "password");
|
loginPage.login("test-user@localhost", "password");
|
||||||
|
@ -414,4 +418,41 @@ public class AccountTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void sessions() {
|
||||||
|
loginPage.open();
|
||||||
|
loginPage.clickRegister();
|
||||||
|
|
||||||
|
registerPage.register("view", "sessions", "view-sessions@localhost", "view-sessions", "password", "password");
|
||||||
|
|
||||||
|
Event registerEvent = events.expectRegister("view-sessions", "view-sessions@localhost").assertEvent();
|
||||||
|
String userId = registerEvent.getUserId();
|
||||||
|
|
||||||
|
events.expectLogin().user(userId).detail(Details.USERNAME, "view-sessions").assertEvent();
|
||||||
|
|
||||||
|
sessionsPage.open();
|
||||||
|
|
||||||
|
Assert.assertTrue(sessionsPage.isCurrent());
|
||||||
|
|
||||||
|
List<List<String>> sessions = sessionsPage.getSessions();
|
||||||
|
Assert.assertEquals(1, sessions.size());
|
||||||
|
Assert.assertEquals("127.0.0.1", sessions.get(0).get(0));
|
||||||
|
|
||||||
|
// Create second session
|
||||||
|
WebDriver driver2 = WebRule.createWebDriver();
|
||||||
|
OAuthClient oauth2 = new OAuthClient(driver2);
|
||||||
|
oauth2.doLogin("view-sessions", "password");
|
||||||
|
|
||||||
|
Event login2Event = events.expectLogin().user(userId).detail(Details.USERNAME, "view-sessions").assertEvent();
|
||||||
|
|
||||||
|
sessionsPage.open();
|
||||||
|
sessions = sessionsPage.getSessions();
|
||||||
|
Assert.assertEquals(2, sessions.size());
|
||||||
|
|
||||||
|
sessionsPage.logoutAll();
|
||||||
|
|
||||||
|
events.expectLogout(registerEvent.getSessionId());
|
||||||
|
events.expectLogout(login2Event.getSessionId());
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,11 +21,10 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.testsuite.pages;
|
package org.keycloak.testsuite.pages;
|
||||||
|
|
||||||
import org.keycloak.services.resources.AccountService;
|
import org.keycloak.services.resources.flows.Urls;
|
||||||
import org.keycloak.testsuite.Constants;
|
import org.keycloak.testsuite.Constants;
|
||||||
import org.openqa.selenium.By;
|
import org.openqa.selenium.By;
|
||||||
import org.openqa.selenium.WebElement;
|
import org.openqa.selenium.WebElement;
|
||||||
import org.openqa.selenium.support.FindBy;
|
|
||||||
|
|
||||||
import javax.ws.rs.core.UriBuilder;
|
import javax.ws.rs.core.UriBuilder;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
|
@ -36,7 +35,7 @@ import java.util.List;
|
||||||
*/
|
*/
|
||||||
public class AccountLogPage extends AbstractAccountPage {
|
public class AccountLogPage extends AbstractAccountPage {
|
||||||
|
|
||||||
private static String PATH = AccountService.logUrl(UriBuilder.fromUri(Constants.AUTH_SERVER_ROOT)).build("test").toString();
|
private static String PATH = Urls.accountLogPage(UriBuilder.fromUri(Constants.AUTH_SERVER_ROOT).build(), "test").toString();
|
||||||
|
|
||||||
public boolean isCurrent() {
|
public boolean isCurrent() {
|
||||||
return driver.getTitle().contains("Account Management") && driver.getCurrentUrl().endsWith("/account/log");
|
return driver.getTitle().contains("Account Management") && driver.getCurrentUrl().endsWith("/account/log");
|
||||||
|
|
|
@ -33,7 +33,7 @@ import javax.ws.rs.core.UriBuilder;
|
||||||
*/
|
*/
|
||||||
public class AccountPasswordPage extends AbstractAccountPage {
|
public class AccountPasswordPage extends AbstractAccountPage {
|
||||||
|
|
||||||
private static String PATH = AccountService.passwordUrl(UriBuilder.fromUri(Constants.AUTH_SERVER_ROOT)).build("test").toString();
|
public static String PATH = AccountService.passwordUrl(UriBuilder.fromUri(Constants.AUTH_SERVER_ROOT)).build("test").toString();
|
||||||
|
|
||||||
@FindBy(id = "password")
|
@FindBy(id = "password")
|
||||||
private WebElement passwordInput;
|
private WebElement passwordInput;
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
/*
|
||||||
|
* 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.pages;
|
||||||
|
|
||||||
|
import org.keycloak.services.resources.flows.Urls;
|
||||||
|
import org.keycloak.testsuite.Constants;
|
||||||
|
import org.openqa.selenium.By;
|
||||||
|
import org.openqa.selenium.WebElement;
|
||||||
|
import org.openqa.selenium.support.FindBy;
|
||||||
|
|
||||||
|
import javax.ws.rs.core.UriBuilder;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
|
*/
|
||||||
|
public class AccountSessionsPage extends AbstractAccountPage {
|
||||||
|
|
||||||
|
private static String PATH = Urls.accountSessionsPage(UriBuilder.fromUri(Constants.AUTH_SERVER_ROOT).build(), "test").toString();
|
||||||
|
|
||||||
|
@FindBy(id = "logout-all-sessions")
|
||||||
|
private WebElement logoutAllLink;
|
||||||
|
|
||||||
|
public boolean isCurrent() {
|
||||||
|
return driver.getTitle().contains("Account Management") && driver.getCurrentUrl().endsWith("/account/sessions");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void open() {
|
||||||
|
driver.navigate().to(PATH);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void logoutAll() {
|
||||||
|
logoutAllLink.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<List<String>> getSessions() {
|
||||||
|
List<List<String>> table = new LinkedList<List<String>>();
|
||||||
|
for (WebElement r : driver.findElements(By.tagName("tr"))) {
|
||||||
|
List<String> row = new LinkedList<String>();
|
||||||
|
for (WebElement col : r.findElements(By.tagName("td"))) {
|
||||||
|
row.add(col.getText());
|
||||||
|
}
|
||||||
|
table.add(row);
|
||||||
|
}
|
||||||
|
table.remove(0);
|
||||||
|
return table;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in a new issue