keycloak-memoizeit

Merge pull request #382 from stianst/master KEYCLOAK-431

5/14/2014 7:56:52 AM

Changes

Details

diff --git a/core/src/main/java/org/keycloak/util/Time.java b/core/src/main/java/org/keycloak/util/Time.java
index 3b3aef4..a7dc0fb 100644
--- a/core/src/main/java/org/keycloak/util/Time.java
+++ b/core/src/main/java/org/keycloak/util/Time.java
@@ -1,5 +1,7 @@
 package org.keycloak.util;
 
+import java.util.Date;
+
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
  */
@@ -9,4 +11,8 @@ public class Time {
         return (int) (System.currentTimeMillis() / 1000);
     }
 
+    public static Date toDate(int time) {
+        return new Date(((long) time ) * 1000);
+    }
+
 }
diff --git a/examples/demo-template/customer-app-cli/src/main/java/org/keycloak/example/CustomerCli.java b/examples/demo-template/customer-app-cli/src/main/java/org/keycloak/example/CustomerCli.java
index f04a283..824165b 100644
--- a/examples/demo-template/customer-app-cli/src/main/java/org/keycloak/example/CustomerCli.java
+++ b/examples/demo-template/customer-app-cli/src/main/java/org/keycloak/example/CustomerCli.java
@@ -7,6 +7,7 @@ import org.codehaus.jackson.map.SerializationConfig;
 import org.codehaus.jackson.map.annotate.JsonSerialize;
 import org.keycloak.adapters.ServerRequest;
 import org.keycloak.adapters.installed.KeycloakInstalled;
+import org.keycloak.util.Time;
 
 import java.io.BufferedReader;
 import java.io.IOException;
@@ -65,7 +66,7 @@ public class CustomerCli {
                     System.out.println(mapper.writeValueAsString(keycloak.getIdToken()));
                 } else if (s.equals("refresh")) {
                     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")) {
                     System.exit(0);
                 } else {
diff --git a/forms/account-api/src/main/java/org/keycloak/account/Account.java b/forms/account-api/src/main/java/org/keycloak/account/Account.java
index 5a62fec..fde5324 100644
--- a/forms/account-api/src/main/java/org/keycloak/account/Account.java
+++ b/forms/account-api/src/main/java/org/keycloak/account/Account.java
@@ -3,6 +3,7 @@ package org.keycloak.account;
 import org.keycloak.audit.Event;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserModel;
+import org.keycloak.models.UserSessionModel;
 
 import javax.ws.rs.core.MultivaluedMap;
 import javax.ws.rs.core.Response;
@@ -31,5 +32,7 @@ public interface Account {
 
     Account setEvents(List<Event> events);
 
+    Account setSessions(List<UserSessionModel> sessions);
+
     Account setFeatures(boolean social, boolean audit, boolean passwordUpdateSupported);
 }
diff --git a/forms/account-api/src/main/java/org/keycloak/account/AccountPages.java b/forms/account-api/src/main/java/org/keycloak/account/AccountPages.java
index ceeca40..dc7e3c0 100644
--- a/forms/account-api/src/main/java/org/keycloak/account/AccountPages.java
+++ b/forms/account-api/src/main/java/org/keycloak/account/AccountPages.java
@@ -5,6 +5,6 @@ package org.keycloak.account;
  */
 public enum AccountPages {
 
-    ACCOUNT, PASSWORD, TOTP, SOCIAL, LOG;
+    ACCOUNT, PASSWORD, TOTP, SOCIAL, LOG, SESSIONS;
 
 }
diff --git a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccount.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccount.java
index fb30dac..a2e7979 100755
--- a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccount.java
+++ b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccount.java
@@ -9,6 +9,7 @@ import org.keycloak.account.freemarker.model.FeaturesBean;
 import org.keycloak.account.freemarker.model.LogBean;
 import org.keycloak.account.freemarker.model.MessageBean;
 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.UrlBean;
 import org.keycloak.audit.Event;
@@ -19,6 +20,7 @@ import org.keycloak.freemarker.ThemeLoader;
 import org.keycloak.models.ApplicationModel;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserModel;
+import org.keycloak.models.UserSessionModel;
 
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
@@ -43,6 +45,7 @@ public class FreeMarkerAccount implements Account {
     private RealmModel realm;
     private String[] referrer;
     private List<Event> events;
+    private List<UserSessionModel> sessions;
     private boolean social;
     private boolean audit;
     private boolean passwordUpdateSupported;
@@ -100,7 +103,7 @@ public class FreeMarkerAccount implements Account {
             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));
 
@@ -116,6 +119,10 @@ public class FreeMarkerAccount implements Account {
                 break;
             case LOG:
                 attributes.put("log", new LogBean(events));
+                break;
+            case SESSIONS:
+                attributes.put("sessions", new SessionsBean(sessions));
+                break;
         }
 
         try {
@@ -179,6 +186,12 @@ public class FreeMarkerAccount implements Account {
     }
 
     @Override
+    public Account setSessions(List<UserSessionModel> sessions) {
+        this.sessions = sessions;
+        return this;
+    }
+
+    @Override
     public Account setFeatures(boolean social, boolean audit, boolean passwordUpdateSupported) {
         this.social = social;
         this.audit = audit;
diff --git a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/SessionsBean.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/SessionsBean.java
new file mode 100644
index 0000000..4db66dd
--- /dev/null
+++ b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/SessionsBean.java
@@ -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());
+        }
+
+    }
+
+}
diff --git a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/UrlBean.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/UrlBean.java
index 6467146..fce032f 100644
--- a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/UrlBean.java
+++ b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/UrlBean.java
@@ -2,6 +2,7 @@ package org.keycloak.account.freemarker.model;
 
 import org.keycloak.freemarker.Theme;
 import org.keycloak.models.RealmModel;
+import org.keycloak.services.resources.TokenService;
 import org.keycloak.services.resources.flows.Urls;
 
 import java.net.URI;
@@ -15,12 +16,14 @@ public class UrlBean {
     private Theme theme;
     private URI baseURI;
     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.theme = theme;
         this.baseURI = baseURI;
         this.baseQueryURI = baseQueryURI;
+        this.currentURI = currentURI;
     }
 
     public String getAccessUrl() {
@@ -47,12 +50,20 @@ public class UrlBean {
         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() {
         return Urls.accountTotpRemove(baseQueryURI, realm).toString();
     }
 
     public String getLogoutUrl() {
-        return Urls.accountLogout(baseQueryURI, realm).toString();
+        return Urls.accountLogout(baseQueryURI, currentURI, realm).toString();
     }
 
     public String getResourcesPath() {
diff --git a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/Templates.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/Templates.java
index f9d051c..80fc179 100644
--- a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/Templates.java
+++ b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/Templates.java
@@ -19,6 +19,8 @@ public class Templates {
                 return "social.ftl";
             case LOG:
                 return "log.ftl";
+            case SESSIONS:
+                return "sessions.ftl";
             default:
                 throw new IllegalArgumentException();
         }
diff --git a/forms/common-themes/src/main/resources/theme/account/base/sessions.ftl b/forms/common-themes/src/main/resources/theme/account/base/sessions.ftl
new file mode 100644
index 0000000..e424e51
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/account/base/sessions.ftl
@@ -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>
\ No newline at end of file
diff --git a/forms/common-themes/src/main/resources/theme/account/base/template.ftl b/forms/common-themes/src/main/resources/theme/account/base/template.ftl
index 883d8fb..49040df 100644
--- a/forms/common-themes/src/main/resources/theme/account/base/template.ftl
+++ b/forms/common-themes/src/main/resources/theme/account/base/template.ftl
@@ -43,6 +43,7 @@
                 <#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>
                 <#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>
             </ul>
         </div>
diff --git a/model/api/src/main/java/org/keycloak/models/RealmModel.java b/model/api/src/main/java/org/keycloak/models/RealmModel.java
index e3f41eb..ac91fc7 100755
--- a/model/api/src/main/java/org/keycloak/models/RealmModel.java
+++ b/model/api/src/main/java/org/keycloak/models/RealmModel.java
@@ -257,6 +257,8 @@ public interface RealmModel extends RoleContainerModel, RoleMapperModel, ScopeMa
 
     UserSessionModel getUserSession(String id);
 
+    List<UserSessionModel> getUserSessions(UserModel user);
+
     void removeUserSession(UserSessionModel session);
 
     void removeUserSessions(UserModel user);
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserSessionEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserSessionEntity.java
index 5d4fde7..83db701 100644
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserSessionEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserSessionEntity.java
@@ -15,6 +15,7 @@ import javax.persistence.NamedQuery;
  */
 @Entity
 @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 = "removeUserSessionExpired", query = "delete from UserSessionEntity s where s.expires < :currentTime")
 })
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
index d9c3784..d462e65 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
@@ -49,6 +49,7 @@ import java.util.Comparator;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -1406,6 +1407,15 @@ public class RealmAdapter implements RealmModel {
     }
 
     @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
     public void removeUserSession(UserSessionModel session) {
         em.remove(((UserSessionAdapter) session).getEntity());
     }
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
index cb0e8d7..9791740 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
@@ -45,6 +45,7 @@ import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -1376,6 +1377,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
     public void removeUserSession(UserSessionModel session) {
         getMongoStore().removeEntity(((UserSessionAdapter) session).getEntity(), invocationContext);
     }
diff --git a/services/src/main/java/org/keycloak/services/resources/AccountService.java b/services/src/main/java/org/keycloak/services/resources/AccountService.java
index 6c1142b..35c70f6 100755
--- a/services/src/main/java/org/keycloak/services/resources/AccountService.java
+++ b/services/src/main/java/org/keycloak/services/resources/AccountService.java
@@ -241,10 +241,6 @@ public class AccountService {
         return forwardToPage("social", AccountPages.SOCIAL);
     }
 
-    public static UriBuilder logUrl(UriBuilder base) {
-        return RealmsResource.accountUrl(base).path(AccountService.class, "logPage");
-    }
-
     @Path("log")
     @GET
     public Response logPage() {
@@ -269,6 +265,15 @@ public class AccountService {
         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("/")
     @POST
     @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@@ -314,6 +319,18 @@ public class AccountService {
         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")
     @POST
     @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) {
         OAuthRedirect oauth = new OAuthRedirect();
         String authUrl = Urls.realmLoginPage(uriInfo.getBaseUri(), realm.getName()).toString();
diff --git a/services/src/main/java/org/keycloak/services/resources/flows/Urls.java b/services/src/main/java/org/keycloak/services/resources/flows/Urls.java
index 435c34b..961fbcb 100755
--- a/services/src/main/java/org/keycloak/services/resources/flows/Urls.java
+++ b/services/src/main/java/org/keycloak/services/resources/flows/Urls.java
@@ -76,8 +76,16 @@ public class Urls {
         return accountBase(baseUri).path(AccountService.class, "logPage").build(realmId);
     }
 
-    public static URI accountLogout(URI baseUri, String realmId) {
-        return accountBase(baseUri).path(AccountService.class, "logout").build(realmId);
+    public static URI accountSessionsPage(URI baseUri, String 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) {
@@ -128,6 +136,10 @@ public class Urls {
         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) {
         return tokenBase(baseUri).path(TokenService.class, "processRegister").build(realmId);
     }
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java
index d3c5859..167ca7e 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java
@@ -48,6 +48,7 @@ import org.keycloak.testsuite.OAuthClient;
 import org.keycloak.testsuite.Retry;
 import org.keycloak.testsuite.pages.AccountLogPage;
 import org.keycloak.testsuite.pages.AccountPasswordPage;
+import org.keycloak.testsuite.pages.AccountSessionsPage;
 import org.keycloak.testsuite.pages.AccountTotpPage;
 import org.keycloak.testsuite.pages.AccountUpdateProfilePage;
 import org.keycloak.testsuite.pages.AppPage;
@@ -131,6 +132,9 @@ public class AccountTest {
     protected AccountLogPage logPage;
 
     @WebResource
+    protected AccountSessionsPage sessionsPage;
+
+    @WebResource
     protected ErrorPage errorPage;
 
     private TimeBasedOTP totp = new TimeBasedOTP();
@@ -212,7 +216,7 @@ public class AccountTest {
 
         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.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());
+    }
+
 }
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountLogPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountLogPage.java
index ab85433..846b526 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountLogPage.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountLogPage.java
@@ -21,11 +21,10 @@
  */
 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.openqa.selenium.By;
 import org.openqa.selenium.WebElement;
-import org.openqa.selenium.support.FindBy;
 
 import javax.ws.rs.core.UriBuilder;
 import java.util.LinkedList;
@@ -36,7 +35,7 @@ import java.util.List;
  */
 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() {
         return driver.getTitle().contains("Account Management") && driver.getCurrentUrl().endsWith("/account/log");
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountPasswordPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountPasswordPage.java
index d621c71..0dc027b 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountPasswordPage.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountPasswordPage.java
@@ -33,7 +33,7 @@ import javax.ws.rs.core.UriBuilder;
  */
 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")
     private WebElement passwordInput;
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountSessionsPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountSessionsPage.java
new file mode 100755
index 0000000..5467a48
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountSessionsPage.java
@@ -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;
+    }
+
+}