-
Notifications
You must be signed in to change notification settings - Fork 24.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Map only specific type of OIDC Claims (#58524)
This commit changes our behavior in 2 ways: - When mapping claims to user properties ( principal, email, groups, name), we only handle string and array of string type. Previously we would fail to recognize an array of other types and that would cause failures when trying to cast to String. - When adding unmapped claims to the user metadata, we only handle string, number, boolean and arrays of these. Previously, we would fail to recognize an array of other types and that would cause failures when attempting to process role mappings. For user properties that are inherently single valued, like principal(username) we continue to support arrays of strings where we select the first one in case this is being depended on by users but we plan on removing this leniency in the next major release.
- Loading branch information
Showing
3 changed files
with
113 additions
and
44 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,6 +11,7 @@ | |
import com.nimbusds.openid.connect.sdk.Nonce; | ||
import org.elasticsearch.action.ActionListener; | ||
import org.elasticsearch.action.support.PlainActionFuture; | ||
import org.elasticsearch.common.Nullable; | ||
import org.elasticsearch.common.settings.Settings; | ||
import org.elasticsearch.common.util.concurrent.ThreadContext; | ||
import org.elasticsearch.env.Environment; | ||
|
@@ -85,7 +86,7 @@ public void testAuthentication() throws Exception { | |
|
||
final boolean notPopulateMetadata = randomBoolean(); | ||
final String authenticatingRealm = randomBoolean() ? REALM_NAME : null; | ||
AuthenticationResult result = authenticateWithOidc(principal, roleMapper, notPopulateMetadata, false, authenticatingRealm); | ||
AuthenticationResult result = authenticateWithOidc(principal, roleMapper, notPopulateMetadata, false, authenticatingRealm, null); | ||
assertThat(result, notNullValue()); | ||
assertThat(result.getStatus(), equalTo(AuthenticationResult.Status.SUCCESS)); | ||
assertThat(result.getUser().principal(), equalTo(principal)); | ||
|
@@ -104,6 +105,68 @@ public void testAuthentication() throws Exception { | |
} | ||
} | ||
|
||
public void testClaimPropertyMapping() throws Exception { | ||
final UserRoleMapper roleMapper = mock(UserRoleMapper.class); | ||
final String principal = randomAlphaOfLength(12); | ||
AtomicReference<UserRoleMapper.UserData> userData = new AtomicReference<>(); | ||
doAnswer(invocation -> { | ||
assert invocation.getArguments().length == 2; | ||
userData.set((UserRoleMapper.UserData) invocation.getArguments()[0]); | ||
ActionListener<Set<String>> listener = (ActionListener<Set<String>>) invocation.getArguments()[1]; | ||
listener.onResponse(new HashSet<>(Arrays.asList("kibana_user", "role1"))); | ||
return null; | ||
}).when(roleMapper).resolveRoles(any(UserRoleMapper.UserData.class), any(ActionListener.class)); | ||
Map<String, Object> claimsWithObject = Map.of( | ||
"groups", List.of(Map.of("key1", List.of("value1", "value2")), Map.of("key2", List.of("value1", "value2"))) | ||
); | ||
Map<String, Object> claimsWithNumber = Map.of( | ||
"groups", List.of(2, "value2")); | ||
Exception e = expectThrows(Exception.class, () -> authenticateWithOidc(principal, roleMapper, false, false, | ||
REALM_NAME, claimsWithObject)); | ||
Exception e2 = expectThrows(Exception.class, () -> authenticateWithOidc(principal, roleMapper, false, false, | ||
REALM_NAME, claimsWithNumber)); | ||
assertThat(e.getCause().getMessage(), containsString("expects a claim with String or a String Array value")); | ||
assertThat(e2.getCause().getMessage(), containsString("expects a claim with String or a String Array value")); | ||
} | ||
|
||
public void testClaimMetadataMapping() throws Exception { | ||
final UserRoleMapper roleMapper = mock(UserRoleMapper.class); | ||
final String principal = randomAlphaOfLength(12); | ||
AtomicReference<UserRoleMapper.UserData> userData = new AtomicReference<>(); | ||
doAnswer(invocation -> { | ||
assert invocation.getArguments().length == 2; | ||
userData.set((UserRoleMapper.UserData) invocation.getArguments()[0]); | ||
ActionListener<Set<String>> listener = (ActionListener<Set<String>>) invocation.getArguments()[1]; | ||
listener.onResponse(new HashSet<>(Arrays.asList("kibana_user", "role1"))); | ||
return null; | ||
}).when(roleMapper).resolveRoles(any(UserRoleMapper.UserData.class), any(ActionListener.class)); | ||
Map<String, Object> claims = Map.of( | ||
"string", "String", | ||
"number", 232, | ||
"boolean", true, | ||
"string_array", List.of("one", "two", "three"), | ||
"number_array", List.of(1, 2, 3), | ||
"boolean_array", List.of(true, false, true), | ||
"object", Map.of("key", List.of("value1", "value2")), | ||
"object_array", List.of(Map.of("key1", List.of("value1", "value2")), Map.of("key2", List.of("value1", "value2"))) | ||
); | ||
AuthenticationResult result = authenticateWithOidc(principal, roleMapper, false, false, REALM_NAME, claims); | ||
assertThat(result, notNullValue()); | ||
assertThat(result.getStatus(), equalTo(AuthenticationResult.Status.SUCCESS)); | ||
assertThat(result.getUser().principal(), equalTo(principal)); | ||
assertThat(result.getUser().email(), equalTo("[email protected]")); | ||
assertThat(result.getUser().fullName(), equalTo("Clinton Barton")); | ||
assertThat(result.getUser().roles(), arrayContainingInAnyOrder("kibana_user", "role1")); | ||
assertTrue(result.getUser().metadata().containsKey("oidc(string)")); | ||
assertTrue(result.getUser().metadata().containsKey("oidc(number)")); | ||
assertTrue(result.getUser().metadata().containsKey("oidc(boolean)")); | ||
assertTrue(result.getUser().metadata().containsKey("oidc(string_array)")); | ||
assertTrue(result.getUser().metadata().containsKey("oidc(boolean_array)")); | ||
assertTrue(result.getUser().metadata().containsKey("oidc(number_array)")); | ||
assertFalse(result.getUser().metadata().containsKey("oidc(object_array)")); | ||
assertFalse(result.getUser().metadata().containsKey("oidc(object)")); | ||
} | ||
|
||
public void testWithAuthorizingRealm() throws Exception { | ||
final UserRoleMapper roleMapper = mock(UserRoleMapper.class); | ||
final String principal = randomAlphaOfLength(12); | ||
|
@@ -114,7 +177,7 @@ public void testWithAuthorizingRealm() throws Exception { | |
return null; | ||
}).when(roleMapper).resolveRoles(any(UserRoleMapper.UserData.class), any(ActionListener.class)); | ||
final String authenticatingRealm = randomBoolean() ? REALM_NAME : null; | ||
AuthenticationResult result = authenticateWithOidc(principal, roleMapper, randomBoolean(), true, authenticatingRealm); | ||
AuthenticationResult result = authenticateWithOidc(principal, roleMapper, randomBoolean(), true, authenticatingRealm, null); | ||
assertThat(result, notNullValue()); | ||
assertThat(result.getStatus(), equalTo(AuthenticationResult.Status.SUCCESS)); | ||
assertThat(result.getUser().principal(), equalTo(principal)); | ||
|
@@ -132,7 +195,7 @@ public void testWithAuthorizingRealm() throws Exception { | |
public void testAuthenticationWithWrongRealm() throws Exception{ | ||
final String principal = randomAlphaOfLength(12); | ||
AuthenticationResult result = authenticateWithOidc(principal, mock(UserRoleMapper.class), randomBoolean(), true, | ||
REALM_NAME+randomAlphaOfLength(8)); | ||
REALM_NAME + randomAlphaOfLength(8), null); | ||
assertThat(result, notNullValue()); | ||
assertThat(result.getStatus(), equalTo(AuthenticationResult.Status.CONTINUE)); | ||
} | ||
|
@@ -334,8 +397,8 @@ public void testBuildingAuthenticationRequestWithLoginHint() { | |
} | ||
|
||
private AuthenticationResult authenticateWithOidc(String principal, UserRoleMapper roleMapper, boolean notPopulateMetadata, | ||
boolean useAuthorizingRealm | ||
,String authenticatingRealm) | ||
boolean useAuthorizingRealm, String authenticatingRealm, | ||
@Nullable Map<String, Object> additionalClaims) | ||
throws Exception { | ||
RealmConfig.RealmIdentifier realmIdentifier = new RealmConfig.RealmIdentifier("mock", "mock_lookup"); | ||
final MockLookupRealm lookupRealm = new MockLookupRealm( | ||
|
@@ -360,7 +423,7 @@ private AuthenticationResult authenticateWithOidc(String principal, UserRoleMapp | |
final OpenIdConnectRealm realm = new OpenIdConnectRealm(config, authenticator, roleMapper); | ||
initializeRealms(realm, lookupRealm); | ||
final OpenIdConnectToken token = new OpenIdConnectToken("", new State(), new Nonce(), authenticatingRealm); | ||
final JWTClaimsSet claims = new JWTClaimsSet.Builder() | ||
final JWTClaimsSet.Builder claimsBuilder = new JWTClaimsSet.Builder() | ||
.subject(principal) | ||
.audience("https://rp.elastic.co/cb") | ||
.expirationTime(Date.from(now().plusSeconds(3600))) | ||
|
@@ -370,9 +433,13 @@ private AuthenticationResult authenticateWithOidc(String principal, UserRoleMapp | |
.claim("groups", Arrays.asList("group1", "group2", "groups3")) | ||
.claim("mail", "[email protected]") | ||
.claim("name", "Clinton Barton") | ||
.claim("id_token_hint", "thisis.aserialized.jwt") | ||
.build(); | ||
|
||
.claim("id_token_hint", "thisis.aserialized.jwt"); | ||
if (additionalClaims != null) { | ||
for (Map.Entry<String, Object> entry : additionalClaims.entrySet()) { | ||
claimsBuilder.claim(entry.getKey(), entry.getValue()); | ||
} | ||
} | ||
final JWTClaimsSet claims = claimsBuilder.build(); | ||
doAnswer((i) -> { | ||
ActionListener<JWTClaimsSet> listener = (ActionListener<JWTClaimsSet>) i.getArguments()[1]; | ||
listener.onResponse(claims); | ||
|