forked from elastic/elasticsearch
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge claims from userinfo and ID Token correctly (elastic#42277)
Enhance the handling of merging the claims sets of the ID Token and the UserInfo response. JsonObject#merge would throw a runtime exception when attempting to merge two objects with the same key and different values. This could happen for an OP that returns different vales for the same claim in the ID Token and the UserInfo response ( Google does that for profile claim ). If a claim is contained in both sets, we attempt to merge the values if they are objects or arrays, otherwise the ID Token claim value takes presedence and overwrites the userinfo response.
- Loading branch information
Showing
2 changed files
with
226 additions
and
3 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
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -37,6 +37,8 @@ | |
import com.nimbusds.openid.connect.sdk.claims.AccessTokenHash; | ||
import com.nimbusds.openid.connect.sdk.validators.IDTokenValidator; | ||
import com.nimbusds.openid.connect.sdk.validators.InvalidHashException; | ||
import net.minidev.json.JSONArray; | ||
import net.minidev.json.JSONObject; | ||
import org.elasticsearch.ElasticsearchSecurityException; | ||
import org.elasticsearch.action.ActionListener; | ||
import org.elasticsearch.action.support.PlainActionFuture; | ||
|
@@ -72,6 +74,7 @@ | |
import java.util.UUID; | ||
|
||
import static java.time.Instant.now; | ||
import static org.hamcrest.Matchers.containsInAnyOrder; | ||
import static org.hamcrest.Matchers.containsString; | ||
import static org.hamcrest.Matchers.equalTo; | ||
import static org.hamcrest.Matchers.instanceOf; | ||
|
@@ -96,7 +99,9 @@ public void setup() { | |
|
||
@After | ||
public void cleanup() { | ||
authenticator.close(); | ||
if (authenticator != null) { | ||
authenticator.close(); | ||
} | ||
} | ||
|
||
private OpenIdConnectAuthenticator buildAuthenticator() throws URISyntaxException { | ||
|
@@ -632,6 +637,140 @@ public void testImplicitFlowFailsWithUnsignedJwt() throws Exception { | |
assertThat(e.getCause().getMessage(), containsString("Signed ID token expected")); | ||
} | ||
|
||
public void testJsonObjectMerging() throws Exception { | ||
final Nonce nonce = new Nonce(); | ||
final String subject = "janedoe"; | ||
final Tuple<Key, JWKSet> keyMaterial = getRandomJwkForType(randomFrom("ES", "RS")); | ||
final JWK jwk = keyMaterial.v2().getKeys().get(0); | ||
RelyingPartyConfiguration rpConfig = getRpConfig(jwk.getAlgorithm().getName()); | ||
OpenIdConnectProviderConfiguration opConfig = getOpConfig(); | ||
JSONObject address = new JWTClaimsSet.Builder() | ||
.claim("street_name", "12, Test St.") | ||
.claim("locality", "New York") | ||
.claim("region", "NY") | ||
.claim("country", "USA") | ||
.build() | ||
.toJSONObject(); | ||
JSONObject idTokenObject = new JWTClaimsSet.Builder() | ||
.jwtID(randomAlphaOfLength(8)) | ||
.audience(rpConfig.getClientId().getValue()) | ||
.expirationTime(Date.from(now().plusSeconds(3600))) | ||
.issuer(opConfig.getIssuer().getValue()) | ||
.issueTime(Date.from(now().minusSeconds(200))) | ||
.notBeforeTime(Date.from(now().minusSeconds(200))) | ||
.claim("nonce", nonce) | ||
.claim("given_name", "Jane Doe") | ||
.claim("family_name", "Doe") | ||
.claim("profile", "https://test-profiles.com/jane.doe") | ||
.claim("name", "Jane") | ||
.claim("email", "[email protected]") | ||
.claim("roles", new JSONArray().appendElement("role1").appendElement("role2").appendElement("role3")) | ||
.claim("address", address) | ||
.subject(subject) | ||
.build() | ||
.toJSONObject(); | ||
|
||
JSONObject userinfoObject = new JWTClaimsSet.Builder() | ||
.claim("given_name", "Jane Doe") | ||
.claim("family_name", "Doe") | ||
.claim("profile", "https://test-profiles.com/jane.doe") | ||
.claim("name", "Jane") | ||
.claim("email", "[email protected]") | ||
.subject(subject) | ||
.build() | ||
.toJSONObject(); | ||
|
||
OpenIdConnectAuthenticator.mergeObjects(idTokenObject, userinfoObject); | ||
assertTrue(idTokenObject.containsKey("given_name")); | ||
assertTrue(idTokenObject.containsKey("family_name")); | ||
assertTrue(idTokenObject.containsKey("profile")); | ||
assertTrue(idTokenObject.containsKey("name")); | ||
assertTrue(idTokenObject.containsKey("email")); | ||
assertTrue(idTokenObject.containsKey("address")); | ||
assertTrue(idTokenObject.containsKey("roles")); | ||
assertTrue(idTokenObject.containsKey("nonce")); | ||
assertTrue(idTokenObject.containsKey("sub")); | ||
assertTrue(idTokenObject.containsKey("jti")); | ||
assertTrue(idTokenObject.containsKey("aud")); | ||
assertTrue(idTokenObject.containsKey("exp")); | ||
assertTrue(idTokenObject.containsKey("iss")); | ||
assertTrue(idTokenObject.containsKey("iat")); | ||
assertTrue(idTokenObject.containsKey("email")); | ||
|
||
// Claims with different types throw an error | ||
JSONObject wrongTypeInfo = new JWTClaimsSet.Builder() | ||
.claim("given_name", "Jane Doe") | ||
.claim("family_name", 123334434) | ||
.claim("profile", "https://test-profiles.com/jane.doe") | ||
.claim("name", "Jane") | ||
.claim("email", "[email protected]") | ||
.subject(subject) | ||
.build() | ||
.toJSONObject(); | ||
|
||
final IllegalStateException e = expectThrows(IllegalStateException.class, () -> { | ||
OpenIdConnectAuthenticator.mergeObjects(idTokenObject, wrongTypeInfo); | ||
}); | ||
|
||
// Userinfo Claims overwrite ID Token claims | ||
JSONObject overwriteUserInfo = new JWTClaimsSet.Builder() | ||
.claim("given_name", "Jane Doe") | ||
.claim("family_name", "Doe") | ||
.claim("profile", "https://test-profiles.com/jane.doe2") | ||
.claim("name", "Jane") | ||
.claim("email", "[email protected]") | ||
.subject(subject) | ||
.build() | ||
.toJSONObject(); | ||
|
||
OpenIdConnectAuthenticator.mergeObjects(idTokenObject, overwriteUserInfo); | ||
assertThat(idTokenObject.getAsString("email"), equalTo("[email protected]")); | ||
assertThat(idTokenObject.getAsString("profile"), equalTo("https://test-profiles.com/jane.doe")); | ||
|
||
// Merging Arrays | ||
JSONObject userInfoWithRoles = new JWTClaimsSet.Builder() | ||
.claim("given_name", "Jane Doe") | ||
.claim("family_name", "Doe") | ||
.claim("profile", "https://test-profiles.com/jane.doe") | ||
.claim("name", "Jane") | ||
.claim("email", "[email protected]") | ||
.claim("roles", new JSONArray().appendElement("role4").appendElement("role5")) | ||
.subject(subject) | ||
.build() | ||
.toJSONObject(); | ||
|
||
OpenIdConnectAuthenticator.mergeObjects(idTokenObject, userInfoWithRoles); | ||
assertThat((JSONArray) idTokenObject.get("roles"), containsInAnyOrder("role1", "role2", "role3", "role4", "role5")); | ||
|
||
// Merging nested objects | ||
JSONObject addressUserInfo = new JWTClaimsSet.Builder() | ||
.claim("street_name", "12, Test St.") | ||
.claim("locality", "New York") | ||
.claim("postal_code", "10024") | ||
.build() | ||
.toJSONObject(); | ||
JSONObject userInfoWithAddress = new JWTClaimsSet.Builder() | ||
.claim("given_name", "Jane Doe") | ||
.claim("family_name", "Doe") | ||
.claim("profile", "https://test-profiles.com/jane.doe") | ||
.claim("name", "Jane") | ||
.claim("email", "[email protected]") | ||
.claim("roles", new JSONArray().appendElement("role4").appendElement("role5")) | ||
.claim("address", addressUserInfo) | ||
.subject(subject) | ||
.build() | ||
.toJSONObject(); | ||
OpenIdConnectAuthenticator.mergeObjects(idTokenObject, userInfoWithAddress); | ||
assertTrue(idTokenObject.containsKey("address")); | ||
JSONObject combinedAddress = (JSONObject) idTokenObject.get("address"); | ||
assertTrue(combinedAddress.containsKey("street_name")); | ||
assertTrue(combinedAddress.containsKey("locality")); | ||
assertTrue(combinedAddress.containsKey("street_name")); | ||
assertTrue(combinedAddress.containsKey("postal_code")); | ||
assertTrue(combinedAddress.containsKey("region")); | ||
assertTrue(combinedAddress.containsKey("country")); | ||
} | ||
|
||
private OpenIdConnectProviderConfiguration getOpConfig() throws URISyntaxException { | ||
return new OpenIdConnectProviderConfiguration( | ||
new Issuer("https://op.example.com"), | ||
|