keycloak-aplcache

Changes

testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/ProfileTest.java 362(+0 -362)

Details

diff --git a/core/src/main/java/org/keycloak/representations/account/ClientRepresentation.java b/core/src/main/java/org/keycloak/representations/account/ClientRepresentation.java
new file mode 100644
index 0000000..c296426
--- /dev/null
+++ b/core/src/main/java/org/keycloak/representations/account/ClientRepresentation.java
@@ -0,0 +1,25 @@
+package org.keycloak.representations.account;
+
+/**
+ * Created by st on 29/03/17.
+ */
+public class ClientRepresentation {
+    private String clientId;
+    private String clientName;
+
+    public String getClientId() {
+        return clientId;
+    }
+
+    public void setClientId(String clientId) {
+        this.clientId = clientId;
+    }
+
+    public String getClientName() {
+        return clientName;
+    }
+
+    public void setClientName(String clientName) {
+        this.clientName = clientName;
+    }
+}
diff --git a/core/src/main/java/org/keycloak/representations/account/SessionRepresentation.java b/core/src/main/java/org/keycloak/representations/account/SessionRepresentation.java
new file mode 100644
index 0000000..8e4d96f
--- /dev/null
+++ b/core/src/main/java/org/keycloak/representations/account/SessionRepresentation.java
@@ -0,0 +1,64 @@
+package org.keycloak.representations.account;
+
+import java.util.List;
+
+/**
+ * Created by st on 29/03/17.
+ */
+public class SessionRepresentation {
+
+    private String id;
+    private String ipAddress;
+    private int started;
+    private int lastAccess;
+    private int expires;
+    private List<ClientRepresentation> clients;
+
+    public String getId() {
+        return id;
+    }
+
+    public void setId(String id) {
+        this.id = id;
+    }
+
+    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 getLastAccess() {
+        return lastAccess;
+    }
+
+    public void setLastAccess(int lastAccess) {
+        this.lastAccess = lastAccess;
+    }
+
+    public int getExpires() {
+        return expires;
+    }
+
+    public void setExpires(int expires) {
+        this.expires = expires;
+    }
+
+    public List<ClientRepresentation> getClients() {
+        return clients;
+    }
+
+    public void setClients(List<ClientRepresentation> clients) {
+        this.clients = clients;
+    }
+}
diff --git a/core/src/main/java/org/keycloak/representations/account/UserRepresentation.java b/core/src/main/java/org/keycloak/representations/account/UserRepresentation.java
new file mode 100755
index 0000000..f457b18
--- /dev/null
+++ b/core/src/main/java/org/keycloak/representations/account/UserRepresentation.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.representations.account;
+
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import org.keycloak.json.StringListMapDeserializer;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class UserRepresentation {
+
+    private String id;
+    private String username;
+    private String firstName;
+    private String lastName;
+    private String email;
+    private boolean emailVerified;
+
+    @JsonDeserialize(using = StringListMapDeserializer.class)
+    private Map<String, List<String>> attributes;
+
+    public String getId() {
+        return id;
+    }
+
+    public void setId(String id) {
+        this.id = id;
+    }
+
+    public String getUsername() {
+        return username;
+    }
+
+    public void setUsername(String username) {
+        this.username = username;
+    }
+
+    public String getFirstName() {
+        return firstName;
+    }
+
+    public void setFirstName(String firstName) {
+        this.firstName = firstName;
+    }
+
+    public String getLastName() {
+        return lastName;
+    }
+
+    public void setLastName(String lastName) {
+        this.lastName = lastName;
+    }
+
+    public String getEmail() {
+        return email;
+    }
+
+    public void setEmail(String email) {
+        this.email = email;
+    }
+
+    public boolean isEmailVerified() {
+        return emailVerified;
+    }
+
+    public void setEmailVerified(boolean emailVerified) {
+        this.emailVerified = emailVerified;
+    }
+
+    public Map<String, List<String>> getAttributes() {
+        return attributes;
+    }
+
+    public void setAttributes(Map<String, List<String>> attributes) {
+        this.attributes = attributes;
+    }
+
+}
diff --git a/services/src/main/java/org/keycloak/forms/account/freemarker/model/AccountFederatedIdentityBean.java b/services/src/main/java/org/keycloak/forms/account/freemarker/model/AccountFederatedIdentityBean.java
index 96f4257..54dd7bf 100755
--- a/services/src/main/java/org/keycloak/forms/account/freemarker/model/AccountFederatedIdentityBean.java
+++ b/services/src/main/java/org/keycloak/forms/account/freemarker/model/AccountFederatedIdentityBean.java
@@ -23,7 +23,7 @@ import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.models.utils.KeycloakModelUtils;
-import org.keycloak.services.resources.AccountService;
+import org.keycloak.services.resources.account.AccountFormService;
 import org.keycloak.services.Urls;
 
 import javax.ws.rs.core.UriBuilder;
@@ -80,7 +80,7 @@ public class AccountFederatedIdentityBean {
         this.identities = new LinkedList<FederatedIdentityEntry>(orderedSet); 
 
         // Removing last social provider is not possible if you don't have other possibility to authenticate
-        this.removeLinkPossible = availableIdentities > 1 || user.getFederationLink() != null || AccountService.isPasswordSet(session, realm, user);
+        this.removeLinkPossible = availableIdentities > 1 || user.getFederationLink() != null || AccountFormService.isPasswordSet(session, realm, user);
     }
 
     private FederatedIdentityModel getIdentity(Set<FederatedIdentityModel> identities, String providerId) {
diff --git a/services/src/main/java/org/keycloak/services/managers/AppAuthManager.java b/services/src/main/java/org/keycloak/services/managers/AppAuthManager.java
index 50972f4..bebcb2d 100755
--- a/services/src/main/java/org/keycloak/services/managers/AppAuthManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/AppAuthManager.java
@@ -58,6 +58,10 @@ public class AppAuthManager extends AuthenticationManager {
         return authenticateBearerToken(session, realm, ctx.getUri(), ctx.getConnection(), ctx.getRequestHeaders());
     }
 
+    public AuthResult authenticateBearerToken(KeycloakSession session) {
+        return authenticateBearerToken(session, session.getContext().getRealm(), session.getContext().getUri(), session.getContext().getConnection(), session.getContext().getRequestHeaders());
+    }
+
     public AuthResult authenticateBearerToken(KeycloakSession session, RealmModel realm, UriInfo uriInfo, ClientConnection connection, HttpHeaders headers) {
         String tokenString = extractAuthorizationHeaderToken(headers);
         if (tokenString == null) return null;
diff --git a/services/src/main/java/org/keycloak/services/managers/Auth.java b/services/src/main/java/org/keycloak/services/managers/Auth.java
index 8b6086e..7c97c8f 100755
--- a/services/src/main/java/org/keycloak/services/managers/Auth.java
+++ b/services/src/main/java/org/keycloak/services/managers/Auth.java
@@ -23,6 +23,7 @@ import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.models.UserSessionModel;
 import org.keycloak.representations.AccessToken;
+import org.keycloak.services.ForbiddenException;
 
 /**
 * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -79,6 +80,18 @@ public class Auth {
         this.clientSession = clientSession;
     }
 
+    public void require(String role) {
+        if (!hasClientRole(client, role)) {
+            throw new ForbiddenException();
+        }
+    }
+
+    public void requireOneOf(String... roles) {
+        if (!hasOneOfAppRole(client, roles)) {
+            throw new ForbiddenException();
+        }
+    }
+
     public boolean hasRealmRole(String role) {
         if (cookie) {
             return user.hasRole(realm.getRole(role));
diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
index 0235120..fa9fec6 100755
--- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
@@ -136,6 +136,19 @@ public class AuthenticationManager {
 
     }
 
+    public static void backchannelLogout(KeycloakSession session, UserSessionModel userSession, boolean logoutBroker) {
+        backchannelLogout(
+                session,
+                session.getContext().getRealm(),
+                userSession,
+                session.getContext().getUri(),
+                session.getContext().getConnection(),
+                session.getContext().getRequestHeaders(),
+                logoutBroker
+        );
+    }
+
+
     /**
      * Do not logout broker
      *
diff --git a/services/src/main/java/org/keycloak/services/messages/Messages.java b/services/src/main/java/org/keycloak/services/messages/Messages.java
index 180694a..35359f4 100755
--- a/services/src/main/java/org/keycloak/services/messages/Messages.java
+++ b/services/src/main/java/org/keycloak/services/messages/Messages.java
@@ -176,6 +176,8 @@ public class Messages {
 
     public static final String READ_ONLY_USER = "readOnlyUserMessage";
 
+    public static final String READ_ONLY_USERNAME = "readOnlyUsernameMessage";
+
     public static final String READ_ONLY_PASSWORD = "readOnlyPasswordMessage";
 
     public static final String SUCCESS_TOTP_REMOVED = "successTotpRemovedMessage";
diff --git a/services/src/main/java/org/keycloak/services/resources/AbstractSecuredLocalService.java b/services/src/main/java/org/keycloak/services/resources/AbstractSecuredLocalService.java
index cc8abfb..7b85795 100755
--- a/services/src/main/java/org/keycloak/services/resources/AbstractSecuredLocalService.java
+++ b/services/src/main/java/org/keycloak/services/resources/AbstractSecuredLocalService.java
@@ -203,32 +203,6 @@ public abstract class AbstractSecuredLocalService {
         return oauth.redirect(uriInfo, accountUri.toString());
     }
 
-    protected Response authenticateBrowser() {
-        AppAuthManager authManager = new AppAuthManager();
-        AuthenticationManager.AuthResult authResult = authManager.authenticateIdentityCookie(session, realm);
-        if (authResult != null) {
-            auth = new Auth(realm, authResult.getToken(), authResult.getUser(), client, authResult.getSession(), true);
-        } else {
-            return login(null);
-        }
-        // don't allow cors requests
-        // This is to prevent CSRF attacks.
-        String requestOrigin = UriUtils.getOrigin(uriInfo.getBaseUri());
-        String origin = headers.getRequestHeaders().getFirst("Origin");
-        if (origin != null && !requestOrigin.equals(origin)) {
-            throw new ForbiddenException();
-        }
-
-        if (!request.getHttpMethod().equals("GET")) {
-            String referrer = headers.getRequestHeaders().getFirst("Referer");
-            if (referrer != null && !requestOrigin.equals(UriUtils.getOrigin(referrer))) {
-                throw new ForbiddenException();
-            }
-        }
-        updateCsrfChecks();
-        return null;
-    }
-
     static class OAuthRedirect extends AbstractOAuthClient {
 
         /**
diff --git a/services/src/main/java/org/keycloak/services/resources/account/AccountLoader.java b/services/src/main/java/org/keycloak/services/resources/account/AccountLoader.java
new file mode 100644
index 0000000..11c3161
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/resources/account/AccountLoader.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.services.resources.account;
+
+import org.jboss.logging.Logger;
+import org.jboss.resteasy.spi.HttpRequest;
+import org.jboss.resteasy.spi.ResteasyProviderFactory;
+import org.keycloak.events.EventBuilder;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.Constants;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.services.managers.AppAuthManager;
+import org.keycloak.services.managers.Auth;
+import org.keycloak.services.managers.AuthenticationManager;
+
+import javax.ws.rs.HttpMethod;
+import javax.ws.rs.NotAuthorizedException;
+import javax.ws.rs.NotFoundException;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class AccountLoader {
+
+    private static final Logger logger = Logger.getLogger(AccountLoader.class);
+
+    private AccountLoader() {
+    }
+
+    public static Object getAccountService(KeycloakSession session, EventBuilder event) {
+        RealmModel realm = session.getContext().getRealm();
+
+        ClientModel client = realm.getClientByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID);
+        if (client == null || !client.isEnabled()) {
+            logger.debug("account management not enabled");
+            throw new NotFoundException("account management not enabled");
+        }
+
+        HttpRequest request = session.getContext().getContextObject(HttpRequest.class);
+        HttpHeaders headers = session.getContext().getRequestHeaders();
+        MediaType content = headers.getMediaType();
+        List<MediaType> accepts = headers.getAcceptableMediaTypes();
+
+        if (request.getHttpMethod().equals(HttpMethod.OPTIONS)) {
+            return new CorsPreflightService(request);
+        } else if ((accepts.contains(MediaType.APPLICATION_JSON_TYPE) || MediaType.APPLICATION_JSON_TYPE.equals(content)) && !request.getUri().getPath().endsWith("keycloak.json")) {
+            AuthenticationManager.AuthResult authResult = new AppAuthManager().authenticateBearerToken(session);
+            if (authResult == null) {
+                throw new NotAuthorizedException("Bearer token required");
+            }
+
+            Auth auth = new Auth(session.getContext().getRealm(), authResult.getToken(), authResult.getUser(), client, authResult.getSession(), false);
+            AccountRestService accountRestService = new AccountRestService(session, auth, client, event);
+            ResteasyProviderFactory.getInstance().injectProperties(accountRestService);
+            accountRestService.init();
+            return accountRestService;
+        } else {
+            AccountFormService accountFormService = new AccountFormService(realm, client, event);
+            ResteasyProviderFactory.getInstance().injectProperties(accountFormService);
+            accountFormService.init();
+            return accountFormService;
+        }
+    }
+
+}
diff --git a/services/src/main/java/org/keycloak/services/resources/account/AccountRestService.java b/services/src/main/java/org/keycloak/services/resources/account/AccountRestService.java
new file mode 100755
index 0000000..e327519
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/resources/account/AccountRestService.java
@@ -0,0 +1,270 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.services.resources.account;
+
+import org.jboss.resteasy.annotations.cache.NoCache;
+import org.jboss.resteasy.spi.HttpRequest;
+import org.keycloak.common.ClientConnection;
+import org.keycloak.events.Details;
+import org.keycloak.events.EventBuilder;
+import org.keycloak.events.EventStoreProvider;
+import org.keycloak.events.EventType;
+import org.keycloak.models.AccountRoles;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.UserSessionModel;
+import org.keycloak.representations.account.ClientRepresentation;
+import org.keycloak.representations.account.SessionRepresentation;
+import org.keycloak.representations.account.UserRepresentation;
+import org.keycloak.services.ErrorResponse;
+import org.keycloak.services.managers.Auth;
+import org.keycloak.services.managers.AuthenticationManager;
+import org.keycloak.services.resources.Cors;
+import org.keycloak.storage.ReadOnlyException;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.OPTIONS;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class AccountRestService {
+
+    @Context
+    private HttpRequest request;
+    @Context
+    protected UriInfo uriInfo;
+    @Context
+    protected HttpHeaders headers;
+    @Context
+    protected ClientConnection clientConnection;
+
+    private final KeycloakSession session;
+    private final ClientModel client;
+    private final EventBuilder event;
+    private EventStoreProvider eventStore;
+    private Auth auth;
+    
+    private final RealmModel realm;
+    private final UserModel user;
+
+    public AccountRestService(KeycloakSession session, Auth auth, ClientModel client, EventBuilder event) {
+        this.session = session;
+        this.auth = auth;
+        this.realm = auth.getRealm();
+        this.user = auth.getUser();
+        this.client = client;
+        this.event = event;
+    }
+    
+    public void init() {
+        eventStore = session.getProvider(EventStoreProvider.class);
+    }
+
+    /**
+     * CORS preflight
+     *
+     * @return
+     */
+    @Path("/")
+    @OPTIONS
+    @NoCache
+    public Response preflight() {
+        return Cors.add(request, Response.ok()).auth().preflight().build();
+    }
+
+    /**
+     * Get account information.
+     *
+     * @return
+     */
+    @Path("/")
+    @GET
+    @Produces(MediaType.APPLICATION_JSON)
+    @NoCache
+    public Response account() {
+        auth.requireOneOf(AccountRoles.MANAGE_ACCOUNT, AccountRoles.VIEW_PROFILE);
+
+        UserModel user = auth.getUser();
+
+        UserRepresentation rep = new UserRepresentation();
+        rep.setUsername(user.getUsername());
+        rep.setFirstName(user.getFirstName());
+        rep.setLastName(user.getLastName());
+        rep.setEmail(user.getEmail());
+        rep.setEmailVerified(user.isEmailVerified());
+        rep.setAttributes(user.getAttributes());
+
+        return Cors.add(request, Response.ok(rep)).auth().allowedOrigins(auth.getToken()).build();
+    }
+
+    @Path("/")
+    @POST
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    @NoCache
+    public Response updateAccount(UserRepresentation userRep) {
+        auth.require(AccountRoles.MANAGE_ACCOUNT);
+
+        event.event(EventType.UPDATE_PROFILE).client(auth.getClient()).user(user);
+
+        try {
+            RealmModel realm = session.getContext().getRealm();
+
+            boolean usernameChanged = userRep.getUsername() != null && !userRep.getUsername().equals(user.getUsername());
+            if (realm.isEditUsernameAllowed()) {
+                if (usernameChanged) {
+                    UserModel existing = session.users().getUserByUsername(userRep.getUsername(), realm);
+                    if (existing != null) {
+                        return ErrorResponse.exists(Errors.USERNAME_EXISTS);
+                    }
+
+                    user.setUsername(userRep.getUsername());
+                }
+            } else if (usernameChanged) {
+                return ErrorResponse.error(Errors.READ_ONLY_USERNAME, Response.Status.BAD_REQUEST);
+            }
+
+            boolean emailChanged = userRep.getEmail() != null && !userRep.getEmail().equals(user.getEmail());
+            if (emailChanged && !realm.isDuplicateEmailsAllowed()) {
+                UserModel existing = session.users().getUserByEmail(userRep.getEmail(), realm);
+                if (existing != null) {
+                    return ErrorResponse.exists(Errors.EMAIL_EXISTS);
+                }
+            }
+
+            if (realm.isRegistrationEmailAsUsername() && !realm.isDuplicateEmailsAllowed()) {
+                UserModel existing = session.users().getUserByUsername(userRep.getEmail(), realm);
+                if (existing != null) {
+                    return ErrorResponse.exists(Errors.USERNAME_EXISTS);
+                }
+            }
+
+            if (emailChanged) {
+                String oldEmail = user.getEmail();
+                user.setEmail(userRep.getEmail());
+                user.setEmailVerified(false);
+                event.clone().event(EventType.UPDATE_EMAIL).detail(Details.PREVIOUS_EMAIL, oldEmail).detail(Details.UPDATED_EMAIL, userRep.getEmail()).success();
+
+                if (realm.isRegistrationEmailAsUsername()) {
+                    user.setUsername(userRep.getEmail());
+                }
+            }
+
+            user.setFirstName(userRep.getFirstName());
+            user.setLastName(userRep.getLastName());
+
+            if (userRep.getAttributes() != null) {
+                for (String k : user.getAttributes().keySet()) {
+                    if (!userRep.getAttributes().containsKey(k)) {
+                        user.removeAttribute(k);
+                    }
+                }
+
+                for (Map.Entry<String, List<String>> e : userRep.getAttributes().entrySet()) {
+                    user.setAttribute(e.getKey(), e.getValue());
+                }
+            }
+
+            event.success();
+
+            return Cors.add(request, Response.ok()).auth().allowedOrigins(auth.getToken()).build();
+        } catch (ReadOnlyException e) {
+            return ErrorResponse.error(Errors.READ_ONLY_USER, Response.Status.BAD_REQUEST);
+        }
+    }
+
+    /**
+     * Get session information.
+     *
+     * @return
+     */
+    @Path("/sessions")
+    @GET
+    @Produces(MediaType.APPLICATION_JSON)
+    @NoCache
+    public Response sessions() {
+        List<SessionRepresentation> reps = new LinkedList<>();
+
+        List<UserSessionModel> sessions = session.sessions().getUserSessions(realm, user);
+        for (UserSessionModel s : sessions) {
+            SessionRepresentation rep = new SessionRepresentation();
+            rep.setId(s.getId());
+            rep.setIpAddress(s.getIpAddress());
+            rep.setStarted(s.getStarted());
+            rep.setLastAccess(s.getLastSessionRefresh());
+            rep.setExpires(s.getStarted() + realm.getSsoSessionMaxLifespan());
+            rep.setClients(new LinkedList());
+
+            for (String clientUUID : s.getAuthenticatedClientSessions().keySet()) {
+                ClientModel client = realm.getClientById(clientUUID);
+                ClientRepresentation clientRep = new ClientRepresentation();
+                clientRep.setClientId(client.getClientId());
+                clientRep.setClientName(client.getName());
+                rep.getClients().add(clientRep);
+            }
+
+            reps.add(rep);
+        }
+
+        return Cors.add(request, Response.ok(reps)).auth().allowedOrigins(auth.getToken()).build();
+    }
+
+    /**
+     * Remove sessions
+     *
+     * @param removeCurrent remove current session (default is false)
+     * @return
+     */
+    @Path("/sessions")
+    @DELETE
+    @Produces(MediaType.APPLICATION_JSON)
+    @NoCache
+    public Response sessionsLogout(@QueryParam("current") boolean removeCurrent) {
+        UserSessionModel userSession = auth.getSession();
+
+        List<UserSessionModel> userSessions = session.sessions().getUserSessions(realm, user);
+        for (UserSessionModel s : userSessions) {
+            if (removeCurrent || !s.getId().equals(userSession.getId())) {
+                AuthenticationManager.backchannelLogout(session, s, true);
+            }
+        }
+
+        return Cors.add(request, Response.ok()).auth().allowedOrigins(auth.getToken()).build();
+    }
+
+    // TODO Federated identities
+    // TODO Applications
+    // TODO Logs
+
+}
diff --git a/services/src/main/java/org/keycloak/services/resources/account/Constants.java b/services/src/main/java/org/keycloak/services/resources/account/Constants.java
new file mode 100644
index 0000000..95b76de
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/resources/account/Constants.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.services.resources.account;
+
+import org.keycloak.events.Details;
+import org.keycloak.events.EventType;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class Constants {
+
+    public static final EventType[] EXPOSED_LOG_EVENTS = {
+            EventType.LOGIN, EventType.LOGOUT, EventType.REGISTER, EventType.REMOVE_FEDERATED_IDENTITY, EventType.REMOVE_TOTP, EventType.SEND_RESET_PASSWORD,
+            EventType.SEND_VERIFY_EMAIL, EventType.FEDERATED_IDENTITY_LINK, EventType.UPDATE_EMAIL, EventType.UPDATE_PASSWORD, EventType.UPDATE_PROFILE, EventType.UPDATE_TOTP, EventType.VERIFY_EMAIL
+    };
+
+    public static final Set<String> EXPOSED_LOG_DETAILS = new HashSet<>();
+
+    static {
+        EXPOSED_LOG_DETAILS.add(Details.UPDATED_EMAIL);
+        EXPOSED_LOG_DETAILS.add(Details.EMAIL);
+        EXPOSED_LOG_DETAILS.add(Details.PREVIOUS_EMAIL);
+        EXPOSED_LOG_DETAILS.add(Details.USERNAME);
+        EXPOSED_LOG_DETAILS.add(Details.REMEMBER_ME);
+        EXPOSED_LOG_DETAILS.add(Details.REGISTER_METHOD);
+        EXPOSED_LOG_DETAILS.add(Details.AUTH_METHOD);
+    }
+
+}
diff --git a/services/src/main/java/org/keycloak/services/resources/account/CorsPreflightService.java b/services/src/main/java/org/keycloak/services/resources/account/CorsPreflightService.java
new file mode 100644
index 0000000..f9c0fa6
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/resources/account/CorsPreflightService.java
@@ -0,0 +1,33 @@
+package org.keycloak.services.resources.account;
+
+import org.jboss.resteasy.spi.HttpRequest;
+import org.keycloak.services.resources.Cors;
+
+import javax.ws.rs.OPTIONS;
+import javax.ws.rs.Path;
+import javax.ws.rs.core.Response;
+
+/**
+ * Created by st on 21/03/17.
+ */
+public class CorsPreflightService {
+
+    private HttpRequest request;
+
+    public CorsPreflightService(HttpRequest request) {
+        this.request = request;
+    }
+
+    /**
+     * CORS preflight
+     *
+     * @return
+     */
+    @Path("/")
+    @OPTIONS
+    public Response preflight() {
+        Cors cors = Cors.add(request, Response.ok()).auth().allowedMethods("GET", "POST", "HEAD", "OPTIONS").preflight();
+        return cors.build();
+    }
+
+}
diff --git a/services/src/main/java/org/keycloak/services/resources/account/Errors.java b/services/src/main/java/org/keycloak/services/resources/account/Errors.java
new file mode 100644
index 0000000..6cb55bd
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/resources/account/Errors.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.services.resources.account;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class Errors {
+
+    public static final String USERNAME_EXISTS = "username_exists";
+    public static final String EMAIL_EXISTS = "email_exists";
+    public static final String READ_ONLY_USER = "user_read_only";
+    public static final String READ_ONLY_USERNAME = "username_read_only";
+
+}
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java
index 21943cc..5af5beb 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java
@@ -68,8 +68,8 @@ import org.keycloak.services.ServicesLogger;
 import org.keycloak.services.managers.AuthenticationManager;
 import org.keycloak.services.managers.BruteForceProtector;
 import org.keycloak.services.managers.UserSessionManager;
-import org.keycloak.services.resources.AccountService;
 import org.keycloak.services.resources.LoginActionsService;
+import org.keycloak.services.resources.account.AccountFormService;
 import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
 import org.keycloak.services.validation.Validation;
 import org.keycloak.storage.ReadOnlyException;
@@ -282,7 +282,7 @@ public class UserResource {
         String sessionId = KeycloakModelUtils.generateId();
         UserSessionModel userSession = session.sessions().createUserSession(sessionId, realm, user, user.getUsername(), clientConnection.getRemoteAddr(), "impersonate", false, null, null);
         AuthenticationManager.createLoginCookie(session, realm, userSession.getUser(), userSession, uriInfo, clientConnection);
-        URI redirect = AccountService.accountServiceApplicationPage(uriInfo).build(realm.getName());
+        URI redirect = AccountFormService.accountServiceApplicationPage(uriInfo).build(realm.getName());
         Map<String, Object> result = new HashMap<>();
         result.put("sameRealm", sameRealm);
         result.put("redirect", redirect.toString());
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
index c7b9945..578885d 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
@@ -68,7 +68,6 @@ import org.keycloak.services.managers.AuthenticationManager;
 import org.keycloak.services.managers.BruteForceProtector;
 import org.keycloak.services.*;
 import org.keycloak.services.managers.*;
-import org.keycloak.services.resources.AccountService;
 import org.keycloak.services.resources.LoginActionsService;
 import org.keycloak.services.validation.Validation;
 import org.keycloak.storage.ReadOnlyException;
diff --git a/services/src/main/java/org/keycloak/services/resources/Cors.java b/services/src/main/java/org/keycloak/services/resources/Cors.java
index c9bfa03..b647c75 100755
--- a/services/src/main/java/org/keycloak/services/resources/Cors.java
+++ b/services/src/main/java/org/keycloak/services/resources/Cors.java
@@ -108,28 +108,32 @@ public class Cors {
 
     public Cors allowedOrigins(String... allowedOrigins) {
         if (allowedOrigins != null && allowedOrigins.length > 0) {
-            this.allowedOrigins = new HashSet<String>(Arrays.asList(allowedOrigins));
+            this.allowedOrigins = new HashSet<>(Arrays.asList(allowedOrigins));
         }
         return this;
     }
 
     public Cors allowedMethods(String... allowedMethods) {
-        this.allowedMethods = new HashSet<String>(Arrays.asList(allowedMethods));
+        this.allowedMethods = new HashSet<>(Arrays.asList(allowedMethods));
         return this;
     }
 
     public Cors exposedHeaders(String... exposedHeaders) {
-        this.exposedHeaders = new HashSet<String>(Arrays.asList(exposedHeaders));
+        this.exposedHeaders = new HashSet<>(Arrays.asList(exposedHeaders));
         return this;
     }
 
     public Response build() {
         String origin = request.getHttpHeaders().getRequestHeaders().getFirst(ORIGIN_HEADER);
         if (origin == null) {
+            logger.trace("No origin header ignoring");
             return builder.build();
         }
 
         if (!preflight && (allowedOrigins == null || (!allowedOrigins.contains(origin) && !allowedOrigins.contains(ACCESS_CONTROL_ALLOW_ORIGIN_WILDCARD)))) {
+            if (logger.isDebugEnabled()) {
+                logger.debugv("Invalid CORS request: origin {0} not in allowed origins {1}", origin, Arrays.toString(allowedOrigins.toArray()));
+            }
             return builder.build();
         }
 
@@ -165,23 +169,25 @@ public class Cors {
             builder.header(ACCESS_CONTROL_MAX_AGE, DEFAULT_MAX_AGE);
         }
 
+        logger.debug("Added CORS headers to response");
+
         return builder.build();
     }
 
     public void build(HttpResponse response) {
         String origin = request.getHttpHeaders().getRequestHeaders().getFirst(ORIGIN_HEADER);
         if (origin == null) {
-            logger.debug("No origin returning");
+            logger.trace("No origin header ignoring");
             return;
         }
 
         if (!preflight && (allowedOrigins == null || (!allowedOrigins.contains(origin) && !allowedOrigins.contains(ACCESS_CONTROL_ALLOW_ORIGIN_WILDCARD)))) {
-            logger.debug("!preflight and no origin");
+            if (logger.isDebugEnabled()) {
+                logger.debugv("Invalid CORS request: origin {0} not in allowed origins {1}", origin, Arrays.toString(allowedOrigins.toArray()));
+            }
             return;
         }
 
-        logger.debug("build CORS headers and return");
-
         if (allowedOrigins.contains(ACCESS_CONTROL_ALLOW_ORIGIN_WILDCARD)) {
             response.getOutputHeaders().add(ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_ALLOW_ORIGIN_WILDCARD);
         } else {
@@ -213,6 +219,8 @@ public class Cors {
         if (preflight) {
             response.getOutputHeaders().add(ACCESS_CONTROL_MAX_AGE, DEFAULT_MAX_AGE);
         }
+
+        logger.debug("Added CORS headers to response");
     }
 
 }
diff --git a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java
index 7961163..2ef481e 100755
--- a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java
+++ b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java
@@ -37,7 +37,6 @@ import org.keycloak.common.ClientConnection;
 import org.keycloak.common.util.Base64Url;
 import org.keycloak.common.util.ObjectUtil;
 import org.keycloak.common.util.Time;
-import org.keycloak.common.util.UriUtils;
 import org.keycloak.events.Details;
 import org.keycloak.events.Errors;
 import org.keycloak.events.EventBuilder;
@@ -77,6 +76,7 @@ import org.keycloak.services.managers.AuthenticationSessionManager;
 import org.keycloak.services.managers.BruteForceProtector;
 import org.keycloak.services.managers.ClientSessionCode;
 import org.keycloak.services.messages.Messages;
+import org.keycloak.services.resources.account.AccountFormService;
 import org.keycloak.services.util.BrowserHistoryHelper;
 import org.keycloak.services.util.CacheControlUtil;
 import org.keycloak.services.validation.Validation;
@@ -1082,7 +1082,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
         FormMessage errorMessage = new FormMessage(message, parameters);
         try {
             String serializedError = JsonSerialization.writeValueAsString(errorMessage);
-            authSession.setAuthNote(AccountService.ACCOUNT_MGMT_FORWARDED_ERROR_NOTE, serializedError);
+            authSession.setAuthNote(AccountFormService.ACCOUNT_MGMT_FORWARDED_ERROR_NOTE, serializedError);
         } catch (IOException ioe) {
             throw new RuntimeException(ioe);
         }
diff --git a/services/src/main/java/org/keycloak/services/resources/PublicRealmResource.java b/services/src/main/java/org/keycloak/services/resources/PublicRealmResource.java
index 7526139..2890229 100755
--- a/services/src/main/java/org/keycloak/services/resources/PublicRealmResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/PublicRealmResource.java
@@ -25,6 +25,7 @@ import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
 import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
 import org.keycloak.representations.idm.PublishedRealmRepresentation;
+import org.keycloak.services.resources.account.AccountFormService;
 import org.keycloak.services.resources.admin.AdminRoot;
 
 import javax.ws.rs.GET;
@@ -91,7 +92,7 @@ public class PublicRealmResource {
         PublishedRealmRepresentation rep = new PublishedRealmRepresentation();
         rep.setRealm(realm.getName());
         rep.setTokenServiceUrl(OIDCLoginProtocolService.tokenServiceBaseUrl(uriInfo).build(realm.getName()).toString());
-        rep.setAccountServiceUrl(AccountService.accountServiceBaseUrl(uriInfo).build(realm.getName()).toString());
+        rep.setAccountServiceUrl(AccountFormService.accountServiceBaseUrl(uriInfo).build(realm.getName()).toString());
         rep.setAdminApiUrl(uriInfo.getBaseUriBuilder().path(AdminRoot.class).build().toString());
         rep.setPublicKeyPem(PemUtils.encodeKey(session.keys().getActiveRsaKey(realm).getPublicKey()));
         rep.setNotBefore(realm.getNotBefore());
diff --git a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java
index bc3f8dc..18a6fd9 100755
--- a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java
@@ -26,7 +26,6 @@ import org.keycloak.common.Profile;
 import org.keycloak.common.util.KeycloakUriBuilder;
 import org.keycloak.events.EventBuilder;
 import org.keycloak.models.ClientModel;
-import org.keycloak.models.Constants;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
 import org.keycloak.protocol.LoginProtocol;
@@ -34,6 +33,7 @@ import org.keycloak.protocol.LoginProtocolFactory;
 import org.keycloak.services.clientregistration.ClientRegistrationService;
 import org.keycloak.services.managers.RealmManager;
 import org.keycloak.services.resource.RealmResourceProvider;
+import org.keycloak.services.resources.account.AccountLoader;
 import org.keycloak.services.util.CacheControlUtil;
 import org.keycloak.services.util.ResolveRelative;
 import org.keycloak.utils.ProfileHelper;
@@ -206,20 +206,10 @@ public class RealmsResource {
     }
 
     @Path("{realm}/account")
-    public AccountService getAccountService(final @PathParam("realm") String name) {
+    public Object getAccountService(final @PathParam("realm") String name) {
         RealmModel realm = init(name);
-
-        ClientModel client = realm.getClientByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID);
-        if (client == null || !client.isEnabled()) {
-            logger.debug("account management not enabled");
-            throw new NotFoundException("account management not enabled");
-        }
-
         EventBuilder event = new EventBuilder(realm, session, clientConnection);
-        AccountService accountService = new AccountService(realm, client, event);
-        ResteasyProviderFactory.getInstance().injectProperties(accountService);
-        accountService.init();
-        return accountService;
+        return AccountLoader.getAccountService(session, event);
     }
 
     @Path("{realm}")
diff --git a/services/src/main/java/org/keycloak/services/Urls.java b/services/src/main/java/org/keycloak/services/Urls.java
index 51f505e..cb023fa 100755
--- a/services/src/main/java/org/keycloak/services/Urls.java
+++ b/services/src/main/java/org/keycloak/services/Urls.java
@@ -21,7 +21,7 @@ import org.keycloak.common.Version;
 import org.keycloak.models.Constants;
 import org.keycloak.protocol.oidc.OIDCLoginProtocol;
 import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
-import org.keycloak.services.resources.AccountService;
+import org.keycloak.services.resources.account.AccountFormService;
 import org.keycloak.services.resources.IdentityBrokerService;
 import org.keycloak.services.resources.LoginActionsService;
 import org.keycloak.services.resources.RealmsResource;
@@ -41,7 +41,7 @@ public class Urls {
     }
 
     public static URI accountApplicationsPage(URI baseUri, String realmName) {
-        return accountBase(baseUri).path(AccountService.class, "applicationsPage").build(realmName);
+        return accountBase(baseUri).path(AccountFormService.class, "applicationsPage").build(realmName);
     }
 
     public static UriBuilder accountBase(URI baseUri) {
@@ -53,19 +53,19 @@ public class Urls {
     }
 
     public static UriBuilder accountPageBuilder(URI baseUri) {
-        return accountBase(baseUri).path(AccountService.class, "accountPage");
+        return accountBase(baseUri).path(AccountFormService.class, "accountPage");
     }
 
     public static URI accountPasswordPage(URI baseUri, String realmName) {
-        return accountBase(baseUri).path(AccountService.class, "passwordPage").build(realmName);
+        return accountBase(baseUri).path(AccountFormService.class, "passwordPage").build(realmName);
     }
 
     public static URI accountFederatedIdentityPage(URI baseUri, String realmName) {
-        return accountBase(baseUri).path(AccountService.class, "federatedIdentityPage").build(realmName);
+        return accountBase(baseUri).path(AccountFormService.class, "federatedIdentityPage").build(realmName);
     }
 
     public static URI accountFederatedIdentityUpdate(URI baseUri, String realmName) {
-        return accountBase(baseUri).path(AccountService.class, "processFederatedIdentityUpdate").build(realmName);
+        return accountBase(baseUri).path(AccountFormService.class, "processFederatedIdentityUpdate").build(realmName);
     }
 
     public static URI identityProviderAuthnResponse(URI baseUri, String providerId, String realmName) {
@@ -123,31 +123,31 @@ public class Urls {
     }
 
     public static URI accountTotpPage(URI baseUri, String realmName) {
-        return accountBase(baseUri).path(AccountService.class, "totpPage").build(realmName);
+        return accountBase(baseUri).path(AccountFormService.class, "totpPage").build(realmName);
     }
 
     public static URI accountTotpRemove(URI baseUri, String realmName, String stateChecker) {
-        return accountBase(baseUri).path(AccountService.class, "processTotpRemove")
+        return accountBase(baseUri).path(AccountFormService.class, "processTotpRemove")
                 .queryParam("stateChecker", stateChecker)
                 .build(realmName);
     }
 
     public static URI accountLogPage(URI baseUri, String realmName) {
-        return accountBase(baseUri).path(AccountService.class, "logPage").build(realmName);
+        return accountBase(baseUri).path(AccountFormService.class, "logPage").build(realmName);
     }
 
     public static URI accountSessionsPage(URI baseUri, String realmName) {
-        return accountBase(baseUri).path(AccountService.class, "sessionsPage").build(realmName);
+        return accountBase(baseUri).path(AccountFormService.class, "sessionsPage").build(realmName);
     }
 
     public static URI accountSessionsLogoutPage(URI baseUri, String realmName, String stateChecker) {
-        return accountBase(baseUri).path(AccountService.class, "processSessionsLogout")
+        return accountBase(baseUri).path(AccountFormService.class, "processSessionsLogout")
                 .queryParam("stateChecker", stateChecker)
                 .build(realmName);
     }
 
     public static URI accountRevokeClientPage(URI baseUri, String realmName) {
-        return accountBase(baseUri).path(AccountService.class, "processRevokeGrant")
+        return accountBase(baseUri).path(AccountFormService.class, "processRevokeGrant")
                 .build(realmName);
     }
 
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 1e1da65..c334074 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
@@ -16,7 +16,7 @@
  */
 package org.keycloak.testsuite.pages;
 
-import org.keycloak.services.resources.AccountService;
+import org.keycloak.services.resources.account.AccountFormService;
 import org.keycloak.testsuite.Constants;
 import org.openqa.selenium.WebElement;
 import org.openqa.selenium.support.FindBy;
@@ -70,6 +70,6 @@ public class AccountPasswordPage extends AbstractAccountPage {
     }
 
     public String getPath() {
-        return AccountService.passwordUrl(UriBuilder.fromUri(Constants.AUTH_SERVER_ROOT)).build(this.realmName).toString();
+        return AccountFormService.passwordUrl(UriBuilder.fromUri(Constants.AUTH_SERVER_ROOT)).build(this.realmName).toString();
     }
 }
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountTotpPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountTotpPage.java
index a715c56..8d1dfac 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountTotpPage.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountTotpPage.java
@@ -16,7 +16,7 @@
  */
 package org.keycloak.testsuite.pages;
 
-import org.keycloak.services.resources.AccountService;
+import org.keycloak.services.resources.account.AccountFormService;
 import org.keycloak.testsuite.Constants;
 import org.openqa.selenium.WebElement;
 import org.openqa.selenium.support.FindBy;
@@ -28,7 +28,7 @@ import javax.ws.rs.core.UriBuilder;
  */
 public class AccountTotpPage extends AbstractAccountPage {
 
-    private static String PATH = AccountService.totpUrl(UriBuilder.fromUri(Constants.AUTH_SERVER_ROOT)).build("test").toString();
+    private static String PATH = AccountFormService.totpUrl(UriBuilder.fromUri(Constants.AUTH_SERVER_ROOT)).build("test").toString();
 
     @FindBy(id = "totpSecret")
     private WebElement totpSecret;
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/AccountPasswordPage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/AccountPasswordPage.java
index 2c98c55..d62806f 100755
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/AccountPasswordPage.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/AccountPasswordPage.java
@@ -16,7 +16,7 @@
  */
 package org.keycloak.testsuite.pages;
 
-import org.keycloak.services.resources.AccountService;
+import org.keycloak.services.resources.account.AccountFormService;
 import org.openqa.selenium.WebElement;
 import org.openqa.selenium.support.FindBy;
 
@@ -69,6 +69,6 @@ public class AccountPasswordPage extends AbstractAccountPage {
     }
 
     public String getPath() {
-        return AccountService.passwordUrl(UriBuilder.fromUri(getAuthServerRoot())).build(this.realmName).toString();
+        return AccountFormService.passwordUrl(UriBuilder.fromUri(getAuthServerRoot())).build(this.realmName).toString();
     }
 }
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/AccountTotpPage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/AccountTotpPage.java
index 1029e10..6527476 100755
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/AccountTotpPage.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/AccountTotpPage.java
@@ -16,7 +16,7 @@
  */
 package org.keycloak.testsuite.pages;
 
-import org.keycloak.services.resources.AccountService;
+import org.keycloak.services.resources.account.AccountFormService;
 import org.openqa.selenium.WebElement;
 import org.openqa.selenium.support.FindBy;
 
@@ -40,7 +40,7 @@ public class AccountTotpPage extends AbstractAccountPage {
     private WebElement removeLink;
 
     private String getPath() {
-        return AccountService.totpUrl(UriBuilder.fromUri(getAuthServerRoot())).build("test").toString();
+        return AccountFormService.totpUrl(UriBuilder.fromUri(getAuthServerRoot())).build("test").toString();
     }
 
     public void configure(String totp) {
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/TokenUtil.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/TokenUtil.java
new file mode 100644
index 0000000..a75f5cd
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/TokenUtil.java
@@ -0,0 +1,85 @@
+package org.keycloak.testsuite.util;
+
+import org.junit.rules.TestRule;
+import org.junit.runners.model.Statement;
+import org.keycloak.common.util.Time;
+
+import static org.junit.Assert.fail;
+
+/**
+ * Created by st on 22/03/17.
+ */
+public class TokenUtil implements TestRule {
+
+    private final String username;
+    private final String password;
+    private OAuthClient oauth;
+
+    private String refreshToken;
+    private String token;
+    private int expires;
+
+    public TokenUtil() {
+        this("test-user@localhost", "password");
+    }
+
+    public TokenUtil(String username, String password) {
+        this.username = username;
+        this.password = password;
+        this.oauth = new OAuthClient();
+        this.oauth.init(null, null);
+        this.oauth.clientId("direct-grant");
+    }
+
+    @Override
+    public Statement apply(final Statement base, org.junit.runner.Description description) {
+        return new Statement() {
+            @Override
+            public void evaluate() throws Throwable {
+                base.evaluate();
+            }
+        };
+    }
+
+    public String getToken() {
+        if (refreshToken == null) {
+            load();
+        } else if (expires < Time.currentTime()) {
+            refresh();
+        }
+        return token;
+    }
+
+    private void load() {
+        try {
+            OAuthClient.AccessTokenResponse accessTokenResponse = oauth.doGrantAccessTokenRequest("password", username, password);
+            if (accessTokenResponse.getStatusCode() != 200) {
+                fail("Failed to get token: " + accessTokenResponse.getErrorDescription());
+            }
+
+            this.refreshToken = accessTokenResponse.getRefreshToken();
+            this.token = accessTokenResponse.getAccessToken();
+
+            expires = Time.currentTime() + accessTokenResponse.getExpiresIn() - 20;
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private void refresh() {
+        try {
+            OAuthClient.AccessTokenResponse accessTokenResponse = oauth.doRefreshTokenRequest(refreshToken, "password");
+            if (accessTokenResponse.getStatusCode() != 200) {
+                fail("Failed to get token: " + accessTokenResponse.getErrorDescription());
+            }
+
+            this.refreshToken = accessTokenResponse.getRefreshToken();
+            this.token = accessTokenResponse.getAccessToken();
+
+            expires = Time.currentTime() + accessTokenResponse.getExpiresIn() - 20;
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/WebDriverLogDumper.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/WebDriverLogDumper.java
new file mode 100644
index 0000000..f56a119
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/WebDriverLogDumper.java
@@ -0,0 +1,26 @@
+package org.keycloak.testsuite.util;
+
+import org.jboss.logging.Logger;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.logging.LogEntries;
+import org.openqa.selenium.logging.LogEntry;
+
+/**
+ * Created by st on 21/03/17.
+ */
+public class WebDriverLogDumper {
+
+    public static String dumpBrowserLogs(WebDriver driver) {
+        try {
+            StringBuilder sb = new StringBuilder();
+            LogEntries logEntries = driver.manage().logs().get("browser");
+            for (LogEntry e : logEntries.getAll()) {
+                sb.append("\n\t" + e.getMessage());
+            }
+            return sb.toString();
+        } catch (UnsupportedOperationException e) {
+            return "Browser doesn't support fetching logs";
+        }
+    }
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceCorsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceCorsTest.java
new file mode 100755
index 0000000..3526386
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceCorsTest.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.testsuite.account;
+
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
+import org.keycloak.testsuite.AssertEvents;
+import org.keycloak.testsuite.util.OAuthClient;
+import org.keycloak.testsuite.util.TokenUtil;
+import org.keycloak.testsuite.util.WebDriverLogDumper;
+import org.keycloak.util.JsonSerialization;
+import org.openqa.selenium.JavascriptExecutor;
+
+import java.io.IOException;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class AccountRestServiceCorsTest extends AbstractTestRealmKeycloakTest {
+
+    private static final String VALID_CORS_URL = "http://localtest.me:8180/auth";
+    private static final String INVALID_CORS_URL = "http://invalid.localtest.me:8180/auth";
+
+    @Rule
+    public TokenUtil tokenUtil = new TokenUtil();
+
+    private CloseableHttpClient client;
+    private JavascriptExecutor executor;
+
+    @Before
+    public void before() {
+        client = HttpClientBuilder.create().build();
+        oauth.clientId("direct-grant");
+        executor = (JavascriptExecutor) driver;
+    }
+
+    @After
+    public void after() {
+        try {
+            client.close();
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Override
+    public void configureTestRealm(RealmRepresentation testRealm) {
+    }
+
+    @Rule
+    public AssertEvents events = new AssertEvents(this);
+
+    @Test
+    public void testGetProfile() throws IOException, InterruptedException {
+        driver.navigate().to(VALID_CORS_URL);
+
+        doJsGet(executor, getAccountUrl(), tokenUtil.getToken(), true);
+    }
+
+    @Test
+    public void testGetProfileInvalidOrigin() throws IOException, InterruptedException {
+        driver.navigate().to(INVALID_CORS_URL);
+
+        doJsGet(executor, getAccountUrl(), tokenUtil.getToken(), false);
+    }
+
+    @Test
+    public void testUpdateProfile() throws IOException {
+        driver.navigate().to(VALID_CORS_URL);
+
+        doJsPost(executor, getAccountUrl(), tokenUtil.getToken(), "{ \"firstName\" : \"Bob\" }", true);
+    }
+
+    @Test
+    public void testUpdateProfileInvalidOrigin() throws IOException {
+        driver.navigate().to(INVALID_CORS_URL);
+
+        doJsPost(executor, getAccountUrl(), tokenUtil.getToken(), "{ \"firstName\" : \"Bob\" }", false);
+    }
+
+    private String getAccountUrl() {
+        return suiteContext.getAuthServerInfo().getContextRoot().toString() + "/auth/realms/test/account";
+    }
+
+    private Result doJsGet(JavascriptExecutor executor, String url, String token, boolean expectAllowed) {
+        String js = "var r = new XMLHttpRequest();" +
+                "var r = new XMLHttpRequest();" +
+                "r.open('GET', '" + url + "', false);" +
+                "r.setRequestHeader('Accept','application/json');" +
+                "r.setRequestHeader('Authorization','bearer " + token + "');" +
+                "r.send();" +
+                "return r.status + ':::' + r.responseText";
+        return doXhr(executor, js, expectAllowed);
+    }
+
+    private Result doJsPost(JavascriptExecutor executor, String url, String token, String data, boolean expectAllowed) {
+        String js = "var r = new XMLHttpRequest();" +
+                "var r = new XMLHttpRequest();" +
+                "r.open('POST', '" + url + "', false);" +
+                "r.setRequestHeader('Accept','application/json');" +
+                "r.setRequestHeader('Content-Type','application/json');" +
+                "r.setRequestHeader('Authorization','bearer " + token + "');" +
+                "r.send('" + data + "');" +
+                "return r.status + ':::' + r.responseText";
+        return doXhr(executor, js, expectAllowed);
+    }
+
+    private Result doXhr(JavascriptExecutor executor, String js, boolean expectAllowed) {
+        Result result = null;
+        Throwable error = null;
+        try {
+            String response = (String) executor.executeScript(js);
+            String r[] = response.split(":::");
+            result = new Result(Integer.parseInt(r[0]), r.length == 2 ? r[1] : null);
+        } catch (Throwable t ) {
+            error = t;
+        }
+
+        if (result == null || result.getStatus() != 200 || error != null) {
+            if (expectAllowed) {
+                throw new AssertionError("Cors request failed: " + WebDriverLogDumper.dumpBrowserLogs(driver));
+            } else {
+                return result;
+            }
+        } else {
+            if (!expectAllowed) {
+                throw new AssertionError("Expected CORS request to be rejected, but was successful");
+            } else {
+                return result;
+            }
+        }
+    }
+
+    private static class Result {
+        int status;
+
+        String result;
+
+        public Result(int status, String result) {
+            this.status = status;
+            this.result = result;
+        }
+
+        public int getStatus() {
+            return status;
+        }
+
+        public String getResult() {
+            return result;
+        }
+    }
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceTest.java
new file mode 100755
index 0000000..e4f4ec1
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceTest.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.testsuite.account;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.keycloak.broker.provider.util.SimpleHttp;
+import org.keycloak.representations.account.SessionRepresentation;
+import org.keycloak.representations.account.UserRepresentation;
+import org.keycloak.representations.idm.ErrorRepresentation;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
+import org.keycloak.testsuite.AssertEvents;
+import org.keycloak.testsuite.util.TokenUtil;
+import org.keycloak.testsuite.util.UserBuilder;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class AccountRestServiceTest extends AbstractTestRealmKeycloakTest {
+
+    @Rule
+    public TokenUtil tokenUtil = new TokenUtil();
+
+    @Rule
+    public AssertEvents events = new AssertEvents(this);
+
+    private CloseableHttpClient client;
+
+    @Before
+    public void before() {
+        client = HttpClientBuilder.create().build();
+    }
+
+    @After
+    public void after() {
+        try {
+            client.close();
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Override
+    public void configureTestRealm(RealmRepresentation testRealm) {
+        testRealm.getUsers().add(UserBuilder.create().username("no-account-access").password("password").build());
+        testRealm.getUsers().add(UserBuilder.create().username("view-account-access").role("account", "view-profile").password("password").build());
+    }
+
+    @Test
+    public void testGetProfile() throws IOException {
+        UserRepresentation user = SimpleHttp.doGet(getAccountUrl(null), client).auth(tokenUtil.getToken()).asJson(UserRepresentation.class);
+        assertEquals("Tom", user.getFirstName());
+        assertEquals("Brady", user.getLastName());
+        assertEquals("test-user@localhost", user.getEmail());
+        assertFalse(user.isEmailVerified());
+        assertTrue(user.getAttributes().isEmpty());
+    }
+
+    @Test
+    public void testUpdateProfile() throws IOException {
+        UserRepresentation user = SimpleHttp.doGet(getAccountUrl(null), client).auth(tokenUtil.getToken()).asJson(UserRepresentation.class);
+        user.setFirstName("Homer");
+        user.setLastName("Simpsons");
+        user.getAttributes().put("attr1", Collections.singletonList("val1"));
+        user.getAttributes().put("attr2", Collections.singletonList("val2"));
+
+        user = updateAndGet(user);
+
+        assertEquals("Homer", user.getFirstName());
+        assertEquals("Simpsons", user.getLastName());
+        assertEquals(2, user.getAttributes().size());
+        assertEquals(1, user.getAttributes().get("attr1").size());
+        assertEquals("val1", user.getAttributes().get("attr1").get(0));
+        assertEquals(1, user.getAttributes().get("attr2").size());
+        assertEquals("val2", user.getAttributes().get("attr2").get(0));
+
+        // Update attributes
+        user.getAttributes().remove("attr1");
+        user.getAttributes().get("attr2").add("val3");
+
+        user = updateAndGet(user);
+
+        assertEquals(1, user.getAttributes().size());
+        assertEquals(2, user.getAttributes().get("attr2").size());
+        assertEquals("val2", user.getAttributes().get("attr2").get(0));
+        assertEquals("val3", user.getAttributes().get("attr2").get(1));
+
+        // Update email
+        user.setEmail("bobby@localhost");
+        user = updateAndGet(user);
+        assertEquals("bobby@localhost", user.getEmail());
+
+        user.setEmail("john-doh@localhost");
+        updateError(user, 409, "email_exists");
+
+        user.setEmail("test-user@localhost");
+        user = updateAndGet(user);
+        assertEquals("test-user@localhost", user.getEmail());
+
+        // Update username
+        user.setUsername("updatedUsername");
+        user = updateAndGet(user);
+        assertEquals("updatedusername", user.getUsername());
+
+        user.setUsername("john-doh@localhost");
+        updateError(user, 409, "username_exists");
+
+        user.setUsername("test-user@localhost");
+        user = updateAndGet(user);
+        assertEquals("test-user@localhost", user.getUsername());
+
+        RealmRepresentation realmRep = adminClient.realm("test").toRepresentation();
+        realmRep.setEditUsernameAllowed(false);
+        adminClient.realm("test").update(realmRep);
+
+        user.setUsername("updatedUsername2");
+        updateError(user, 400, "username_read_only");
+    }
+
+    private UserRepresentation updateAndGet(UserRepresentation user) throws IOException {
+        int status = SimpleHttp.doPost(getAccountUrl(null), client).auth(tokenUtil.getToken()).json(user).asStatus();
+        assertEquals(200, status);
+        return SimpleHttp.doGet(getAccountUrl(null), client).auth(tokenUtil.getToken()).asJson(UserRepresentation.class);
+    }
+
+
+    private void updateError(UserRepresentation user, int expectedStatus, String expectedMessage) throws IOException {
+        SimpleHttp.Response response = SimpleHttp.doPost(getAccountUrl(null), client).auth(tokenUtil.getToken()).json(user).asResponse();
+        assertEquals(expectedStatus, response.getStatus());
+        assertEquals(expectedMessage, response.asJson(ErrorRepresentation.class).getErrorMessage());
+    }
+
+    @Test
+    public void testProfilePermissions() throws IOException {
+        TokenUtil noaccessToken = new TokenUtil("no-account-access", "password");
+        TokenUtil viewToken = new TokenUtil("view-account-access", "password");
+
+        // Read with no access
+        assertEquals(403, SimpleHttp.doGet(getAccountUrl(null), client).header("Accept", "application/json").auth(noaccessToken.getToken()).asStatus());
+
+        // Update with no access
+        assertEquals(403, SimpleHttp.doPost(getAccountUrl(null), client).auth(noaccessToken.getToken()).json(new UserRepresentation()).asStatus());
+
+        // Update with read only
+        assertEquals(403, SimpleHttp.doPost(getAccountUrl(null), client).auth(viewToken.getToken()).json(new UserRepresentation()).asStatus());
+    }
+
+    @Test
+    public void testUpdateProfilePermissions() throws IOException {
+        TokenUtil noaccessToken = new TokenUtil("no-account-access", "password");
+        int status = SimpleHttp.doGet(getAccountUrl(null), client).header("Accept", "application/json").auth(noaccessToken.getToken()).asStatus();
+        assertEquals(403, status);
+
+        TokenUtil viewToken = new TokenUtil("view-account-access", "password");
+        status = SimpleHttp.doGet(getAccountUrl(null), client).header("Accept", "application/json").auth(viewToken.getToken()).asStatus();
+        assertEquals(200, status);
+    }
+
+    @Test
+    public void testGetSessions() throws IOException {
+        List<SessionRepresentation> sessions = SimpleHttp.doGet(getAccountUrl("sessions"), client).auth(tokenUtil.getToken()).asJson(new TypeReference<List<SessionRepresentation>>() {});
+
+        assertEquals(1, sessions.size());
+    }
+
+    private String getAccountUrl(String resource) {
+        return suiteContext.getAuthServerInfo().getContextRoot().toString() + "/auth/realms/test/account" + (resource != null ? "/" + resource : "");
+    }
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/custom/CustomThemeTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/custom/CustomThemeTest.java
index 81942fe..d9e30b3 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/custom/CustomThemeTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/custom/CustomThemeTest.java
@@ -27,7 +27,7 @@ import org.keycloak.representations.idm.RealmRepresentation;
 import org.keycloak.representations.idm.UserRepresentation;
 import org.keycloak.testsuite.AssertEvents;
 import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
-import org.keycloak.testsuite.account.AccountTest;
+import org.keycloak.testsuite.account.AccountFormServiceTest;
 import org.keycloak.testsuite.pages.AccountUpdateProfilePage;
 import org.keycloak.testsuite.pages.LoginPage;
 import org.keycloak.testsuite.util.RealmBuilder;
@@ -68,7 +68,7 @@ public class CustomThemeTest extends AbstractTestRealmKeycloakTest {
         profilePage.open();
         loginPage.login("test-user@localhost", "password");
 
-        events.expectLogin().client("account").detail(Details.REDIRECT_URI, AccountTest.ACCOUNT_REDIRECT).assertEvent();
+        events.expectLogin().client("account").detail(Details.REDIRECT_URI, AccountFormServiceTest.ACCOUNT_REDIRECT).assertEvent();
 
         Assert.assertEquals("test-user@localhost", profilePage.getEmail());
         Assert.assertEquals("", profilePage.getAttribute("street"));
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTotpTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTotpTest.java
index 1e349d9..f2448e1 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTotpTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTotpTest.java
@@ -28,7 +28,7 @@ import org.keycloak.models.utils.TimeBasedOTP;
 import org.keycloak.representations.idm.AdminEventRepresentation;
 import org.keycloak.representations.idm.RealmRepresentation;
 import org.keycloak.representations.idm.UserRepresentation;
-import org.keycloak.services.resources.AccountService;
+import org.keycloak.services.resources.account.AccountFormService;
 import org.keycloak.testsuite.AssertEvents;
 import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
 import org.keycloak.testsuite.pages.AccountTotpPage;
@@ -45,7 +45,7 @@ import java.util.List;
 public class UserTotpTest extends AbstractTestRealmKeycloakTest {
 
     private static final UriBuilder BASE = UriBuilder.fromUri("http://localhost:8180/auth");
-    public static String ACCOUNT_REDIRECT = AccountService.loginRedirectUrl(BASE.clone()).build("test").toString();
+    public static String ACCOUNT_REDIRECT = AccountFormService.loginRedirectUrl(BASE.clone()).build("test").toString();
 
     @Rule
     public AssertEvents events = new AssertEvents(this);
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthGrantTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthGrantTest.java
index 926ff69..d0e0517 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthGrantTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthGrantTest.java
@@ -38,7 +38,6 @@ import org.keycloak.representations.idm.RealmRepresentation;
 import org.keycloak.representations.idm.RoleRepresentation;
 import org.keycloak.testsuite.AbstractKeycloakTest;
 import org.keycloak.testsuite.AssertEvents;
-import org.keycloak.testsuite.account.AccountTest;
 import org.keycloak.testsuite.admin.ApiUtil;
 import org.keycloak.testsuite.pages.AccountApplicationsPage;
 import org.keycloak.testsuite.pages.AppPage;
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java
index c0ca7f1..6254b51 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java
@@ -43,7 +43,7 @@ import org.keycloak.representations.idm.RoleRepresentation;
 import org.keycloak.representations.idm.UserRepresentation;
 import org.keycloak.testsuite.AbstractKeycloakTest;
 import org.keycloak.testsuite.AssertEvents;
-import org.keycloak.testsuite.account.AccountTest;
+import org.keycloak.testsuite.account.AccountFormServiceTest;
 import org.keycloak.testsuite.admin.ApiUtil;
 import org.keycloak.testsuite.arquillian.AuthServerTestEnricher;
 import org.keycloak.testsuite.auth.page.AuthRealm;
@@ -530,7 +530,7 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
         // Go to account mgmt applications page
         applicationsPage.open();
         loginPage.login("test-user@localhost", "password");
-        events.expectLogin().client("account").detail(Details.REDIRECT_URI, AccountTest.ACCOUNT_REDIRECT + "?path=applications").assertEvent();
+        events.expectLogin().client("account").detail(Details.REDIRECT_URI, AccountFormServiceTest.ACCOUNT_REDIRECT + "?path=applications").assertEvent();
         Assert.assertTrue(applicationsPage.isCurrent());
         Map<String, AccountApplicationsPage.AppEntry> apps = applicationsPage.getApplications();
         Assert.assertTrue(apps.containsKey("offline-client-2"));
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/testrealm.json b/testsuite/integration-arquillian/tests/base/src/test/resources/testrealm.json
index 2ce6b39..99cd578 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/resources/testrealm.json
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/testrealm.json
@@ -358,6 +358,13 @@
       ],
       "adminUrl": "http://localhost:8180/varnamedapp/base/admin",
       "secret": "password"
+    },
+    {
+      "clientId": "direct-grant",
+      "enabled": true,
+      "directAccessGrantsEnabled": true,
+      "secret": "password",
+      "webOrigins": [ "http://localtest.me:8180" ]
     }
   ],
   "roles" : {
diff --git a/themes/src/main/resources/theme/base/account/messages/messages_en.properties b/themes/src/main/resources/theme/base/account/messages/messages_en.properties
index 906c525..c5ce32a 100755
--- a/themes/src/main/resources/theme/base/account/messages/messages_en.properties
+++ b/themes/src/main/resources/theme/base/account/messages/messages_en.properties
@@ -119,6 +119,7 @@ usernameExistsMessage=Username already exists.
 emailExistsMessage=Email already exists.
 
 readOnlyUserMessage=You can''t update your account as it is read only.
+readOnlyUsernameMessage=You can''t update your username as it is read only.
 readOnlyPasswordMessage=You can''t update your password as your account is read only.
 
 successTotpMessage=Mobile authenticator configured.