keycloak-uncached

Merge pull request #4527 from Hitachi/master OIDC Financial

10/10/2017 6:37:45 AM

Details

diff --git a/core/src/main/java/org/keycloak/representations/AccessTokenResponse.java b/core/src/main/java/org/keycloak/representations/AccessTokenResponse.java
index b211aef..6539cd9 100755
--- a/core/src/main/java/org/keycloak/representations/AccessTokenResponse.java
+++ b/core/src/main/java/org/keycloak/representations/AccessTokenResponse.java
@@ -57,7 +57,17 @@ public class AccessTokenResponse {
 
     protected Map<String, Object> otherClaims = new HashMap<String, Object>();
 
+    // OIDC Financial API Read Only Profile : scope MUST be returned in the response from Token Endpoint
+    @JsonProperty("scope")
+    protected String scope;
 
+    public String getScope() {
+        return scope;
+    }
+
+    public void setScope(String scope) {
+        this.scope = scope;
+    }
 
     public String getToken() {
         return token;
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
index d4bb40f..39a4215 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
@@ -70,6 +70,7 @@ import javax.ws.rs.core.Response;
 import javax.ws.rs.core.UriInfo;
 
 import java.security.PublicKey;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
@@ -854,8 +855,57 @@ public class TokenManager {
             if (userNotBefore > notBefore) notBefore = userNotBefore;
             res.setNotBeforePolicy(notBefore);
 
+            // OIDC Financial API Read Only Profile : scope MUST be returned in the response from Token Endpoint
+            String requestedScope = clientSession.getNote(OAuth2Constants.SCOPE);
+            if (accessToken != null && requestedScope != null) {
+                List<String> returnedScopes = new ArrayList<String>();
+                // at attachAuthenticationSession(), take over notes from AuthenticationSessionModel to AuthenticatedClientSessionModel
+                List<String> requestedScopes = Arrays.asList(requestedScope.split(" "));
+
+                // distinguish between so called role scope and oauth scope
+                // only pick up oauth scope following https://tools.ietf.org/html/rfc6749#section-5.1
+
+                // for realm role - scope
+                if (accessToken.getRealmAccess() != null && accessToken.getRealmAccess().getRoles() != null) {
+                    addRolesAsScopes(returnedScopes, requestedScopes, accessToken.getRealmAccess().getRoles());
+                }
+                // for client role - scope
+                if (accessToken.getResourceAccess() != null) {
+                    for (String clientId : accessToken.getResourceAccess().keySet()) {
+                        if (accessToken.getResourceAccess(clientId).getRoles() != null) {
+                            addRolesAsScopes(returnedScopes, requestedScopes, accessToken.getResourceAccess(clientId).getRoles(), clientId);
+                        }
+                    }
+                }
+                StringBuilder builder = new StringBuilder();
+                for (String s : returnedScopes) {
+                    builder.append(s).append(" ");
+                }
+                res.setScope(builder.toString().trim());
+            }
+
             return res;
         }
+
+        private void addRolesAsScopes(List<String> returnedScopes, List<String> requestedScopes, Set<String> roles) {
+            for (String r : roles) {
+                for (String s : requestedScopes) {
+                    if (s.equals(r)) {
+                        returnedScopes.add(s);
+                    }
+                }
+            }
+        }
+
+        private void addRolesAsScopes(List<String> returnedScopes, List<String> requestedScopes, Set<String> roles, String clientId) {
+            for (String r : roles) {
+                for (String s : requestedScopes) {
+                    if (s.equals(clientId + "/" + r)) {
+                        returnedScopes.add(s);
+                    }
+                }
+            }       	
+        }
     }
 
     public class RefreshResult {
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java
index 7a3a5b3..53d3f48 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java
@@ -949,6 +949,8 @@ public class OAuthClient {
         private int expiresIn;
         private int refreshExpiresIn;
         private String refreshToken;
+        // OIDC Financial API Read Only Profile : scope MUST be returned in the response from Token Endpoint
+        private String scope;
 
         private String error;
         private String errorDescription;
@@ -970,6 +972,11 @@ public class OAuthClient {
                     expiresIn = (Integer) responseJson.get("expires_in");
                     refreshExpiresIn = (Integer) responseJson.get("refresh_expires_in");
 
+                    // OIDC Financial API Read Only Profile : scope MUST be returned in the response from Token Endpoint
+                    if (responseJson.containsKey(OAuth2Constants.SCOPE)) {
+                        scope = (String) responseJson.get(OAuth2Constants.SCOPE);
+                    }
+
                     if (responseJson.containsKey(OAuth2Constants.REFRESH_TOKEN)) {
                         refreshToken = (String) responseJson.get(OAuth2Constants.REFRESH_TOKEN);
                     }
@@ -1017,6 +1024,11 @@ public class OAuthClient {
         public String getTokenType() {
             return tokenType;
         }
+
+        // OIDC Financial API Read Only Profile : scope MUST be returned in the response from Token Endpoint
+        public String getScope() {
+            return scope;
+        }
     }
 
     public PublicKey getRealmPublicKey(String realm) {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthScopeInTokenResponseTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthScopeInTokenResponseTest.java
new file mode 100644
index 0000000..e6c8253
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthScopeInTokenResponseTest.java
@@ -0,0 +1,208 @@
+package org.keycloak.testsuite.oauth;
+
+import static org.junit.Assert.assertEquals;
+import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.testsuite.AbstractKeycloakTest;
+import org.keycloak.testsuite.util.OAuthClient;
+
+//OIDC Financial API Read Only Profile : scope MUST be returned in the response from Token Endpoint
+public class OAuthScopeInTokenResponseTest extends AbstractKeycloakTest {
+
+    @Override
+    public void beforeAbstractKeycloakTest() throws Exception {
+        super.beforeAbstractKeycloakTest();
+    }
+
+    @Override
+    public void addTestRealms(List<RealmRepresentation> testRealms) {
+        RealmRepresentation realm = loadJson(getClass().getResourceAsStream("/testrealm.json"), RealmRepresentation.class);
+        testRealms.add(realm);
+    }
+
+    @Test
+    public void specifyNoScopeTest() throws Exception {
+        String loginUser = "john-doh@localhost";
+        String loginPassword = "password";
+        String clientSecret = "password";
+        
+    	String expectedScope = "";
+    	
+        oauth.doLogin(loginUser, loginPassword);
+
+        String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+        
+        expectSuccessfulResponseFromTokenEndpoint(code, expectedScope, clientSecret);
+    }
+    
+    @Test
+    public void specifySingleScopeAsRealmRoleTest() throws Exception {
+        String loginUser = "john-doh@localhost";
+        String loginPassword = "password";
+        String clientSecret = "password";
+        
+    	String requestedScope = "user";
+    	String expectedScope = requestedScope;
+    	
+    	oauth.scope(requestedScope);
+        oauth.doLogin(loginUser, loginPassword);
+
+        String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+        
+        expectSuccessfulResponseFromTokenEndpoint(code, expectedScope, clientSecret);
+    }
+    
+    @Test
+    public void specifyMultipleScopeAsRealmRoleTest() throws Exception {
+        String loginUser = "rich.roles@redhat.com";
+        String loginPassword = "password";
+        String clientSecret = "password";
+        
+    	String requestedScope = "user realm-composite-role";
+    	String expectedScope = requestedScope;
+    	
+    	oauth.scope(requestedScope);
+        oauth.doLogin(loginUser, loginPassword);
+
+        String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+        
+        expectSuccessfulResponseFromTokenEndpoint(code, expectedScope, clientSecret);
+    }
+
+    @Test
+    public void specifyNotAssignedScopeAsRealmRoleTest() throws Exception {
+        String loginUser = "john-doh@localhost";
+        String loginPassword = "password";
+        String clientSecret = "password";
+        
+    	String requestedScope = "user realm-composite-role";
+    	String expectedScope = "user";
+    	
+    	oauth.scope(requestedScope);
+        oauth.doLogin(loginUser, loginPassword);
+
+        String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+        
+        expectSuccessfulResponseFromTokenEndpoint(code, expectedScope, clientSecret);
+    }
+
+    @Test
+    public void specifySingleScopeAsClientRoleTest() throws Exception {
+        String loginUser = "john-doh@localhost";
+        String loginPassword = "password";
+        String clientSecret = "password";
+        
+    	String requestedScope = "test-app/customer-user";
+    	String expectedScope = requestedScope;
+    	
+    	oauth.scope(requestedScope);
+        oauth.doLogin(loginUser, loginPassword);
+
+        String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+        
+        expectSuccessfulResponseFromTokenEndpoint(code, expectedScope, clientSecret);
+    }
+
+    @Test
+    public void specifyMultipleScopeAsClientRoleTest() throws Exception {
+        String loginUser = "rich.roles@redhat.com";
+        String loginPassword = "password";
+        String clientSecret = "password";
+        
+    	String requestedScope = "test-app-scope/test-app-disallowed-by-scope test-app-scope/test-app-allowed-by-scope";
+    	String expectedScope = requestedScope;
+    	
+    	oauth.scope(requestedScope);
+        oauth.doLogin(loginUser, loginPassword);
+
+        String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+        
+        expectSuccessfulResponseFromTokenEndpoint(code, expectedScope, clientSecret);
+    }
+
+    @Test
+    public void specifyNotAssignedScopeAsClientRoleTest() throws Exception {
+        String loginUser = "rich.roles@redhat.com";
+        String loginPassword = "password";
+        String clientSecret = "password";
+        
+    	String requestedScope = "test-app-scope/test-app-unspecified-by-scope test-app-scope/test-app-allowed-by-scope";
+    	String expectedScope = "test-app-scope/test-app-allowed-by-scope";
+    	
+    	oauth.scope(requestedScope);
+        oauth.doLogin(loginUser, loginPassword);
+
+        String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+        
+        expectSuccessfulResponseFromTokenEndpoint(code, expectedScope, clientSecret);
+    }
+
+    @Test
+    public void specifyMultipleScopeAsRealmAndClientRoleTest() throws Exception {
+        String loginUser = "rich.roles@redhat.com";
+        String loginPassword = "password";
+        String clientSecret = "password";
+        
+    	String requestedScope = "test-app-scope/test-app-disallowed-by-scope admin test-app/customer-user test-app-scope/test-app-allowed-by-scope";
+    	String expectedScope = requestedScope;
+    	
+    	oauth.scope(requestedScope);
+        oauth.doLogin(loginUser, loginPassword);
+
+        String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+        
+        expectSuccessfulResponseFromTokenEndpoint(code, expectedScope, clientSecret);
+    }
+
+    @Test
+    public void specifyNotAssignedScopeAsRealmAndClientRoleTest() throws Exception {
+        String loginUser = "john-doh@localhost";
+        String loginPassword = "password";
+        String clientSecret = "password";
+        
+    	String requestedScope = "test-app/customer-user test-app-scope/test-app-disallowed-by-scope admin test-app/customer-user user test-app-scope/test-app-allowed-by-scope";
+    	String expectedScope = "user test-app/customer-user";
+    	
+    	oauth.scope(requestedScope);
+        oauth.doLogin(loginUser, loginPassword);
+
+        String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+        
+        expectSuccessfulResponseFromTokenEndpoint(code, expectedScope, clientSecret);
+    }
+ 
+    @Test
+    public void specifyDuplicatedScopeAsRealmAndClientRoleTest() throws Exception {
+        String loginUser = "john-doh@localhost";
+        String loginPassword = "password";
+        String clientSecret = "password";
+        
+    	String requestedScope = "test-app/customer-user user user test-app/customer-user";
+    	String expectedScope = "user test-app/customer-user";
+    	
+    	oauth.scope(requestedScope);
+        oauth.doLogin(loginUser, loginPassword);
+
+        String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+        
+        expectSuccessfulResponseFromTokenEndpoint(code, expectedScope, clientSecret);
+    }
+    
+    private void expectSuccessfulResponseFromTokenEndpoint(String code, String expectedScope, String clientSecret) throws Exception {
+    	OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, clientSecret);
+        assertEquals(200, response.getStatusCode());
+        log.info("expectedScopes = " + expectedScope);
+        log.info("receivedScopes = " + response.getScope());
+    	Collection<String> expectedScopes = Arrays.asList(expectedScope.split(" "));
+    	Collection<String> receivedScopes = Arrays.asList(response.getScope().split(" "));
+    	Assert.assertTrue(expectedScopes.containsAll(receivedScopes) && receivedScopes.containsAll(expectedScopes));
+    }
+}