Skip to content

Commit 20cfb3b

Browse files
authored
Short circuit failure handling in OIDC flow (#130618) (#132404)
We should return after the `onFailure` call during the claims check.
1 parent cac4752 commit 20cfb3b

File tree

2 files changed

+62
-22
lines changed

2 files changed

+62
-22
lines changed

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectAuthenticator.java

Lines changed: 14 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -477,7 +477,20 @@ static void handleUserinfoResponse(
477477
if (httpResponse.getStatusLine().getStatusCode() == 200) {
478478
if (ContentType.parse(contentHeader.getValue()).getMimeType().equals("application/json")) {
479479
final JWTClaimsSet userInfoClaims = JWTClaimsSet.parse(contentAsString);
480-
validateUserInfoResponse(userInfoClaims, verifiedIdTokenClaims.getSubject(), claimsListener);
480+
String expectedSub = verifiedIdTokenClaims.getSubject();
481+
if (userInfoClaims.getSubject() == null || userInfoClaims.getSubject().isEmpty()) {
482+
claimsListener.onFailure(new ElasticsearchSecurityException("Userinfo Response did not contain a sub Claim"));
483+
return;
484+
} else if (userInfoClaims.getSubject().equals(expectedSub) == false) {
485+
claimsListener.onFailure(
486+
new ElasticsearchSecurityException(
487+
"Userinfo Response is not valid as it is for " + "subject [{}] while the ID Token was for subject [{}]",
488+
userInfoClaims.getSubject(),
489+
expectedSub
490+
)
491+
);
492+
return;
493+
}
481494
if (LOGGER.isTraceEnabled()) {
482495
LOGGER.trace("Successfully retrieved user information: [{}]", userInfoClaims);
483496
}
@@ -527,27 +540,6 @@ static void handleUserinfoResponse(
527540
}
528541
}
529542

530-
/**
531-
* Validates that the userinfo response contains a sub Claim and that this claim value is the same as the one returned in the ID Token
532-
*/
533-
private static void validateUserInfoResponse(
534-
JWTClaimsSet userInfoClaims,
535-
String expectedSub,
536-
ActionListener<JWTClaimsSet> claimsListener
537-
) {
538-
if (userInfoClaims.getSubject().isEmpty()) {
539-
claimsListener.onFailure(new ElasticsearchSecurityException("Userinfo Response did not contain a sub Claim"));
540-
} else if (userInfoClaims.getSubject().equals(expectedSub) == false) {
541-
claimsListener.onFailure(
542-
new ElasticsearchSecurityException(
543-
"Userinfo Response is not valid as it is for " + "subject [{}] while the ID Token was for subject [{}]",
544-
userInfoClaims.getSubject(),
545-
expectedSub
546-
)
547-
);
548-
}
549-
}
550-
551543
/**
552544
* Attempts to make a request to the Token Endpoint of the OpenID Connect provider in order to exchange an
553545
* authorization code for an Id Token (and potentially an Access Token)

x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectAuthenticatorTests.java

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@
108108
import java.util.Map;
109109
import java.util.UUID;
110110
import java.util.concurrent.CountDownLatch;
111+
import java.util.concurrent.atomic.AtomicBoolean;
111112
import java.util.concurrent.atomic.AtomicReference;
112113

113114
import javax.crypto.SecretKey;
@@ -968,6 +969,53 @@ public void testHandleUserinfoResponseFailure() throws Exception {
968969
);
969970
}
970971

972+
public void testHandleUserinfoValidationFailsOnNotMatchingSubject() throws Exception {
973+
final ProtocolVersion httpVersion = randomFrom(HttpVersion.HTTP_0_9, HttpVersion.HTTP_1_0, HttpVersion.HTTP_1_1);
974+
final HttpResponse response = new BasicHttpResponse(new BasicStatusLine(httpVersion, RestStatus.OK.getStatus(), "OK"));
975+
976+
final String sub = randomAlphaOfLengthBetween(4, 36);
977+
final String inf = randomAlphaOfLength(12);
978+
final JWTClaimsSet infoClaims = new JWTClaimsSet.Builder().subject("it-is-a-different-subject").claim("inf", inf).build();
979+
final StringEntity entity = new StringEntity(infoClaims.toString(), ContentType.APPLICATION_JSON);
980+
if (randomBoolean()) {
981+
entity.setContentEncoding(
982+
randomFrom(StandardCharsets.UTF_8.name(), StandardCharsets.UTF_16.name(), StandardCharsets.US_ASCII.name())
983+
);
984+
}
985+
response.setEntity(entity);
986+
987+
final String idx = randomAlphaOfLength(8);
988+
final JWTClaimsSet idClaims = new JWTClaimsSet.Builder().subject(sub).claim("idx", idx).build();
989+
final AtomicBoolean listenerCalled = new AtomicBoolean(false);
990+
final PlainActionFuture<JWTClaimsSet> future = new PlainActionFuture<>() {
991+
992+
@Override
993+
public void onResponse(JWTClaimsSet result) {
994+
assertTrue("listener called more than once", listenerCalled.compareAndSet(false, true));
995+
super.onResponse(result);
996+
}
997+
998+
@Override
999+
public void onFailure(Exception e) {
1000+
assertTrue("listener called more than once", listenerCalled.compareAndSet(false, true));
1001+
super.onFailure(e);
1002+
}
1003+
};
1004+
1005+
this.authenticator = buildAuthenticator();
1006+
OpenIdConnectAuthenticator.handleUserinfoResponse(response, idClaims, future);
1007+
var e = expectThrows(ElasticsearchSecurityException.class, future::actionGet);
1008+
1009+
assertThat(
1010+
e.getMessage(),
1011+
equalTo(
1012+
"Userinfo Response is not valid as it is for subject [it-is-a-different-subject] while the ID Token was for subject ["
1013+
+ sub
1014+
+ "]"
1015+
)
1016+
);
1017+
}
1018+
9711019
public void testHandleTokenResponseNullContentType() {
9721020
final HttpResponse response = new BasicHttpResponse(HttpVersion.HTTP_1_1, RestStatus.OK.getStatus(), "");
9731021
final StringEntity entity = new StringEntity("", (ContentType) null);

0 commit comments

Comments
 (0)