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));
+ }
+}