keycloak-aplcache

Allow oauth clients to ask for permission to view user profile,

11/3/2013 2:14:15 PM

Details

diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/login-oauth-grant.ftl b/forms/src/main/resources/META-INF/resources/forms/theme/default/login-oauth-grant.ftl
index edc7ef7..045ba9b 100755
--- a/forms/src/main/resources/META-INF/resources/forms/theme/default/login-oauth-grant.ftl
+++ b/forms/src/main/resources/META-INF/resources/forms/theme/default/login-oauth-grant.ftl
@@ -25,7 +25,7 @@
             <ul>
                 <#list oauth.resourceRolesRequested[resourceRole] as role>
                     <li>
-                        <span>${role.description}</span>
+                        <span><#if role.description??>${role.description}<#else>${role.name}</#if></span>
                     </li>
                 </#list>
             </ul>
diff --git a/model/api/src/main/java/org/keycloak/models/Constants.java b/model/api/src/main/java/org/keycloak/models/Constants.java
index b0af34c..57002e5 100755
--- a/model/api/src/main/java/org/keycloak/models/Constants.java
+++ b/model/api/src/main/java/org/keycloak/models/Constants.java
@@ -12,5 +12,7 @@ public interface Constants {
     String IDENTITY_REQUESTER_ROLE = "KEYCLOAK_IDENTITY_REQUESTER";
     String WILDCARD_ROLE = "*";
 
-    String ACCOUNT_MANAGEMENT_APPLICATION = "Account Management";
+    String ACCOUNT_APPLICATION = "Account";
+    String ACCOUNT_PROFILE_ROLE = "KEYCLOAK_ACCOUNT_PROFILE";
+    String ACCOUNT_MANAGE_ROLE = "KEYCLOAK_ACCOUNT_MANAGE";
 }
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 5fe4475..bc50831 100755
--- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
@@ -173,7 +173,7 @@ public class AuthenticationManager {
                 return null;
             }
 
-            Auth auth = new Auth();
+            Auth auth = new Auth(token);
 
             UserModel user = realm.getUser(token.getPrincipal());
             if (user == null || !user.isEnabled()) {
@@ -220,7 +220,7 @@ public class AuthenticationManager {
                 throw new NotAuthorizedException("token_expired");
             }
 
-            Auth auth = new Auth();
+            Auth auth = new Auth(token);
 
             UserModel user = realm.getUser(token.getPrincipal());
             if (user == null || !user.isEnabled()) {
@@ -309,9 +309,18 @@ public class AuthenticationManager {
     }
 
     public static class Auth {
+        private SkeletonKeyToken token;
         private UserModel user;
         private UserModel client;
 
+        public Auth(SkeletonKeyToken token) {
+            this.token = token;
+        }
+
+        public SkeletonKeyToken getToken() {
+            return token;
+        }
+
         public UserModel getUser() {
             return user;
         }
diff --git a/services/src/main/java/org/keycloak/services/managers/RealmManager.java b/services/src/main/java/org/keycloak/services/managers/RealmManager.java
index 14220b2..52f88f1 100755
--- a/services/src/main/java/org/keycloak/services/managers/RealmManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/RealmManager.java
@@ -107,9 +107,11 @@ public class RealmManager {
     }
 
     private void enableAccountManagement(RealmModel realm) {
-        ApplicationModel application = realm.getApplicationById(Constants.ACCOUNT_MANAGEMENT_APPLICATION);
+        ApplicationModel application = realm.getApplicationById(Constants.ACCOUNT_APPLICATION);
         if (application == null) {
-            application = realm.addApplication(Constants.ACCOUNT_MANAGEMENT_APPLICATION);
+            application = realm.addApplication(Constants.ACCOUNT_APPLICATION);
+            application.addRole(Constants.ACCOUNT_PROFILE_ROLE);
+            application.addRole(Constants.ACCOUNT_MANAGE_ROLE);
 
             UserCredentialModel password = new UserCredentialModel();
             password.setType(UserCredentialModel.PASSWORD);
@@ -124,7 +126,7 @@ public class RealmManager {
     }
 
     private void disableAccountManagement(RealmModel realm) {
-        ApplicationModel application = realm.getApplicationNameMap().get(Constants.ACCOUNT_MANAGEMENT_APPLICATION);
+        ApplicationModel application = realm.getApplicationNameMap().get(Constants.ACCOUNT_APPLICATION);
         if (application != null) {
             application.setEnabled(false); // TODO Should we delete the application instead?
         }
@@ -424,7 +426,7 @@ public class RealmManager {
         rep.setSmtpServer(realm.getSmtpConfig());
         rep.setSocialProviders(realm.getSocialConfig());
 
-        ApplicationModel accountManagementApplication = realm.getApplicationNameMap().get(Constants.ACCOUNT_MANAGEMENT_APPLICATION);
+        ApplicationModel accountManagementApplication = realm.getApplicationNameMap().get(Constants.ACCOUNT_APPLICATION);
         rep.setAccountManagement(accountManagementApplication != null && accountManagementApplication.isEnabled());
 
         List<RoleModel> defaultRoles = realm.getDefaultRoles();
diff --git a/services/src/main/java/org/keycloak/services/managers/TokenManager.java b/services/src/main/java/org/keycloak/services/managers/TokenManager.java
index 3c1cfa0..4b6f79e 100755
--- a/services/src/main/java/org/keycloak/services/managers/TokenManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/TokenManager.java
@@ -11,10 +11,7 @@ import org.keycloak.representations.SkeletonKeyToken;
 import javax.ws.rs.core.MultivaluedMap;
 import java.io.IOException;
 import java.io.UnsupportedEncodingException;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
+import java.util.*;
 import java.util.concurrent.ConcurrentHashMap;
 
 /**
@@ -54,6 +51,9 @@ public class TokenManager {
             if (scope.size() > 0) {
                 Set<String> scopeRequest = null;
                 if (scopeMap != null) {
+                    if (scopeRequest == null) {
+                        scopeRequest = new HashSet<String>();
+                    }
                     scopeRequest.addAll(scopeMap.get("realm"));
                     if (scopeRequest.contains(Constants.WILDCARD_ROLE)) scopeRequest = null;
                 }
@@ -73,6 +73,9 @@ public class TokenManager {
                 if (scope.size() > 0) {
                     Set<String> scopeRequest = null;
                     if (scopeMap != null) {
+                        if (scopeRequest == null) {
+                            scopeRequest = new HashSet<String>();
+                        }
                         scopeRequest.addAll(scopeMap.get(resource.getName()));
                         if (scopeRequest.contains(Constants.WILDCARD_ROLE)) scopeRequest = null;
                     }
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 ac4fabf..2abcf99 100755
--- a/services/src/main/java/org/keycloak/services/resources/AccountService.java
+++ b/services/src/main/java/org/keycloak/services/resources/AccountService.java
@@ -22,16 +22,12 @@
 package org.keycloak.services.resources;
 
 import java.net.URI;
-import java.util.HashSet;
 import java.util.List;
-import java.util.Set;
 
 import javax.ws.rs.*;
 import javax.ws.rs.core.*;
-import javax.ws.rs.core.Response.Status;
 import javax.ws.rs.ext.Providers;
 
-import org.jboss.resteasy.annotations.cache.NoCache;
 import org.jboss.resteasy.jose.jws.JWSInput;
 import org.jboss.resteasy.jose.jws.crypto.RSAProvider;
 import org.jboss.resteasy.logging.Logger;
@@ -39,15 +35,13 @@ import org.jboss.resteasy.spi.HttpRequest;
 import org.keycloak.AbstractOAuthClient;
 import org.keycloak.jaxrs.JaxrsOAuthClient;
 import org.keycloak.models.*;
+import org.keycloak.representations.SkeletonKeyToken;
 import org.keycloak.representations.idm.CredentialRepresentation;
-import org.keycloak.representations.idm.UserRepresentation;
-import org.keycloak.services.email.EmailSender;
 import org.keycloak.services.managers.AccessCodeEntry;
 import org.keycloak.services.managers.AuthenticationManager;
 import org.keycloak.services.managers.RealmManager;
 import org.keycloak.services.managers.TokenManager;
 import org.keycloak.services.messages.Messages;
-import org.keycloak.models.UserModel.RequiredAction;
 import org.keycloak.services.resources.flows.Flows;
 import org.keycloak.services.resources.flows.FormFlows;
 import org.keycloak.services.resources.flows.Pages;
@@ -99,13 +93,13 @@ public class AccountService {
         }
     }
 
-    @Path("")
+    @Path("/")
     @OPTIONS
     public Response accountPreflight() {
         return Cors.add(request, Response.ok()).auth().preflight().build();
     }
 
-    @Path("")
+    @Path("/")
     @GET
     public Response accountPage() {
         List<MediaType> types = headers.getAcceptableMediaTypes();
@@ -113,6 +107,10 @@ public class AccountService {
             return forwardToPage(null, Pages.ACCOUNT);
         } else if (types.contains(MediaType.APPLICATION_JSON_TYPE)) {
             AuthenticationManager.Auth auth = getAuth(true);
+            if (!hasAccess(auth, Constants.ACCOUNT_PROFILE_ROLE)) {
+                throw new ForbiddenException();
+            }
+
             return Cors.add(request, Response.ok(RealmManager.toRepresentation(auth.getUser()))).auth().allowedOrigins(auth.getClient()).build();
         } else {
             return Response.notAcceptable(Variant.VariantListBuilder.newInstance().mediaTypes(MediaType.TEXT_HTML_TYPE, MediaType.APPLICATION_JSON_TYPE).build()).build();
@@ -143,7 +141,7 @@ public class AccountService {
         return forwardToPage("access", Pages.ACCESS);
     }
 
-    @Path("")
+    @Path("/")
     @POST
     @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
     public Response processAccountUpdate(final MultivaluedMap<String, String> formData) {
@@ -330,7 +328,7 @@ public class AccountService {
         String authUrl = Urls.realmLoginPage(uriInfo.getBaseUri(), realm.getId()).toString();
         oauth.setAuthUrl(authUrl);
 
-        oauth.setClientId(Constants.ACCOUNT_MANAGEMENT_APPLICATION);
+        oauth.setClientId(Constants.ACCOUNT_APPLICATION);
 
         URI accountUri = Urls.accountPageBuilder(uriInfo.getBaseUri()).path(AccountService.class, "loginRedirect").build(realm.getId());
 
@@ -346,4 +344,27 @@ public class AccountService {
         return auth;
     }
 
+    private boolean hasAccess(AuthenticationManager.Auth auth, String requiredRole) {
+        UserModel client = auth.getClient();
+
+        if (realm.hasRole(client, Constants.APPLICATION_ROLE)) {
+            return true;
+        }
+
+        SkeletonKeyToken token = auth.getToken();
+        SkeletonKeyToken.Access access = token.getResourceAccess(application.getName());
+
+        if (access != null) {
+            if (access.isUserInRole(Constants.ACCOUNT_MANAGE_ROLE)) {
+                return true;
+            }
+
+            if (access.isUserInRole(Constants.ACCOUNT_PROFILE_ROLE)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
 }
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 54fd5a5..0e1bc0f 100755
--- a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java
@@ -69,7 +69,7 @@ public class RealmsResource {
             throw new NotFoundException();
         }
 
-        ApplicationModel application = realm.getApplicationNameMap().get(Constants.ACCOUNT_MANAGEMENT_APPLICATION);
+        ApplicationModel application = realm.getApplicationNameMap().get(Constants.ACCOUNT_APPLICATION);
         if (application == null || !application.isEnabled()) {
             logger.debug("account management not enabled");
             throw new NotFoundException();
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/ProfileTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/ProfileTest.java
index cdcef48..cc5144d 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/ProfileTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/ProfileTest.java
@@ -18,6 +18,7 @@ import org.keycloak.testsuite.Constants;
 import org.keycloak.testsuite.OAuthClient;
 import org.keycloak.testsuite.pages.AccountUpdateProfilePage;
 import org.keycloak.testsuite.pages.LoginPage;
+import org.keycloak.testsuite.pages.OAuthGrantPage;
 import org.keycloak.testsuite.rule.KeycloakRule;
 import org.keycloak.testsuite.rule.WebResource;
 import org.keycloak.testsuite.rule.WebRule;
@@ -30,6 +31,7 @@ import java.io.IOException;
 import java.net.URI;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 /**
@@ -47,6 +49,14 @@ public class ProfileTest {
             user.setAttribute("key1", "value1");
             user.setAttribute("key2", "value2");
 
+            ApplicationModel accountApp = appRealm.getApplicationNameMap().get(org.keycloak.models.Constants.ACCOUNT_APPLICATION);
+
+            accountApp.grantRole(user, accountApp.getRole(org.keycloak.models.Constants.ACCOUNT_PROFILE_ROLE));
+            accountApp.grantRole(user, accountApp.getRole(org.keycloak.models.Constants.ACCOUNT_MANAGE_ROLE));
+
+            UserModel thirdParty = appRealm.getUser("third-party");
+            accountApp.addScopeMapping(thirdParty, org.keycloak.models.Constants.ACCOUNT_PROFILE_ROLE);
+
             for (ApplicationModel app : appRealm.getApplications()) {
                 if (app.getName().equals("test-app")) {
                     app.getApplicationUser().addWebOrigin("http://localtest.me:8081");
@@ -70,6 +80,9 @@ public class ProfileTest {
     @WebResource
     protected LoginPage loginPage;
 
+    @WebResource
+    protected OAuthGrantPage grantPage;
+
     @Test
     public void getProfile() throws Exception {
         oauth.doLogin("test-user@localhost", "password");
@@ -139,6 +152,35 @@ public class ProfileTest {
         assertEquals(403, response.getStatusLine().getStatusCode());
     }
 
+    @Test
+    public void getProfileOAuthClient() throws Exception {
+        oauth.addScope(org.keycloak.models.Constants.ACCOUNT_APPLICATION, org.keycloak.models.Constants.ACCOUNT_PROFILE_ROLE);
+        oauth.clientId("third-party");
+        oauth.doLoginGrant("test-user@localhost", "password");
+
+        grantPage.accept();
+
+        String token = oauth.doAccessTokenRequest(oauth.getCurrentQuery().get("code"), "password").getAccessToken();
+        HttpResponse response = doGetProfile(token, null);
+
+        assertEquals(200, response.getStatusLine().getStatusCode());
+        JSONObject profile = new JSONObject(IOUtils.toString(response.getEntity().getContent()));
+
+        assertEquals("test-user@localhost", profile.getString("username"));
+    }
+
+    @Test
+    public void getProfileOAuthClientNoScope() throws Exception {
+        oauth.addScope(org.keycloak.models.Constants.ACCOUNT_APPLICATION);
+        oauth.clientId("third-party");
+        oauth.doLoginGrant("test-user@localhost", "password");
+
+        String token = oauth.doAccessTokenRequest(oauth.getCurrentQuery().get("code"), "password").getAccessToken();
+        HttpResponse response = doGetProfile(token, null);
+
+        assertEquals(403, response.getStatusLine().getStatusCode());
+    }
+
     private URI getAccountURI() {
         return UriBuilder.fromUri(Constants.AUTH_SERVER_ROOT + "/rest/realms/" + oauth.getRealm() + "/account").build();
     }
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionMultipleActionsTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionMultipleActionsTest.java
index 51ba944..d591829 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionMultipleActionsTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionMultipleActionsTest.java
@@ -76,7 +76,7 @@ public class RequiredActionMultipleActionsTest {
     protected LoginUpdateProfilePage updateProfilePage;
 
     @Test
-    public void updateProfileAndPassword() {
+    public void updateProfileAndPassword() throws Exception {
         loginPage.open();
         loginPage.login("test-user@localhost", "password");
 
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionResetPasswordTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionResetPasswordTest.java
index f4cb070..76757f4 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionResetPasswordTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionResetPasswordTest.java
@@ -81,7 +81,7 @@ public class RequiredActionResetPasswordTest {
     protected LoginPasswordUpdatePage changePasswordPage;
 
     @Test
-    public void tempPassword() {
+    public void tempPassword() throws Exception {
         loginPage.open();
         loginPage.login("test-user@localhost", "password");
 
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTotpTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTotpTest.java
index 54451f4..2ff938b 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTotpTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTotpTest.java
@@ -93,7 +93,7 @@ public class LoginTotpTest {
     }
 
     @Test
-    public void loginWithTotpFailure() {
+    public void loginWithTotpFailure() throws Exception {
         loginPage.open();
         loginPage.login("test-user@localhost", "password");
 
@@ -106,7 +106,7 @@ public class LoginTotpTest {
     }
 
     @Test
-    public void loginWithTotpSuccess() {
+    public void loginWithTotpSuccess() throws Exception {
         loginPage.open();
         loginPage.login("test-user@localhost", "password");
 
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OAuthGrantTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OAuthGrantTest.java
index 0513f67..e5d7ef6 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OAuthGrantTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OAuthGrantTest.java
@@ -22,14 +22,15 @@
 package org.keycloak.testsuite.oauth;
 
 import java.io.IOException;
+import java.util.Map;
 
 import org.junit.Assert;
 import org.junit.BeforeClass;
 import org.junit.ClassRule;
 import org.junit.Rule;
 import org.junit.Test;
+import org.keycloak.representations.SkeletonKeyToken;
 import org.keycloak.testsuite.OAuthClient;
-import org.keycloak.testsuite.OAuthGrantServlet;
 import org.keycloak.testsuite.pages.LoginPage;
 import org.keycloak.testsuite.pages.OAuthGrantPage;
 import org.keycloak.testsuite.rule.KeycloakRule;
@@ -45,11 +46,6 @@ public class OAuthGrantTest {
     @ClassRule
     public static KeycloakRule keycloakRule = new KeycloakRule();
 
-    @BeforeClass
-    public static void before() {
-        keycloakRule.deployServlet("grant", "/grant", OAuthGrantServlet.class);
-    }
-
     @Rule
     public WebRule webRule = new WebRule(this);
 
@@ -65,28 +61,13 @@ public class OAuthGrantTest {
     @WebResource
     protected OAuthGrantPage grantPage;
 
-    private static String GRANT_APP_URL = "http://localhost:8081/grant/";
-
-    private static String ACCESS_GRANTED = "Access rights granted.";
-    private static String ACCESS_NOT_GRANTED = "Access rights not granted.";
-
     private static String ROLE_USER = "Have User privileges";
     private static String ROLE_CUSTOMER = "Have Customer User privileges";
 
-    private static String GRANT_ROLE = "user";
-    private static String GRANT_APP = "test-app";
-    private static String GRANT_APP_ROLE = "customer-user";
-
     @Test
     public void oauthGrantAcceptTest() throws IOException {
-
-        driver.navigate().to(GRANT_APP_URL);
-
-        Assert.assertFalse(driver.getPageSource().contains(ACCESS_GRANTED));
-        Assert.assertFalse(driver.getPageSource().contains(ACCESS_NOT_GRANTED));
-
-        loginPage.isCurrent();
-        loginPage.login("test-user@localhost", "password");
+        oauth.clientId("third-party");
+        oauth.doLoginGrant("test-user@localhost", "password");
 
         grantPage.assertCurrent();
         Assert.assertTrue(driver.getPageSource().contains(ROLE_USER));
@@ -94,23 +75,50 @@ public class OAuthGrantTest {
 
         grantPage.accept();
 
-        Assert.assertTrue(driver.getPageSource().contains(ACCESS_GRANTED));
-        Assert.assertFalse(driver.getPageSource().contains(ACCESS_NOT_GRANTED));
+        Assert.assertTrue(oauth.getCurrentQuery().containsKey("code"));
+        OAuthClient.AccessTokenResponse accessToken = oauth.doAccessTokenRequest(oauth.getCurrentQuery().get("code"), "password");
+
+        SkeletonKeyToken token = oauth.verifyToken(accessToken.getAccessToken());
 
-        Assert.assertTrue(driver.getPageSource().contains("Role:"+ GRANT_ROLE +"."));
-        Assert.assertTrue(driver.getPageSource().contains("App:"+ GRANT_APP +";"+ GRANT_APP_ROLE +"."));
+        SkeletonKeyToken.Access realmAccess = token.getRealmAccess();
+        Assert.assertEquals(1, realmAccess.getRoles().size());
+        Assert.assertTrue(realmAccess.isUserInRole("user"));
+
+        Map<String,SkeletonKeyToken.Access> resourceAccess = token.getResourceAccess();
+        Assert.assertEquals(1, resourceAccess.size());
+        Assert.assertEquals(1, resourceAccess.get("test-app").getRoles().size());
+        Assert.assertTrue(resourceAccess.get("test-app").isUserInRole("customer-user"));
     }
 
     @Test
-    public void oauthGrantCancelTest() throws IOException {
+    public void oauthGrantAcceptTestWithScope() throws IOException {
+        oauth.addScope("test-app", "customer-user");
+        oauth.clientId("third-party");
+        oauth.doLoginGrant("test-user@localhost", "password");
 
-        driver.navigate().to(GRANT_APP_URL);
+        grantPage.assertCurrent();
+        Assert.assertTrue(driver.getPageSource().contains(ROLE_CUSTOMER));
+
+        grantPage.accept();
+
+        Assert.assertTrue(oauth.getCurrentQuery().containsKey("code"));
+        OAuthClient.AccessTokenResponse accessToken = oauth.doAccessTokenRequest(oauth.getCurrentQuery().get("code"), "password");
 
-        Assert.assertFalse(driver.getPageSource().contains(ACCESS_GRANTED));
-        Assert.assertFalse(driver.getPageSource().contains(ACCESS_NOT_GRANTED));
+        SkeletonKeyToken token = oauth.verifyToken(accessToken.getAccessToken());
 
-        loginPage.isCurrent();
-        loginPage.login("test-user@localhost", "password");
+        SkeletonKeyToken.Access realmAccess = token.getRealmAccess();
+        Assert.assertNull(realmAccess);
+
+        Map<String,SkeletonKeyToken.Access> resourceAccess = token.getResourceAccess();
+        Assert.assertEquals(1, resourceAccess.size());
+        Assert.assertEquals(1, resourceAccess.get("test-app").getRoles().size());
+        Assert.assertTrue(resourceAccess.get("test-app").isUserInRole("customer-user"));
+    }
+
+    @Test
+    public void oauthGrantCancelTest() throws IOException {
+        oauth.clientId("third-party");
+        oauth.doLoginGrant("test-user@localhost", "password");
 
         grantPage.assertCurrent();
         Assert.assertTrue(driver.getPageSource().contains(ROLE_USER));
@@ -118,7 +126,7 @@ public class OAuthGrantTest {
 
         grantPage.cancel();
 
-        Assert.assertFalse(driver.getPageSource().contains(ACCESS_GRANTED));
-        Assert.assertTrue(driver.getPageSource().contains(ACCESS_NOT_GRANTED));
+        Assert.assertTrue(oauth.getCurrentQuery().containsKey("error"));
+        Assert.assertEquals("access_denied", oauth.getCurrentQuery().get("error"));
     }
 }
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java
index dcda888..bff07dd 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java
@@ -32,6 +32,7 @@ import java.util.Map;
 
 import javax.ws.rs.core.UriBuilder;
 
+import com.google.api.client.repackaged.org.apache.commons.codec.binary.Base64;
 import org.apache.commons.io.IOUtils;
 import org.apache.http.HttpResponse;
 import org.apache.http.NameValuePair;
@@ -41,11 +42,16 @@ import org.apache.http.client.methods.HttpPost;
 import org.apache.http.client.utils.URLEncodedUtils;
 import org.apache.http.impl.client.DefaultHttpClient;
 import org.apache.http.message.BasicNameValuePair;
+import org.jboss.resteasy.jose.Base64Url;
+import org.jboss.resteasy.jwt.JsonSerialization;
 import org.jboss.resteasy.security.PemUtils;
 import org.json.JSONObject;
 import org.junit.Assert;
 import org.keycloak.RSATokenVerifier;
+import org.keycloak.VerificationException;
+import org.keycloak.representations.SkeletonKeyScope;
 import org.keycloak.representations.SkeletonKeyToken;
+import org.keycloak.testsuite.pages.OAuthGrantPage;
 import org.openqa.selenium.By;
 import org.openqa.selenium.WebDriver;
 
@@ -68,17 +74,21 @@ public class OAuthClient {
 
     private String redirectUri = "http://localhost:8081/app/auth";
 
-    private String scope;
+    private SkeletonKeyScope scope;
 
     private String state;
 
     private PublicKey realmPublicKey;
 
-    public OAuthClient(WebDriver driver) throws Exception {
+    public OAuthClient(WebDriver driver) {
         this.driver = driver;
 
-        JSONObject realmJson = new JSONObject(IOUtils.toString(getClass().getResourceAsStream("/testrealm.json")));
-        realmPublicKey = PemUtils.decodePublicKey(realmJson.getString("publicKey"));
+        try {
+            JSONObject realmJson = new JSONObject(IOUtils.toString(getClass().getResourceAsStream("/testrealm.json")));
+            realmPublicKey = PemUtils.decodePublicKey(realmJson.getString("publicKey"));
+        } catch (Exception e) {
+            throw new AssertionError("Failed to retrieve realm public key", e);
+        }
     }
 
     public AuthorizationCodeResponse doLogin(String username, String password) {
@@ -91,7 +101,15 @@ public class OAuthClient {
         return new AuthorizationCodeResponse(this);
     }
 
-    public AccessTokenResponse doAccessTokenRequest(String code, String password) throws Exception {
+    public void doLoginGrant(String username, String password) {
+        openLoginForm();
+
+        driver.findElement(By.id("username")).sendKeys(username);
+        driver.findElement(By.id("password")).sendKeys(password);
+        driver.findElement(By.cssSelector("input[type=\"submit\"]")).click();
+    }
+
+    public AccessTokenResponse doAccessTokenRequest(String code, String password) {
         HttpClient client = new DefaultHttpClient();
         HttpPost post = new HttpPost(getAccessTokenUrl());
 
@@ -115,27 +133,19 @@ public class OAuthClient {
         UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(parameters, Charset.forName("UTF-8"));
         post.setEntity(formEntity);
 
-        return new AccessTokenResponse(client.execute(post));
-    }
-
-    public SkeletonKeyToken verifyToken(String token) throws Exception {
-        return RSATokenVerifier.verifyToken(token, realmPublicKey, realm);
-    }
-
-    public boolean isAuthorizationResponse() {
-        return getCurrentRequest().equals(redirectUri) && getCurrentQuery().containsKey("code");
-    }
-
-    public String getState() {
-        return state;
-    }
-
-    public String getClientId() {
-        return clientId;
+        try {
+            return new AccessTokenResponse(client.execute(post));
+        } catch (Exception e) {
+            throw new AssertionError("Failed to retrieve access token", e);
+        }
     }
 
-    public String getResponseType() {
-        return responseType;
+    public SkeletonKeyToken verifyToken(String token) {
+        try {
+            return RSATokenVerifier.verifyToken(token, realmPublicKey, realm);
+        } catch (VerificationException e) {
+            throw new AssertionError("Failed to verify token", e);
+        }
     }
 
     public String getCurrentRequest() {
@@ -175,10 +185,6 @@ public class OAuthClient {
         return redirectUri;
     }
 
-    public String getScope() {
-        return scope;
-    }
-
     public String getLoginFormUrl() {
         UriBuilder b = UriBuilder.fromUri(baseUrl + "/realms/" + realm + "/tokens/login");
         if (responseType != null) {
@@ -191,7 +197,12 @@ public class OAuthClient {
             b.queryParam("redirect_uri", redirectUri);
         }
         if (scope != null) {
-            b.queryParam("scope", scope);
+            try {
+
+                b.queryParam("scope", Base64Url.encode(JsonSerialization.toByteArray(scope, false)));
+            } catch (Exception e) {
+                throw new AssertionError("Failed to serialize scope", e);
+            }
         }
         if (state != null) {
             b.queryParam("state", state);
@@ -224,8 +235,11 @@ public class OAuthClient {
         return this;
     }
 
-    public OAuthClient scope(String scope) {
-        this.scope = scope;
+    public OAuthClient addScope(String resource, String... roles) {
+        if (scope == null) {
+            scope = new SkeletonKeyScope();
+        }
+        scope.addAll(resource, roles);
         return this;
     }
 
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AbstractPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AbstractPage.java
index 1183ece..c17b86a 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AbstractPage.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AbstractPage.java
@@ -41,6 +41,6 @@ public abstract class AbstractPage {
 
     abstract boolean isCurrent();
 
-    abstract void open();
+    abstract void open() throws Exception;
 
 }