Repository: gravitee-io/graviteeio-access-management (java)

Issue:
[oidc] Requesting Claims using Scope Values
https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims

Pull request description:
feat(oidc): requesting claims using scope values
closes #380

Patch:
diff --git a/gravitee-am-common/src/main/java/io/gravitee/am/common/oidc/Scope.java b/gravitee-am-common/src/main/java/io/gravitee/am/common/oidc/Scope.java
index bec85addd2..8044cb4eed 100644
--- a/gravitee-am-common/src/main/java/io/gravitee/am/common/oidc/Scope.java
+++ b/gravitee-am-common/src/main/java/io/gravitee/am/common/oidc/Scope.java
@@ -55,4 +55,13 @@ public enum Scope {
     public List<String> getClaims() {
         return claims;
     }
+
+    public static boolean exists(String scope) {
+        try {
+            Scope.valueOf(scope);
+            return true;
+        } catch (IllegalArgumentException e) {
+            return false;
+        }
+    }
 }
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/oidc/idtoken/impl/IDTokenServiceImpl.java b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/oidc/idtoken/impl/IDTokenServiceImpl.java
index 6360926f2d..2711c72c33 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/oidc/idtoken/impl/IDTokenServiceImpl.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/oidc/idtoken/impl/IDTokenServiceImpl.java
@@ -18,7 +18,7 @@ package io.gravitee.am.gateway.handler.oidc.idtoken.impl;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import io.gravitee.am.certificate.api.CertificateMetadata;
 import io.gravitee.am.certificate.api.CertificateProvider;
-import io.gravitee.am.common.oidc.StandardClaims;
+import io.gravitee.am.common.oidc.Scope;
 import io.gravitee.am.gateway.handler.oauth2.certificate.CertificateManager;
 import io.gravitee.am.gateway.handler.oauth2.request.OAuth2Request;
 import io.gravitee.am.gateway.handler.oauth2.utils.OIDCParameters;
@@ -44,10 +44,8 @@ import org.springframework.beans.factory.annotation.Value;
 
 import java.security.Key;
 import java.security.SecureRandom;
-import java.util.Calendar;
-import java.util.Collections;
-import java.util.Map;
-import java.util.TimeZone;
+import java.util.*;
+import java.util.stream.Collectors;
 
 /**
  * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
@@ -80,9 +78,6 @@ public class IDTokenServiceImpl implements IDTokenService, InitializingBean {
         Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
         idToken.setIss(iss);
         idToken.setSub(oAuth2Request.isClientOnly() ? oAuth2Request.getClientId() : user.getId());
-        if (!oAuth2Request.isClientOnly() && user.getUsername() != null) {
-            idToken.addAdditionalClaim(StandardClaims.PREFERRED_USERNAME, user.getUsername());
-        }
         idToken.setAud(oAuth2Request.getClientId());
         idToken.setIat(calendar.getTimeInMillis() / 1000l);
 
@@ -109,12 +104,15 @@ public class IDTokenServiceImpl implements IDTokenService, InitializingBean {
             final Map<String, Object> userAdditionalInformation = user.getAdditionalInformation();
             boolean requestForSpecificClaims = false;
             // processing claims list
-            // 1. process the request using the claims values (If present, the listed Claims are being requested to be added to the default Claims in the ID Token)
-            // 2. If not present, check if the client has enabled the ID token mapping claims.
-            // 3. Else send all user claims
+            // 1. process the request using scope values
+            if (oAuth2Request.getScopes() != null) {
+                requestForSpecificClaims = processScopesRequest(oAuth2Request.getScopes(), userAdditionalInformation, idToken);
+            }
             MultiValueMap<String, String> requestedParameters = oAuth2Request.getRequestParameters();
+            // 2. process the request using the claims values (If present, the listed Claims are being requested to be added to the default Claims in the ID Token)
             if (requestedParameters != null && requestedParameters.getFirst(OIDCParameters.CLAIMS) != null) {
                 requestForSpecificClaims = processClaimsRequest(requestedParameters.getFirst(OIDCParameters.CLAIMS), userAdditionalInformation, idToken);
+            // 3. If not present, check if the client has enabled the ID token mapping claims.
             } else if (client.getIdTokenCustomClaims() != null) {
                 client.getIdTokenCustomClaims().forEach((key, value) -> {
                     if (userAdditionalInformation.get(value) != null) {
@@ -123,6 +121,7 @@ public class IDTokenServiceImpl implements IDTokenService, InitializingBean {
                 });
                 requestForSpecificClaims = true;
             }
+            // 4. Else send all user claims
             if (!requestForSpecificClaims) {
                 idToken.setAdditionalClaims(userAdditionalInformation);
             }
@@ -163,6 +162,39 @@ public class IDTokenServiceImpl implements IDTokenService, InitializingBean {
         setDefaultCertificateProvider(jwtBuilder);
     }
 
+    /**
+     * For OpenID Connect, scopes can be used to request that specific sets of information be made available as Claim Values.
+     *
+     * @param scopes scopes request parameter
+     * @param userClaims user full claims list
+     * @param requestedClaims requested claims
+     * @return true if OpenID Connect scopes have been found
+     */
+    private boolean processScopesRequest(Set<String> scopes, final Map<String, Object> userClaims, Map<String, Object> requestedClaims) {
+        // get requested scopes claims
+        final List<String> scopesClaims = scopes.stream()
+                .map(scope -> scope.toUpperCase())
+                .filter(scope -> Scope.exists(scope) && !Scope.valueOf(scope).getClaims().isEmpty())
+                .map(scope -> Scope.valueOf(scope))
+                .map(scope -> scope.getClaims())
+                .flatMap(List::stream)
+                .collect(Collectors.toList());
+
+        // no OpenID Connect scopes requested continue
+        if (scopesClaims.isEmpty()) {
+            return false;
+        }
+
+        // return specific available sets of information made by scope value request
+        scopesClaims.forEach(scopeClaim -> {
+            if (userClaims.containsKey(scopeClaim)) {
+                requestedClaims.putIfAbsent(scopeClaim, userClaims.get(scopeClaim));
+            }
+        });
+
+        return true;
+    }
+
     /**
      * Handle claims request previously made during the authorization request
      * @param claimsValue claims request parameter
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/oidc/endpoint/UserInfoEndpoint.java b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/oidc/endpoint/UserInfoEndpoint.java
index ae33fa3fd7..3ba592ba6f 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/oidc/endpoint/UserInfoEndpoint.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/oidc/endpoint/UserInfoEndpoint.java
@@ -15,6 +15,7 @@
  */
 package io.gravitee.am.gateway.handler.vertx.handler.oidc.endpoint;
 
+import io.gravitee.am.common.oidc.Scope;
 import io.gravitee.am.common.oidc.StandardClaims;
 import io.gravitee.am.gateway.handler.oauth2.exception.InvalidRequestException;
 import io.gravitee.am.gateway.handler.oauth2.token.AccessToken;
@@ -28,8 +29,8 @@ import io.vertx.core.Handler;
 import io.vertx.core.json.Json;
 import io.vertx.reactivex.ext.web.RoutingContext;
 
-import java.util.HashMap;
-import java.util.Map;
+import java.util.*;
+import java.util.stream.Collectors;
 
 /**
  * The UserInfo Endpoint is an OAuth 2.0 Protected Resource that returns Claims about the authenticated End-User.
@@ -73,16 +74,21 @@ public class UserInfoEndpoint implements Handler<RoutingContext> {
                         throw new InvalidRequestException("UserInfo response is missing required claims");
                     }
 
-                    // Exchange the sub claim from the identity provider to the preferred_username
-                    userClaims.put(StandardClaims.PREFERRED_USERNAME, userClaims.get(StandardClaims.SUB));
+                    // Exchange the sub claim from the identity provider to its technical id
                     userClaims.put(StandardClaims.SUB, subject);
 
+                    // prepare requested claims
                     Map<String, Object> requestedClaims = new HashMap<>();
+                    // SUB claim is required
+                    requestedClaims.put(StandardClaims.SUB, subject);
 
                     boolean requestForSpecificClaims = false;
                     // processing claims list
                     // 1. process the request using scope values
-                    // TODO : 5.4. Requesting Claims using Scope Values (https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims)
+                    if (accessToken.getScope() != null) {
+                        final Set<String> scopes = new HashSet(Arrays.asList(accessToken.getScope().split("\\s+")));
+                        requestForSpecificClaims = processScopesRequest(scopes, userClaims, requestedClaims);
+                    }
                     // 2. process the request using the claims values (If present, the listed Claims are being requested to be added to any Claims that are being requested using scope values.
                     // If not present, the Claims being requested from the UserInfo Endpoint are only those requested using scope values.)
                     Map<String, String> requestedParameters = accessToken.getRequestedParameters();
@@ -90,9 +96,6 @@ public class UserInfoEndpoint implements Handler<RoutingContext> {
                         requestForSpecificClaims = processClaimsRequest(requestedParameters.get(OIDCParameters.CLAIMS), userClaims, requestedClaims);
                     }
 
-                    // Put at the end of the process to avoid override by identity provider
-                    requestedClaims.put(StandardClaims.SUB, subject);
-
                     return (requestForSpecificClaims) ? requestedClaims : userClaims;
                  })
                 .subscribe(claims -> context.response()
@@ -104,6 +107,39 @@ public class UserInfoEndpoint implements Handler<RoutingContext> {
 
     }
 
+    /**
+     * For OpenID Connect, scopes can be used to request that specific sets of information be made available as Claim Values.
+     *
+     * @param scopes scopes request parameter
+     * @param userClaims user full claims list
+     * @param requestedClaims requested claims
+     * @return true if OpenID Connect scopes have been found
+     */
+    private boolean processScopesRequest(Set<String> scopes, final Map<String, Object> userClaims, Map<String, Object> requestedClaims) {
+        // get requested scopes claims
+        final List<String> scopesClaims = scopes.stream()
+                .map(scope -> scope.toUpperCase())
+                .filter(scope -> Scope.exists(scope) && !Scope.valueOf(scope).getClaims().isEmpty())
+                .map(scope -> Scope.valueOf(scope))
+                .map(scope -> scope.getClaims())
+                .flatMap(List::stream)
+                .collect(Collectors.toList());
+
+        // no OpenID Connect scopes requested continue
+        if (scopesClaims.isEmpty()) {
+            return false;
+        }
+
+        // return specific available sets of information made by scope value request
+        scopesClaims.forEach(scopeClaim -> {
+            if (userClaims.containsKey(scopeClaim)) {
+                requestedClaims.putIfAbsent(scopeClaim, userClaims.get(scopeClaim));
+            }
+        });
+
+        return true;
+    }
+
     /**
      * Handle claims request previously made during the authorization request
      * @param claimsValue claims request parameter
diff --git a/gravitee-am-identityprovider/gravitee-am-identityprovider-github/src/main/java/io/gravitee/am/identityprovider/github/authentication/GithubAuthenticationProvider.java b/gravitee-am-identityprovider/gravitee-am-identityprovider-github/src/main/java/io/gravitee/am/identityprovider/github/authentication/GithubAuthenticationProvider.java
index 5af946ceaf..d75c156082 100644
--- a/gravitee-am-identityprovider/gravitee-am-identityprovider-github/src/main/java/io/gravitee/am/identityprovider/github/authentication/GithubAuthenticationProvider.java
+++ b/gravitee-am-identityprovider/gravitee-am-identityprovider-github/src/main/java/io/gravitee/am/identityprovider/github/authentication/GithubAuthenticationProvider.java
@@ -158,6 +158,7 @@ public class GithubAuthenticationProvider implements OAuth2AuthenticationProvide
         // Standard claims
         additionalInformation.put(StandardClaims.SUB, jsonObject.getValue(GithubUser.LOGIN));
         additionalInformation.put(StandardClaims.NAME, jsonObject.getValue(GithubUser.NAME));
+        additionalInformation.put(StandardClaims.PREFERRED_USERNAME, jsonObject.getValue(GithubUser.LOGIN));
 
         // try to get the first name and last name
         try {
diff --git a/gravitee-am-identityprovider/gravitee-am-identityprovider-inline/src/main/java/io/gravitee/am/identityprovider/inline/authentication/InlineAuthenticationProvider.java b/gravitee-am-identityprovider/gravitee-am-identityprovider-inline/src/main/java/io/gravitee/am/identityprovider/inline/authentication/InlineAuthenticationProvider.java
index 7c9df326e0..bf0067426a 100644
--- a/gravitee-am-identityprovider/gravitee-am-identityprovider-inline/src/main/java/io/gravitee/am/identityprovider/inline/authentication/InlineAuthenticationProvider.java
+++ b/gravitee-am-identityprovider/gravitee-am-identityprovider-inline/src/main/java/io/gravitee/am/identityprovider/inline/authentication/InlineAuthenticationProvider.java
@@ -112,6 +112,7 @@ public class InlineAuthenticationProvider implements AuthenticationProvider, Ini
         claims.put(StandardClaims.NAME, inlineUser.getFirstname() + " " + inlineUser.getLastname());
         claims.put(StandardClaims.GIVEN_NAME, inlineUser.getFirstname());
         claims.put(StandardClaims.FAMILY_NAME, inlineUser.getLastname());
+        claims.put(StandardClaims.PREFERRED_USERNAME, inlineUser.getUsername());
         user.setAdditonalInformation(claims);
 
         // set user roles
diff --git a/gravitee-am-identityprovider/gravitee-am-identityprovider-ldap/src/main/java/io/gravitee/am/identityprovider/ldap/authentication/LdapAuthenticationProvider.java b/gravitee-am-identityprovider/gravitee-am-identityprovider-ldap/src/main/java/io/gravitee/am/identityprovider/ldap/authentication/LdapAuthenticationProvider.java
index 0272f423ea..8fb2bf746d 100644
--- a/gravitee-am-identityprovider/gravitee-am-identityprovider-ldap/src/main/java/io/gravitee/am/identityprovider/ldap/authentication/LdapAuthenticationProvider.java
+++ b/gravitee-am-identityprovider/gravitee-am-identityprovider-ldap/src/main/java/io/gravitee/am/identityprovider/ldap/authentication/LdapAuthenticationProvider.java
@@ -206,6 +206,7 @@ public class LdapAuthenticationProvider implements AuthenticationProvider, Initi
             addClaim(claims, ldapEntry, StandardClaims.GIVEN_NAME, "givenname");
             addClaim(claims, ldapEntry, StandardClaims.FAMILY_NAME, "sn");
             addClaim(claims, ldapEntry, StandardClaims.EMAIL, "mail");
+            addClaim(claims, ldapEntry, StandardClaims.PREFERRED_USERNAME, user.getUsername());
         }
         user.setAdditonalInformation(claims);
 
diff --git a/gravitee-am-identityprovider/gravitee-am-identityprovider-mongo/src/main/java/io/gravitee/am/identityprovider/mongo/authentication/MongoAuthenticationProvider.java b/gravitee-am-identityprovider/gravitee-am-identityprovider-mongo/src/main/java/io/gravitee/am/identityprovider/mongo/authentication/MongoAuthenticationProvider.java
index 1032344d94..6bd8a523fa 100644
--- a/gravitee-am-identityprovider/gravitee-am-identityprovider-mongo/src/main/java/io/gravitee/am/identityprovider/mongo/authentication/MongoAuthenticationProvider.java
+++ b/gravitee-am-identityprovider/gravitee-am-identityprovider-mongo/src/main/java/io/gravitee/am/identityprovider/mongo/authentication/MongoAuthenticationProvider.java
@@ -94,6 +94,7 @@ public class MongoAuthenticationProvider implements AuthenticationProvider {
         DefaultUser user = new DefaultUser(username);
         Map<String, Object> claims = new HashMap<>();
         claims.put(StandardClaims.SUB, username);
+        claims.put(StandardClaims.PREFERRED_USERNAME, username);
         if(this.mapper.getMappers() != null) {
             this.mapper.getMappers().forEach((k, v) -> claims.put(k, document.getString(v)));
         }
diff --git a/gravitee-am-identityprovider/gravitee-am-identityprovider-oauth2-generic/src/main/java/io/gravitee/am/identityprovider/oauth2/authentication/OAuth2GenericAuthenticationProvider.java b/gravitee-am-identityprovider/gravitee-am-identityprovider-oauth2-generic/src/main/java/io/gravitee/am/identityprovider/oauth2/authentication/OAuth2GenericAuthenticationProvider.java
index a6c0867da2..f9ce8d3c85 100644
--- a/gravitee-am-identityprovider/gravitee-am-identityprovider-oauth2-generic/src/main/java/io/gravitee/am/identityprovider/oauth2/authentication/OAuth2GenericAuthenticationProvider.java
+++ b/gravitee-am-identityprovider/gravitee-am-identityprovider-oauth2-generic/src/main/java/io/gravitee/am/identityprovider/oauth2/authentication/OAuth2GenericAuthenticationProvider.java
@@ -156,6 +156,7 @@ public class OAuth2GenericAuthenticationProvider implements OAuth2Authentication
         // set additional information
         Map<String, Object> additionalInformation = new HashMap<>();
         additionalInformation.put(StandardClaims.SUB, jsonNode.getValue(StandardClaims.SUB));
+        additionalInformation.put(StandardClaims.PREFERRED_USERNAME, jsonNode.getValue(StandardClaims.SUB));
         if (this.mapper.getMappers() != null) {
             this.mapper.getMappers().forEach((k, v) -> {
                 if (jsonNode.getValue(v) != null) {


Test patch:
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/src/test/java/io/gravitee/am/gateway/handler/oidc/idtoken/IDTokenServiceTest.java b/gravitee-am-gateway/gravitee-am-gateway-handler/src/test/java/io/gravitee/am/gateway/handler/oidc/idtoken/IDTokenServiceTest.java
index 5206275194..06afdbfa1a 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/src/test/java/io/gravitee/am/gateway/handler/oidc/idtoken/IDTokenServiceTest.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/src/test/java/io/gravitee/am/gateway/handler/oidc/idtoken/IDTokenServiceTest.java
@@ -16,6 +16,7 @@
 package io.gravitee.am.gateway.handler.oidc.idtoken;
 
 import io.gravitee.am.certificate.api.CertificateProvider;
+import io.gravitee.am.common.oidc.StandardClaims;
 import io.gravitee.am.gateway.handler.oauth2.certificate.CertificateManager;
 import io.gravitee.am.gateway.handler.oauth2.request.OAuth2Request;
 import io.gravitee.am.gateway.handler.oidc.idtoken.impl.IDTokenServiceImpl;
@@ -36,9 +37,7 @@ import org.mockito.Mock;
 import org.mockito.runners.MockitoJUnitRunner;
 
 import java.security.Key;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
+import java.util.*;
 
 import static org.mockito.Matchers.anyString;
 import static org.mockito.Mockito.*;
@@ -127,12 +126,7 @@ public class IDTokenServiceTest {
 
         Client client = new Client();
 
-        User user = new User();
-        Map<String, Object> additionalInformation  = new HashMap<>();
-        additionalInformation.put("sub", "user");
-        additionalInformation.put("name", "gravitee user");
-        additionalInformation.put("family_name", "gravitee");
-        user.setAdditionalInformation(additionalInformation);
+        User user = createUser();
 
         AccessToken accessToken = new AccessToken();
         accessToken.setId("token-id");
@@ -159,4 +153,188 @@ public class IDTokenServiceTest {
         verify(certificateManager, times(1)).get(anyString());
         verify(certificateProvider, never()).sign(anyString());
     }
+
+    @Test
+    public void shouldCreateIDToken_withUser_scopesRequest() {
+        OAuth2Request oAuth2Request = new OAuth2Request();
+        oAuth2Request.setClientId("client-id");
+        oAuth2Request.setScopes(new HashSet<>(Arrays.asList("openid", "profile")));
+        oAuth2Request.setSubject("subject");
+
+        Client client = new Client();
+
+        User user = createUser();
+
+        AccessToken accessToken = new AccessToken();
+        accessToken.setId("token-id");
+        accessToken.setToken("token-id");
+        accessToken.setScopes(new HashSet<>(Arrays.asList("openid", "profile")));
+
+        Key key = MacSigner.generateKey();
+        JwtParser parser = Jwts.parser().setSigningKey(key);
+        jwtBuilder = Jwts.builder().signWith(SignatureAlgorithm.HS512, key);
+        ((IDTokenServiceImpl) idTokenService).setJwtBuilder(jwtBuilder);
+
+        when(certificateManager.get(anyString())).thenReturn(Maybe.empty());
+
+        TestObserver<String> testObserver = idTokenService.create(oAuth2Request, client, user).test();
+
+        testObserver.assertComplete();
+        testObserver.assertNoErrors();
+        testObserver.assertValue(idToken -> {
+            Jwt jwt = parser.parse(idToken);
+            Map<String, Object> claims = (Map<String, Object>) jwt.getBody();
+            return claims.size() == 15 + 4; // 4 ID Token standard claims
+        });
+
+        verify(certificateManager, times(1)).get(anyString());
+        verify(certificateProvider, never()).sign(anyString());
+    }
+
+    @Test
+    public void shouldCreateIDToken_withUser_scopesRequest_email() {
+        OAuth2Request oAuth2Request = new OAuth2Request();
+        oAuth2Request.setClientId("client-id");
+        oAuth2Request.setScopes(new HashSet<>(Arrays.asList("openid", "email")));
+        oAuth2Request.setSubject("subject");
+
+        Client client = new Client();
+
+        User user = createUser();
+
+        AccessToken accessToken = new AccessToken();
+        accessToken.setId("token-id");
+        accessToken.setToken("token-id");
+        accessToken.setScopes(new HashSet<>(Arrays.asList("openid", "email")));
+
+        Key key = MacSigner.generateKey();
+        JwtParser parser = Jwts.parser().setSigningKey(key);
+        jwtBuilder = Jwts.builder().signWith(SignatureAlgorithm.HS512, key);
+        ((IDTokenServiceImpl) idTokenService).setJwtBuilder(jwtBuilder);
+
+        when(certificateManager.get(anyString())).thenReturn(Maybe.empty());
+
+        TestObserver<String> testObserver = idTokenService.create(oAuth2Request, client, user).test();
+
+        testObserver.assertComplete();
+        testObserver.assertNoErrors();
+        testObserver.assertValue(idToken -> {
+            Jwt jwt = parser.parse(idToken);
+            Map<String, Object> claims = (Map<String, Object>) jwt.getBody();
+            return claims.size() == 3 + 4; // 4 ID Token standard claims
+        });
+
+        verify(certificateManager, times(1)).get(anyString());
+        verify(certificateProvider, never()).sign(anyString());
+    }
+
+    @Test
+    public void shouldCreateIDToken_withUser_scopesRequest_email_address() {
+        OAuth2Request oAuth2Request = new OAuth2Request();
+        oAuth2Request.setClientId("client-id");
+        oAuth2Request.setScopes(new HashSet<>(Arrays.asList("openid", "email", "address")));
+        oAuth2Request.setSubject("subject");
+
+        Client client = new Client();
+
+        User user = createUser();
+
+        AccessToken accessToken = new AccessToken();
+        accessToken.setId("token-id");
+        accessToken.setToken("token-id");
+        accessToken.setScopes(new HashSet<>(Arrays.asList("openid", "email", "address")));
+
+        Key key = MacSigner.generateKey();
+        JwtParser parser = Jwts.parser().setSigningKey(key);
+        jwtBuilder = Jwts.builder().signWith(SignatureAlgorithm.HS512, key);
+        ((IDTokenServiceImpl) idTokenService).setJwtBuilder(jwtBuilder);
+
+        when(certificateManager.get(anyString())).thenReturn(Maybe.empty());
+
+        TestObserver<String> testObserver = idTokenService.create(oAuth2Request, client, user).test();
+
+        testObserver.assertComplete();
+        testObserver.assertNoErrors();
+        testObserver.assertValue(idToken -> {
+            Jwt jwt = parser.parse(idToken);
+            Map<String, Object> claims = (Map<String, Object>) jwt.getBody();
+            return claims.size() == 4 + 4; // 4 ID Token standard claims
+        });
+
+        verify(certificateManager, times(1)).get(anyString());
+        verify(certificateProvider, never()).sign(anyString());
+    }
+
+    @Test
+    public void shouldCreateIDToken_withUser_scopesRequest_and_claimsRequest() {
+        OAuth2Request oAuth2Request = new OAuth2Request();
+        oAuth2Request.setClientId("client-id");
+        oAuth2Request.setScopes(new HashSet<>(Arrays.asList("openid", "email", "address")));
+        oAuth2Request.setSubject("subject");
+        MultiValueMap<String, String> requestParameters = new LinkedMultiValueMap<>();
+        requestParameters.put("claims", Collections.singletonList("{\"id_token\":{\"name\":{\"essential\":true}}}"));
+        oAuth2Request.setRequestParameters(requestParameters);
+
+        Client client = new Client();
+
+        User user = createUser();
+
+        AccessToken accessToken = new AccessToken();
+        accessToken.setId("token-id");
+        accessToken.setToken("token-id");
+        accessToken.setScopes(new HashSet<>(Arrays.asList("openid", "email", "address")));
+
+        Key key = MacSigner.generateKey();
+        JwtParser parser = Jwts.parser().setSigningKey(key);
+        jwtBuilder = Jwts.builder().signWith(SignatureAlgorithm.HS512, key);
+        ((IDTokenServiceImpl) idTokenService).setJwtBuilder(jwtBuilder);
+
+        when(certificateManager.get(anyString())).thenReturn(Maybe.empty());
+
+        TestObserver<String> testObserver = idTokenService.create(oAuth2Request, client, user).test();
+
+        testObserver.assertComplete();
+        testObserver.assertNoErrors();
+        testObserver.assertValue(idToken -> {
+            Jwt jwt = parser.parse(idToken);
+            Map<String, Object> claims = (Map<String, Object>) jwt.getBody();
+            return claims.size() == 5 + 4   // 4 ID Token standard claims
+                                && claims.containsKey(StandardClaims.NAME)
+                                && claims.containsKey(StandardClaims.EMAIL)
+                                && claims.containsKey(StandardClaims.EMAIL_VERIFIED)
+                                && claims.containsKey(StandardClaims.ADDRESS);
+        });
+
+        verify(certificateManager, times(1)).get(anyString());
+        verify(certificateProvider, never()).sign(anyString());
+    }
+
+    private User createUser() {
+        User user = new User();
+        user.setId("technical-id");
+        Map<String, Object> additionalInformation  = new HashMap<>();
+        additionalInformation.put(StandardClaims.SUB, "user");
+        additionalInformation.put(StandardClaims.NAME, "gravitee user");
+        additionalInformation.put(StandardClaims.FAMILY_NAME, "gravitee");
+        additionalInformation.put(StandardClaims.GIVEN_NAME, "gravitee");
+        additionalInformation.put(StandardClaims.MIDDLE_NAME, "gravitee");
+        additionalInformation.put(StandardClaims.NICKNAME, "gravitee");
+        additionalInformation.put(StandardClaims.PREFERRED_USERNAME, "gravitee");
+        additionalInformation.put(StandardClaims.PROFILE, "gravitee");
+        additionalInformation.put(StandardClaims.PICTURE, "gravitee");
+        additionalInformation.put(StandardClaims.WEBSITE, "gravitee");
+        additionalInformation.put(StandardClaims.GENDER, "gravitee");
+        additionalInformation.put(StandardClaims.BIRTHDATE, "gravitee");
+        additionalInformation.put(StandardClaims.ZONEINFO, "gravitee");
+        additionalInformation.put(StandardClaims.LOCALE, "gravitee");
+        additionalInformation.put(StandardClaims.UPDATED_AT, "gravitee");
+        additionalInformation.put(StandardClaims.EMAIL, "gravitee");
+        additionalInformation.put(StandardClaims.EMAIL_VERIFIED, "gravitee");
+        additionalInformation.put(StandardClaims.ADDRESS, "gravitee");
+        additionalInformation.put(StandardClaims.PHONE_NUMBER, "gravitee");
+        additionalInformation.put(StandardClaims.PHONE_NUMBER_VERIFIED, "gravitee");
+        user.setAdditionalInformation(additionalInformation);
+
+        return user;
+    }
 }
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/src/test/java/io/gravitee/am/gateway/handler/vertx/endpoint/UserInfoEndpointHandlerTest.java b/gravitee-am-gateway/gravitee-am-gateway-handler/src/test/java/io/gravitee/am/gateway/handler/vertx/endpoint/UserInfoEndpointHandlerTest.java
index 3ffb0f986c..7b15ea0670 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/src/test/java/io/gravitee/am/gateway/handler/vertx/endpoint/UserInfoEndpointHandlerTest.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/src/test/java/io/gravitee/am/gateway/handler/vertx/endpoint/UserInfoEndpointHandlerTest.java
@@ -15,6 +15,7 @@
  */
 package io.gravitee.am.gateway.handler.vertx.endpoint;
 
+import io.gravitee.am.common.oidc.StandardClaims;
 import io.gravitee.am.gateway.handler.oauth2.token.AccessToken;
 import io.gravitee.am.gateway.handler.oauth2.token.TokenService;
 import io.gravitee.am.gateway.handler.oauth2.token.impl.DefaultAccessToken;
@@ -194,12 +195,82 @@ public class UserInfoEndpointHandlerTest extends RxWebTestBase {
         ((DefaultAccessToken) accessToken).setScope("openid");
         ((DefaultAccessToken) accessToken).setRequestedParameters(Collections.singletonMap("claims", "{\"userinfo\":{\"name\":{\"essential\":true}}}"));
 
-        User user = new User();
-        Map<String, Object> additionalInformation  = new HashMap<>();
-        additionalInformation.put("sub", "user");
-        additionalInformation.put("name", "gravitee user");
-        additionalInformation.put("family_name", "gravitee");
-        user.setAdditionalInformation(additionalInformation);
+        User user = createUser();
+
+        when(tokenService.getAccessToken(anyString())).thenReturn(Maybe.just(accessToken));
+        when(userService.findById(anyString())).thenReturn(Maybe.just(user));
+
+        testRequest(
+                HttpMethod.GET,
+                "/userinfo",
+                req -> req.putHeader(HttpHeaders.AUTHORIZATION, "Bearer test-token"),
+                resp -> resp.bodyHandler(body -> {
+                    final Map<String, Object> claims = Json.decodeValue(body.toString(), Map.class);
+                    assertNotNull(claims);
+                    assertEquals(2, claims.size());
+                    assertFalse(claims.containsKey(StandardClaims.FAMILY_NAME));
+                }),
+                HttpStatusCode.OK_200, "OK", null);
+    }
+
+    @Test
+    public void shouldInvokeUserEndpoint_scopesRequest() throws Exception {
+        AccessToken accessToken = new DefaultAccessToken("token");
+        ((DefaultAccessToken) accessToken).setSubject("user");
+        ((DefaultAccessToken) accessToken).setExpiresIn(100);
+        ((DefaultAccessToken) accessToken).setScope("openid profile");
+
+        User user = createUser();
+
+        when(tokenService.getAccessToken(anyString())).thenReturn(Maybe.just(accessToken));
+        when(userService.findById(anyString())).thenReturn(Maybe.just(user));
+
+        testRequest(
+                HttpMethod.GET,
+                "/userinfo",
+                req -> req.putHeader(HttpHeaders.AUTHORIZATION, "Bearer test-token"),
+                resp -> resp.bodyHandler(body -> {
+                    final Map<String, Object> claims = Json.decodeValue(body.toString(), Map.class);
+                    assertNotNull(claims);
+                    assertEquals(15, claims.size());
+                }),
+                HttpStatusCode.OK_200, "OK", null);
+    }
+
+    @Test
+    public void shouldInvokeUserEndpoint_scopesRequest_email() throws Exception {
+        AccessToken accessToken = new DefaultAccessToken("token");
+        ((DefaultAccessToken) accessToken).setSubject("user");
+        ((DefaultAccessToken) accessToken).setExpiresIn(100);
+        ((DefaultAccessToken) accessToken).setScope("openid email");
+
+        User user = createUser();
+
+        when(tokenService.getAccessToken(anyString())).thenReturn(Maybe.just(accessToken));
+        when(userService.findById(anyString())).thenReturn(Maybe.just(user));
+
+        testRequest(
+                HttpMethod.GET,
+                "/userinfo",
+                req -> req.putHeader(HttpHeaders.AUTHORIZATION, "Bearer test-token"),
+                resp -> resp.bodyHandler(body -> {
+                    final Map<String, Object> claims = Json.decodeValue(body.toString(), Map.class);
+                    assertNotNull(claims);
+                    assertEquals(3, claims.size());
+                    assertTrue(claims.containsKey(StandardClaims.EMAIL));
+                    assertTrue(claims.containsKey(StandardClaims.EMAIL_VERIFIED));
+                }),
+                HttpStatusCode.OK_200, "OK", null);
+    }
+
+    @Test
+    public void shouldInvokeUserEndpoint_scopesRequest_email_address() throws Exception {
+        AccessToken accessToken = new DefaultAccessToken("token");
+        ((DefaultAccessToken) accessToken).setSubject("user");
+        ((DefaultAccessToken) accessToken).setExpiresIn(100);
+        ((DefaultAccessToken) accessToken).setScope("openid email address");
+
+        User user = createUser();
 
         when(tokenService.getAccessToken(anyString())).thenReturn(Maybe.just(accessToken));
         when(userService.findById(anyString())).thenReturn(Maybe.just(user));
@@ -208,14 +279,71 @@ public class UserInfoEndpointHandlerTest extends RxWebTestBase {
                 HttpMethod.GET,
                 "/userinfo",
                 req -> req.putHeader(HttpHeaders.AUTHORIZATION, "Bearer test-token"),
-                resp -> {
-                    resp.bodyHandler(body -> {
-                        final Map<String, Object> claims = Json.decodeValue(body.toString(), Map.class);
-                        assertNotNull(claims);
-                        assertEquals(2, claims.size());
-                        assertFalse(claims.containsKey("family_name"));
-                    });
-                },
+                resp -> resp.bodyHandler(body -> {
+                    final Map<String, Object> claims = Json.decodeValue(body.toString(), Map.class);
+                    assertNotNull(claims);
+                    assertEquals(4, claims.size());
+                    assertTrue(claims.containsKey(StandardClaims.ADDRESS));
+                    assertTrue(claims.containsKey(StandardClaims.EMAIL));
+                    assertTrue(claims.containsKey(StandardClaims.EMAIL_VERIFIED));
+                }),
                 HttpStatusCode.OK_200, "OK", null);
     }
+
+    @Test
+    public void shouldInvokeUserEndpoint_scopesRequest_and_claimsRequest() throws Exception {
+        AccessToken accessToken = new DefaultAccessToken("token");
+        ((DefaultAccessToken) accessToken).setSubject("user");
+        ((DefaultAccessToken) accessToken).setExpiresIn(100);
+        ((DefaultAccessToken) accessToken).setScope("openid email address");
+        ((DefaultAccessToken) accessToken).setRequestedParameters(Collections.singletonMap("claims", "{\"userinfo\":{\"name\":{\"essential\":true}}}"));
+
+        User user = createUser();
+
+        when(tokenService.getAccessToken(anyString())).thenReturn(Maybe.just(accessToken));
+        when(userService.findById(anyString())).thenReturn(Maybe.just(user));
+
+        testRequest(
+                HttpMethod.GET,
+                "/userinfo",
+                req -> req.putHeader(HttpHeaders.AUTHORIZATION, "Bearer test-token"),
+                resp -> resp.bodyHandler(body -> {
+                    final Map<String, Object> claims = Json.decodeValue(body.toString(), Map.class);
+                    assertNotNull(claims);
+                    assertEquals(5, claims.size());
+                    assertTrue(claims.containsKey(StandardClaims.NAME));
+                    assertTrue(claims.containsKey(StandardClaims.ADDRESS));
+                    assertTrue(claims.containsKey(StandardClaims.EMAIL));
+                    assertTrue(claims.containsKey(StandardClaims.EMAIL_VERIFIED));
+                }),
+                HttpStatusCode.OK_200, "OK", null);
+    }
+
+    private User createUser() {
+        User user = new User();
+        Map<String, Object> additionalInformation  = new HashMap<>();
+        additionalInformation.put(StandardClaims.SUB, "user");
+        additionalInformation.put(StandardClaims.NAME, "gravitee user");
+        additionalInformation.put(StandardClaims.FAMILY_NAME, "gravitee");
+        additionalInformation.put(StandardClaims.GIVEN_NAME, "gravitee");
+        additionalInformation.put(StandardClaims.MIDDLE_NAME, "gravitee");
+        additionalInformation.put(StandardClaims.NICKNAME, "gravitee");
+        additionalInformation.put(StandardClaims.PREFERRED_USERNAME, "gravitee");
+        additionalInformation.put(StandardClaims.PROFILE, "gravitee");
+        additionalInformation.put(StandardClaims.PICTURE, "gravitee");
+        additionalInformation.put(StandardClaims.WEBSITE, "gravitee");
+        additionalInformation.put(StandardClaims.GENDER, "gravitee");
+        additionalInformation.put(StandardClaims.BIRTHDATE, "gravitee");
+        additionalInformation.put(StandardClaims.ZONEINFO, "gravitee");
+        additionalInformation.put(StandardClaims.LOCALE, "gravitee");
+        additionalInformation.put(StandardClaims.UPDATED_AT, "gravitee");
+        additionalInformation.put(StandardClaims.EMAIL, "gravitee");
+        additionalInformation.put(StandardClaims.EMAIL_VERIFIED, "gravitee");
+        additionalInformation.put(StandardClaims.ADDRESS, "gravitee");
+        additionalInformation.put(StandardClaims.PHONE_NUMBER, "gravitee");
+        additionalInformation.put(StandardClaims.PHONE_NUMBER_VERIFIED, "gravitee");
+        user.setAdditionalInformation(additionalInformation);
+
+        return user;
+    }
 }