From 6f6a467c7b21c6314d2fe35a364c237c0966d8c4 Mon Sep 17 00:00:00 2001 From: Takashi Norimatsu Date: Wed, 4 Oct 2017 12:59:49 +0900 Subject: [PATCH] OIDC Financial API Read Only Profile : scope MUST be returned in the response from Token Endpoint --- .../representations/AccessTokenResponse.java | 10 + .../keycloak/protocol/oidc/TokenManager.java | 50 +++++ .../keycloak/testsuite/util/OAuthClient.java | 12 + .../oauth/OAuthScopeInTokenResponseTest.java | 208 ++++++++++++++++++ 4 files changed, 280 insertions(+) create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthScopeInTokenResponseTest.java diff --git a/core/src/main/java/org/keycloak/representations/AccessTokenResponse.java b/core/src/main/java/org/keycloak/representations/AccessTokenResponse.java index b211aef7f3..6539cd9420 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 otherClaims = new HashMap(); + // 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 d4bb40ffa0..39a4215b84 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 returnedScopes = new ArrayList(); + // at attachAuthenticationSession(), take over notes from AuthenticationSessionModel to AuthenticatedClientSessionModel + List 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 returnedScopes, List requestedScopes, Set roles) { + for (String r : roles) { + for (String s : requestedScopes) { + if (s.equals(r)) { + returnedScopes.add(s); + } + } + } + } + + private void addRolesAsScopes(List returnedScopes, List requestedScopes, Set 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 7a3a5b3bd6..53d3f48eb8 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 0000000000..e6c8253cb4 --- /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 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 expectedScopes = Arrays.asList(expectedScope.split(" ")); + Collection receivedScopes = Arrays.asList(response.getScope().split(" ")); + Assert.assertTrue(expectedScopes.containsAll(receivedScopes) && receivedScopes.containsAll(expectedScopes)); + } +}