Browse Source

Add new token_type setting to JWT realm (#91536)

This PR introduces a new optional token_type setting to the JWT realm.
Two possible values are id_token and access_token with id_token being
the default, whose behaviour is what current code implements. The
support for access_token will be added in future PRs.

This PR does not tighten validation behaviours for ID tokens because
tests would break without access_token support in place.

The token_type information is also exposed in user metadata under the
key jwt_token_type.
Yang Wang 2 years ago
parent
commit
7c7c2679ed

+ 5 - 0
docs/changelog/91536.yaml

@@ -0,0 +1,5 @@
+pr: 91536
+summary: Add new `token_type` setting to JWT realm
+area: Authentication
+type: enhancement
+issues: []

+ 42 - 2
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/jwt/JwtRealmSettings.java

@@ -8,6 +8,7 @@ package org.elasticsearch.xpack.core.security.authc.jwt;
 
 import org.elasticsearch.common.settings.SecureString;
 import org.elasticsearch.common.settings.Setting;
+import org.elasticsearch.core.Strings;
 import org.elasticsearch.core.TimeValue;
 import org.elasticsearch.xpack.core.security.authc.RealmSettings;
 import org.elasticsearch.xpack.core.security.authc.support.ClaimSetting;
@@ -16,6 +17,7 @@ import org.elasticsearch.xpack.core.ssl.SSLConfigurationSettings;
 
 import java.util.Collection;
 import java.util.Collections;
+import java.util.EnumSet;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
@@ -77,6 +79,38 @@ public class JwtRealmSettings {
         }
     }
 
+    public enum TokenType {
+        ID_TOKEN("id_token"),
+        ACCESS_TOKEN("access_token");
+
+        private final String value;
+
+        TokenType(String value) {
+            this.value = value;
+        }
+
+        public String value() {
+            return value;
+        }
+
+        public static TokenType parse(String value, String settingKey) {
+            return EnumSet.allOf(TokenType.class)
+                .stream()
+                .filter(type -> type.value.equalsIgnoreCase(value))
+                .findFirst()
+                .orElseThrow(
+                    () -> new IllegalArgumentException(
+                        Strings.format(
+                            "Invalid value [%s] for [%s], allowed values are [%s]",
+                            value,
+                            settingKey,
+                            Stream.of(values()).map(TokenType::value).collect(Collectors.joining(","))
+                        )
+                    )
+                );
+        }
+    }
+
     // Default values and min/max constraints
 
     private static final TimeValue DEFAULT_ALLOWED_CLOCK_SKEW = TimeValue.timeValueSeconds(60);
@@ -111,9 +145,9 @@ public class JwtRealmSettings {
      * @return All non-secure settings.
      */
     private static Set<Setting.AffixSetting<?>> getNonSecureSettings() {
-        final Set<Setting.AffixSetting<?>> set = new HashSet<>();
         // Standard realm settings: order, enabled
-        set.addAll(RealmSettings.getStandardSettings(TYPE));
+        final Set<Setting.AffixSetting<?>> set = new HashSet<>(RealmSettings.getStandardSettings(TYPE));
+        set.add(TOKEN_TYPE);
         // JWT Issuer settings
         set.addAll(List.of(ALLOWED_ISSUER, ALLOWED_SIGNATURE_ALGORITHMS, ALLOWED_CLOCK_SKEW, PKC_JWKSET_PATH));
         // JWT Audience settings
@@ -163,6 +197,12 @@ public class JwtRealmSettings {
         return new HashSet<>(List.of(HMAC_JWKSET, HMAC_KEY, CLIENT_AUTHENTICATION_SHARED_SECRET));
     }
 
+    public static final Setting.AffixSetting<TokenType> TOKEN_TYPE = Setting.affixKeySetting(
+        RealmSettings.realmSettingPrefix(TYPE),
+        "token_type",
+        key -> new Setting<>(key, TokenType.ID_TOKEN.value(), value -> TokenType.parse(value, key), Setting.Property.NodeScope)
+    );
+
     // JWT issuer settings
     public static final Setting.AffixSetting<String> ALLOWED_ISSUER = Setting.affixKeySetting(
         RealmSettings.realmSettingPrefix(TYPE),

+ 12 - 0
x-pack/plugin/security/qa/jwt-realm/build.gradle

@@ -1,4 +1,5 @@
 import org.elasticsearch.gradle.Version
+import org.elasticsearch.gradle.internal.info.BuildParams
 
 apply plugin: 'elasticsearch.internal-java-rest-test'
 
@@ -9,6 +10,8 @@ dependencies {
   javaRestTestImplementation project(":client:rest")
 }
 
+boolean explicitIdTokenType = (new Random(Long.parseUnsignedLong(BuildParams.testSeed.tokenize(':').get(0), 16))).nextBoolean()
+
 testClusters.matching { it.name == 'javaRestTest' }.configureEach {
   testDistribution = 'DEFAULT'
 
@@ -43,6 +46,9 @@ testClusters.matching { it.name == 'javaRestTest' }.configureEach {
 
   // These realm settings are generated by JwtRealmGenerateTests
   setting 'xpack.security.authc.realms.jwt.jwt1.order', '1'
+  if (explicitIdTokenType) {
+    setting 'xpack.security.authc.realms.jwt.jwt1.token_type', 'id_token'
+  }
   setting 'xpack.security.authc.realms.jwt.jwt1.allowed_issuer', 'https://issuer.example.com/'
   setting 'xpack.security.authc.realms.jwt.jwt1.allowed_audiences', 'https://audience.example.com/'
   setting 'xpack.security.authc.realms.jwt.jwt1.claims.principal', 'sub'
@@ -58,6 +64,9 @@ testClusters.matching { it.name == 'javaRestTest' }.configureEach {
   setting 'xpack.security.authc.realms.native.lookup_native.order', '2'
 
   setting 'xpack.security.authc.realms.jwt.jwt2.order', '3'
+  if (explicitIdTokenType) {
+    setting 'xpack.security.authc.realms.jwt.jwt2.token_type', 'id_token'
+  }
   setting 'xpack.security.authc.realms.jwt.jwt2.allowed_issuer', 'my-issuer'
   setting 'xpack.security.authc.realms.jwt.jwt2.allowed_audiences', 'es01,es02,es03'
   setting 'xpack.security.authc.realms.jwt.jwt2.allowed_signature_algorithms', 'HS256,HS384'
@@ -72,6 +81,9 @@ testClusters.matching { it.name == 'javaRestTest' }.configureEach {
   setting 'xpack.security.authc.realms.pki.pki_realm.order', '4'
 
   setting 'xpack.security.authc.realms.jwt.jwt3.order', '5'
+  if (explicitIdTokenType) {
+    setting 'xpack.security.authc.realms.jwt.jwt3.token_type', 'id_token'
+  }
   setting 'xpack.security.authc.realms.jwt.jwt3.allowed_issuer', 'jwt3-issuer'
   setting 'xpack.security.authc.realms.jwt.jwt3.allowed_audiences', '[jwt3-audience]'
   setting 'xpack.security.authc.realms.jwt.jwt3.allowed_signature_algorithms', '[HS384, HS512]'

+ 2 - 0
x-pack/plugin/security/qa/jwt-realm/src/javaRestTest/java/org/elasticsearch/xpack/security/authc/jwt/JwtRestIT.java

@@ -209,6 +209,7 @@ public class JwtRestIT extends ESRestTestCase {
                 hasEntry(User.Fields.REALM_NAME.getPreferredName(), "jwt1")
             );
             assertThat(description, assertList(response, User.Fields.ROLES), Matchers.containsInAnyOrder(roles.toArray(String[]::new)));
+            assertThat(description, assertMap(response, User.Fields.METADATA), hasEntry("jwt_token_type", "id_token"));
 
             // The user has no real role (we never define them) so everything they try to do will be FORBIDDEN
             final ResponseException exception = expectThrows(
@@ -392,6 +393,7 @@ public class JwtRestIT extends ESRestTestCase {
         assertThat(assertMap(response, User.Fields.METADATA), hasEntry("jwt_claim_sub", principal));
         assertThat(assertMap(response, User.Fields.METADATA), hasEntry("jwt_claim_aud", List.of("jwt3-audience")));
         assertThat(assertMap(response, User.Fields.METADATA), hasEntry("jwt_claim_iss", "jwt3-issuer"));
+        assertThat(assertMap(response, User.Fields.METADATA), hasEntry("jwt_token_type", "id_token"));
     }
 
     public void testAuthenticateToOtherRealmsInChain() throws IOException, URISyntaxException {

+ 33 - 13
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwtAuthenticator.java

@@ -36,6 +36,7 @@ public class JwtAuthenticator implements Releasable {
     private final RealmConfig realmConfig;
     private final List<JwtFieldValidator> jwtFieldValidators;
     private final JwtSignatureValidator jwtSignatureValidator;
+    private final JwtRealmSettings.TokenType tokenType;
 
     public JwtAuthenticator(
         final RealmConfig realmConfig,
@@ -43,19 +44,12 @@ public class JwtAuthenticator implements Releasable {
         final JwtSignatureValidator.PkcJwkSetReloadNotifier reloadNotifier
     ) {
         this.realmConfig = realmConfig;
-        final TimeValue allowedClockSkew = realmConfig.getSetting(JwtRealmSettings.ALLOWED_CLOCK_SKEW);
-        final Clock clock = Clock.systemUTC();
-        this.jwtFieldValidators = List.of(
-            JwtTypeValidator.INSTANCE,
-            new JwtStringClaimValidator("iss", List.of(realmConfig.getSetting(JwtRealmSettings.ALLOWED_ISSUER)), true),
-            new JwtStringClaimValidator("aud", realmConfig.getSetting(JwtRealmSettings.ALLOWED_AUDIENCES), false),
-            new JwtAlgorithmValidator(realmConfig.getSetting(JwtRealmSettings.ALLOWED_SIGNATURE_ALGORITHMS)),
-            new JwtDateClaimValidator(clock, "iat", allowedClockSkew, JwtDateClaimValidator.Relationship.BEFORE_NOW, false),
-            new JwtDateClaimValidator(clock, "exp", allowedClockSkew, JwtDateClaimValidator.Relationship.AFTER_NOW, false),
-            new JwtDateClaimValidator(clock, "nbf", allowedClockSkew, JwtDateClaimValidator.Relationship.BEFORE_NOW, true),
-            new JwtDateClaimValidator(clock, "auth_time", allowedClockSkew, JwtDateClaimValidator.Relationship.BEFORE_NOW, true)
-        );
-
+        this.tokenType = realmConfig.getSetting(JwtRealmSettings.TOKEN_TYPE);
+        if (tokenType == JwtRealmSettings.TokenType.ID_TOKEN) {
+            this.jwtFieldValidators = configureFieldValidatorsForIdToken(realmConfig);
+        } else {
+            this.jwtFieldValidators = configureFieldValidatorsForAccessToken(realmConfig);
+        }
         this.jwtSignatureValidator = new JwtSignatureValidator.DelegatingJwtSignatureValidator(realmConfig, sslService, reloadNotifier);
     }
 
@@ -114,9 +108,35 @@ public class JwtAuthenticator implements Releasable {
         jwtSignatureValidator.close();
     }
 
+    public JwtRealmSettings.TokenType getTokenType() {
+        return tokenType;
+    }
+
     // Package private for testing
     JwtSignatureValidator.DelegatingJwtSignatureValidator getJwtSignatureValidator() {
         assert jwtSignatureValidator instanceof JwtSignatureValidator.DelegatingJwtSignatureValidator;
         return (JwtSignatureValidator.DelegatingJwtSignatureValidator) jwtSignatureValidator;
     }
+
+    private static List<JwtFieldValidator> configureFieldValidatorsForIdToken(RealmConfig realmConfig) {
+        assert realmConfig.getSetting(JwtRealmSettings.TOKEN_TYPE) == JwtRealmSettings.TokenType.ID_TOKEN;
+        final TimeValue allowedClockSkew = realmConfig.getSetting(JwtRealmSettings.ALLOWED_CLOCK_SKEW);
+        final Clock clock = Clock.systemUTC();
+        return List.of(
+            JwtTypeValidator.INSTANCE,
+            // TODO: mandate "sub" claim once access token support is in place
+            new JwtStringClaimValidator("iss", List.of(realmConfig.getSetting(JwtRealmSettings.ALLOWED_ISSUER)), true),
+            new JwtStringClaimValidator("aud", realmConfig.getSetting(JwtRealmSettings.ALLOWED_AUDIENCES), false),
+            new JwtAlgorithmValidator(realmConfig.getSetting(JwtRealmSettings.ALLOWED_SIGNATURE_ALGORITHMS)),
+            new JwtDateClaimValidator(clock, "iat", allowedClockSkew, JwtDateClaimValidator.Relationship.BEFORE_NOW, false),
+            new JwtDateClaimValidator(clock, "exp", allowedClockSkew, JwtDateClaimValidator.Relationship.AFTER_NOW, false),
+            new JwtDateClaimValidator(clock, "nbf", allowedClockSkew, JwtDateClaimValidator.Relationship.BEFORE_NOW, true),
+            new JwtDateClaimValidator(clock, "auth_time", allowedClockSkew, JwtDateClaimValidator.Relationship.BEFORE_NOW, true)
+        );
+    }
+
+    private static List<JwtFieldValidator> configureFieldValidatorsForAccessToken(RealmConfig realmConfig) {
+        assert realmConfig.getSetting(JwtRealmSettings.TOKEN_TYPE) == JwtRealmSettings.TokenType.ACCESS_TOKEN;
+        throw new UnsupportedOperationException("NYI");
+    }
 }

+ 38 - 9
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealm.java

@@ -34,8 +34,10 @@ import org.elasticsearch.xpack.core.ssl.SSLService;
 import org.elasticsearch.xpack.security.authc.support.ClaimParser;
 import org.elasticsearch.xpack.security.authc.support.DelegatedAuthorizationSupport;
 
+import java.util.Collection;
 import java.util.Collections;
 import java.util.Date;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -335,15 +337,7 @@ public class JwtRealm extends Realm implements CachingRealm, Releasable {
         }
 
         // User metadata: If enabled, extract metadata from JWT claims set. Use it in UserRoleMapper.UserData and User constructors.
-        final Map<String, Object> userMetadata;
-        try {
-            userMetadata = populateUserMetadata ? JwtUtil.toUserMetadata(claimsSet) : Map.of();
-        } catch (Exception e) {
-            final String msg = "Realm [" + name() + "] parse metadata failed for principal=[" + principal + "].";
-            logger.debug(msg, e);
-            listener.onResponse(AuthenticationResult.unsuccessful(msg, e));
-            return;
-        }
+        final Map<String, Object> userMetadata = buildUserMetadata(claimsSet);
 
         // Role resolution: Handle role mapping in JWT Realm.
         final List<String> groups = claimParserGroups.getClaimValues(claimsSet);
@@ -388,6 +382,41 @@ public class JwtRealm extends Realm implements CachingRealm, Releasable {
         return jwtCache != null && jwtCacheHelper != null;
     }
 
+    /**
+     * Format and filter JWT contents as user metadata.
+     * @param claimsSet Claims are supported. Claim keys are prefixed by "jwt_claim_".
+     * @return Map of formatted and filtered values to be used as user metadata.
+     */
+    private Map<String, Object> buildUserMetadata(JWTClaimsSet claimsSet) {
+        final HashMap<String, Object> metadata = new HashMap<>();
+        metadata.put("jwt_token_type", jwtAuthenticator.getTokenType().value());
+        if (populateUserMetadata) {
+            claimsSet.getClaims()
+                .entrySet()
+                .stream()
+                .filter(entry -> isAllowedTypeForClaim(entry.getValue()))
+                .forEach(entry -> metadata.put("jwt_claim_" + entry.getKey(), entry.getValue()));
+        }
+        return Map.copyOf(metadata);
+    }
+
+    /**
+     * JWTClaimsSet values are only allowed to be String, Boolean, Number, or Collection.
+     * Collections are only allowed to contain String, Boolean, or Number.
+     * Collections recursion is not allowed.
+     * Maps are not allowed.
+     * Nulls are not allowed.
+     * @param value Claim value object.
+     * @return True if the claim value is allowed, otherwise false.
+     */
+    private static boolean isAllowedTypeForClaim(final Object value) {
+        return (value instanceof String
+            || value instanceof Boolean
+            || value instanceof Number
+            || (value instanceof Collection
+                && ((Collection<?>) value).stream().allMatch(e -> e instanceof String || e instanceof Boolean || e instanceof Number)));
+    }
+
     // Cached authenticated users, and adjusted JWT expiration date (=exp+skew) for checking if the JWT expired before the cache entry
     record ExpiringUser(User user, Date exp) {
         ExpiringUser {

+ 0 - 37
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwtUtil.java

@@ -10,7 +10,6 @@ package org.elasticsearch.xpack.security.authc.jwt;
 import com.nimbusds.jose.jwk.JWK;
 import com.nimbusds.jose.jwk.JWKSet;
 import com.nimbusds.jose.util.JSONObjectUtils;
-import com.nimbusds.jwt.JWTClaimsSet;
 
 import org.apache.http.HttpEntity;
 import org.apache.http.HttpResponse;
@@ -55,9 +54,6 @@ import java.security.MessageDigest;
 import java.security.PrivilegedAction;
 import java.security.PrivilegedActionException;
 import java.security.PrivilegedExceptionAction;
-import java.util.Collection;
-import java.util.Map;
-import java.util.stream.Collectors;
 
 import javax.net.ssl.HostnameVerifier;
 import javax.net.ssl.SSLContext;
@@ -337,39 +333,6 @@ public class JwtUtil {
         return new SecureString(sb.toString().toCharArray());
     }
 
-    /**
-     * Format and filter JWT contents as user metadata.
-     *   JWSHeader: Header are not support.
-     *   JWTClaimsSet: Claims are supported. Claim keys are prefixed by "jwt_claim_".
-     *   Base64URL: Signature is not supported.
-     * @return Map of formatted and filtered values to be used as user metadata.
-     */
-    // Values will be filtered by type using isAllowedTypeForClaim().
-    public static Map<String, Object> toUserMetadata(JWTClaimsSet claimsSet) {
-        return claimsSet.getClaims()
-            .entrySet()
-            .stream()
-            .filter(entry -> JwtUtil.isAllowedTypeForClaim(entry.getValue()))
-            .collect(Collectors.toUnmodifiableMap(entry -> "jwt_claim_" + entry.getKey(), Map.Entry::getValue));
-    }
-
-    /**
-     * JWTClaimsSet values are only allowed to be String, Boolean, Number, or Collection.
-     * Collections are only allowed to contain String, Boolean, or Number.
-     * Collections recursion is not allowed.
-     * Maps are not allowed.
-     * Nulls are not allowed.
-     * @param value Claim value object.
-     * @return True if the claim value is allowed, otherwise false.
-     */
-    static boolean isAllowedTypeForClaim(final Object value) {
-        return (value instanceof String
-            || value instanceof Boolean
-            || value instanceof Number
-            || (value instanceof Collection
-                && ((Collection<?>) value).stream().allMatch(e -> e instanceof String || e instanceof Boolean || e instanceof Number)));
-    }
-
     public static byte[] sha256(final CharSequence charSequence) {
         final MessageDigest messageDigest = MessageDigests.sha256();
         messageDigest.update(charSequence.toString().getBytes(StandardCharsets.UTF_8));

+ 6 - 3
x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtAuthenticatorTests.java

@@ -67,14 +67,17 @@ public class JwtAuthenticatorTests extends ESTestCase {
         final RealmConfig.RealmIdentifier realmIdentifier = new RealmConfig.RealmIdentifier(JwtRealmSettings.TYPE, realmName);
         final MockSecureSettings secureSettings = new MockSecureSettings();
         secureSettings.setString(RealmSettings.getFullSettingKey(realmName, JwtRealmSettings.HMAC_KEY), randomAlphaOfLength(40));
-        final Settings settings = Settings.builder()
+        final Settings.Builder builder = Settings.builder()
             .put(RealmSettings.getFullSettingKey(realmName, JwtRealmSettings.ALLOWED_SIGNATURE_ALGORITHMS), allowedAlgorithm)
             .put(RealmSettings.getFullSettingKey(realmName, JwtRealmSettings.ALLOWED_ISSUER), allowedIssuer)
             .put(RealmSettings.getFullSettingKey(realmName, JwtRealmSettings.ALLOWED_AUDIENCES), randomAlphaOfLength(7))
             .put(RealmSettings.getFullSettingKey(realmIdentifier, RealmSettings.ORDER_SETTING), randomIntBetween(0, 99))
             .put("path.home", randomAlphaOfLength(10))
-            .setSecureSettings(secureSettings)
-            .build();
+            .setSecureSettings(secureSettings);
+        if (randomBoolean()) {
+            builder.put(RealmSettings.getFullSettingKey(realmName, JwtRealmSettings.TOKEN_TYPE), "id_token");
+        }
+        final Settings settings = builder.build();
 
         final RealmConfig realmConfig = new RealmConfig(
             realmIdentifier,

+ 32 - 0
x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealmSettingsTests.java

@@ -22,7 +22,9 @@ import java.util.List;
 import java.util.Locale;
 
 import static org.elasticsearch.common.Strings.capitalize;
+import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
 
 /**
  * JWT realm settings unit tests. These are low-level tests against ES settings parsers.
@@ -396,4 +398,34 @@ public class JwtRealmSettingsTests extends JwtTestCase {
             }
         }
     }
+
+    public void testTokenTypeSetting() {
+        final String realmName = randomAlphaOfLengthBetween(3, 8);
+        final String fullSettingKey = RealmSettings.getFullSettingKey(realmName, JwtRealmSettings.TOKEN_TYPE);
+
+        // Default is id_token
+        assertThat(
+            buildRealmConfig(JwtRealmSettings.TYPE, realmName, Settings.EMPTY, randomInt()).getSetting(JwtRealmSettings.TOKEN_TYPE),
+            is(JwtRealmSettings.TokenType.ID_TOKEN)
+        );
+
+        // Valid values
+        final JwtRealmSettings.TokenType expectedTokenType = randomFrom(JwtRealmSettings.TokenType.values());
+        final Settings settings = Settings.builder()
+            .put(fullSettingKey, randomBoolean() ? expectedTokenType.value() : expectedTokenType.value().toUpperCase(Locale.ROOT))
+            .build();
+        assertThat(
+            buildRealmConfig(JwtRealmSettings.TYPE, realmName, settings, randomInt()).getSetting(JwtRealmSettings.TOKEN_TYPE),
+            is(expectedTokenType)
+        );
+
+        // Anything else is invalid
+        final Settings invalidSettings = Settings.builder().put(fullSettingKey, randomAlphaOfLengthBetween(3, 20)).build();
+
+        final IllegalArgumentException e = expectThrows(
+            IllegalArgumentException.class,
+            () -> buildRealmConfig(JwtRealmSettings.TYPE, realmName, invalidSettings, randomInt()).getSetting(JwtRealmSettings.TOKEN_TYPE)
+        );
+        assertThat(e.getMessage(), containsString("Invalid value"));
+    }
 }

+ 6 - 2
x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealmTestCase.java

@@ -60,11 +60,14 @@ import static org.hamcrest.Matchers.anEmptyMap;
 import static org.hamcrest.Matchers.empty;
 import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.greaterThanOrEqualTo;
+import static org.hamcrest.Matchers.hasEntry;
+import static org.hamcrest.Matchers.hasKey;
 import static org.hamcrest.Matchers.is;
 import static org.hamcrest.Matchers.isA;
 import static org.hamcrest.Matchers.not;
 import static org.hamcrest.Matchers.notNullValue;
 import static org.hamcrest.Matchers.nullValue;
+import static org.hamcrest.Matchers.startsWith;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
@@ -568,9 +571,10 @@ public abstract class JwtRealmTestCase extends JwtTestCase {
                 if (jwtRealm.delegatedAuthorizationSupport.hasDelegation()) {
                     assertThat(user.metadata(), is(equalTo(authenticatedUser.metadata()))); // delegated authz returns user's metadata
                 } else if (JwtRealmInspector.shouldPopulateUserMetadata(jwtRealm)) {
-                    assertThat(authenticatedUser.metadata(), is(not(anEmptyMap()))); // role mapping with flag true returns non-empty
+                    assertThat(authenticatedUser.metadata(), hasEntry("jwt_token_type", "id_token"));
+                    assertThat(authenticatedUser.metadata(), hasKey(startsWith("jwt_claim_")));
                 } else {
-                    assertThat(authenticatedUser.metadata(), is(anEmptyMap())); // role mapping with flag false returns empty
+                    assertThat(authenticatedUser.metadata(), equalTo(Map.of("jwt_token_type", "id_token")));
                 }
             } catch (Throwable t) {
                 realmFailureExceptions.forEach(t::addSuppressed); // all previous realm exceptions