Skip to content

Commit 885f78b

Browse files
permit at+jwt typ header value in jwt access tokens
1 parent 55453ae commit 885f78b

File tree

5 files changed

+70
-13
lines changed

5 files changed

+70
-13
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ private static List<JwtFieldValidator> configureFieldValidatorsForIdToken(RealmC
136136
}
137137

138138
return List.of(
139-
JwtTypeValidator.INSTANCE,
139+
JwtTypeValidator.ID_TOKEN_INSTANCE,
140140
new JwtStringClaimValidator("iss", true, List.of(realmConfig.getSetting(JwtRealmSettings.ALLOWED_ISSUER)), List.of()),
141141
subjectClaimValidator,
142142
new JwtStringClaimValidator("aud", false, realmConfig.getSetting(JwtRealmSettings.ALLOWED_AUDIENCES), List.of()),
@@ -157,7 +157,7 @@ private static List<JwtFieldValidator> configureFieldValidatorsForAccessToken(
157157
final Clock clock = Clock.systemUTC();
158158

159159
return List.of(
160-
JwtTypeValidator.INSTANCE,
160+
JwtTypeValidator.ACCESS_TOKEN_INSTANCE,
161161
new JwtStringClaimValidator("iss", true, List.of(realmConfig.getSetting(JwtRealmSettings.ALLOWED_ISSUER)), List.of()),
162162
getSubjectClaimValidator(realmConfig, fallbackClaimLookup),
163163
new JwtStringClaimValidator(

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwtTypeValidator.java

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,20 @@
1717

1818
public class JwtTypeValidator implements JwtFieldValidator {
1919

20-
private static final JOSEObjectTypeVerifier<SecurityContext> JWT_HEADER_TYPE_VERIFIER = new DefaultJOSEObjectTypeVerifier<>(
20+
private final JOSEObjectTypeVerifier<SecurityContext> JWT_HEADER_TYPE_VERIFIER;
21+
22+
public static final JwtTypeValidator ID_TOKEN_INSTANCE = new JwtTypeValidator(JOSEObjectType.JWT, null);
23+
24+
// strictly speaking, this should only permit `at+jwt`, but removing the other two options is a breaking change
25+
public static final JwtTypeValidator ACCESS_TOKEN_INSTANCE = new JwtTypeValidator(
2126
JOSEObjectType.JWT,
27+
new JOSEObjectType("at+jwt"),
2228
null
2329
);
2430

25-
public static final JwtTypeValidator INSTANCE = new JwtTypeValidator();
26-
27-
private JwtTypeValidator() {}
31+
private JwtTypeValidator(JOSEObjectType... allowedTypes) {
32+
JWT_HEADER_TYPE_VERIFIER = new DefaultJOSEObjectTypeVerifier<>(allowedTypes);
33+
}
2834

2935
public void validate(JWSHeader jwsHeader, JWTClaimsSet jwtClaimsSet) {
3036
final JOSEObjectType jwtHeaderType = jwsHeader.getType();

x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtAuthenticatorIdTokenTypeTests.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,21 @@
77

88
package org.elasticsearch.xpack.security.authc.jwt;
99

10+
import com.nimbusds.jose.JWSHeader;
11+
import com.nimbusds.jose.util.Base64URL;
12+
import com.nimbusds.jwt.JWTClaimsSet;
13+
import com.nimbusds.jwt.SignedJWT;
14+
15+
import org.elasticsearch.action.support.PlainActionFuture;
1016
import org.elasticsearch.xpack.core.security.authc.jwt.JwtRealmSettings;
1117

1218
import java.text.ParseException;
19+
import java.util.Map;
1320

1421
import static org.hamcrest.Matchers.containsString;
22+
import static org.hamcrest.Matchers.equalTo;
23+
import static org.mockito.Mockito.mock;
24+
import static org.mockito.Mockito.when;
1525

1626
public class JwtAuthenticatorIdTokenTypeTests extends JwtAuthenticatorTests {
1727

@@ -28,4 +38,23 @@ public void testSubjectIsRequired() throws ParseException {
2838
public void testInvalidIssuerIsCheckedBeforeAlgorithm() throws ParseException {
2939
doTestInvalidIssuerIsCheckedBeforeAlgorithm(buildJwtAuthenticator());
3040
}
41+
42+
public void testAccessTokenHeaderTypeIsRejected() throws ParseException {
43+
final JWTClaimsSet claimsSet = JWTClaimsSet.parse(Map.of());
44+
final SignedJWT signedJWT = new SignedJWT(
45+
JWSHeader.parse(Map.of("alg", allowedAlgorithm, "typ", "at+jwt")).toBase64URL(),
46+
claimsSet.toPayload().toBase64URL(),
47+
Base64URL.encode("signature")
48+
);
49+
50+
final JwtAuthenticationToken jwtAuthenticationToken = mock(JwtAuthenticationToken.class);
51+
when(jwtAuthenticationToken.getSignedJWT()).thenReturn(signedJWT);
52+
when(jwtAuthenticationToken.getJWTClaimsSet()).thenReturn(signedJWT.getJWTClaimsSet());
53+
54+
final PlainActionFuture<JWTClaimsSet> future = new PlainActionFuture<>();
55+
final JwtAuthenticator jwtAuthenticator = buildJwtAuthenticator();
56+
jwtAuthenticator.authenticate(jwtAuthenticationToken, future);
57+
final Exception e = expectThrows(IllegalArgumentException.class, future::actionGet);
58+
assertThat(e.getMessage(), equalTo("invalid jwt typ header"));
59+
}
3160
}

x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealmAuthenticateAccessTokenTypeTests.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77

88
package org.elasticsearch.xpack.security.authc.jwt;
99

10-
import com.nimbusds.jose.JOSEObjectType;
1110
import com.nimbusds.jose.jwk.JWK;
1211
import com.nimbusds.jwt.SignedJWT;
1312
import com.nimbusds.openid.connect.sdk.Nonce;
@@ -134,7 +133,7 @@ protected SecureString randomJwt(JwtIssuerAndRealm jwtIssuerAndRealm, User user)
134133

135134
final Instant now = Instant.now().truncatedTo(ChronoUnit.SECONDS);
136135
unsignedJwt = JwtTestCase.buildUnsignedJwt(
137-
randomBoolean() ? null : JOSEObjectType.JWT.toString(), // kty
136+
randomFrom("at+jwt", "JWT", null), // typ
138137
randomBoolean() ? null : jwk.getKeyID(), // kid
139138
algJwkPair.alg(), // alg
140139
randomAlphaOfLengthBetween(10, 20), // jwtID

x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtTypeValidatorTests.java

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,30 +19,53 @@
1919

2020
public class JwtTypeValidatorTests extends ESTestCase {
2121

22-
public void testValidType() throws ParseException {
22+
public void testValidIdTokenType() throws ParseException {
2323
final String algorithm = randomAlphaOfLengthBetween(3, 8);
2424

25-
// typ is allowed to be missing
2625
final JWSHeader jwsHeader = JWSHeader.parse(
27-
randomFrom(Map.of("alg", randomAlphaOfLengthBetween(3, 8)), Map.of("typ", "JWT", "alg", randomAlphaOfLengthBetween(3, 8)))
26+
randomFrom(
27+
// typ is allowed to be missing
28+
Map.of("alg", algorithm),
29+
Map.of("typ", "JWT", "alg", algorithm)
30+
)
2831
);
2932

3033
try {
31-
JwtTypeValidator.INSTANCE.validate(jwsHeader, JWTClaimsSet.parse(Map.of()));
34+
JwtTypeValidator.ID_TOKEN_INSTANCE.validate(jwsHeader, JWTClaimsSet.parse(Map.of()));
35+
} catch (Exception e) {
36+
throw new AssertionError("validation should have passed without exception", e);
37+
}
38+
}
39+
40+
public void testValidAccessTokenType() throws ParseException {
41+
final String algorithm = randomAlphaOfLengthBetween(3, 8);
42+
43+
final JWSHeader jwsHeader = JWSHeader.parse(
44+
randomFrom(
45+
// typ is allowed to be missing
46+
Map.of("alg", algorithm),
47+
Map.of("typ", "JWT", "alg", algorithm),
48+
Map.of("typ", "at+jwt", "alg", algorithm)
49+
)
50+
);
51+
52+
try {
53+
JwtTypeValidator.ACCESS_TOKEN_INSTANCE.validate(jwsHeader, JWTClaimsSet.parse(Map.of()));
3254
} catch (Exception e) {
3355
throw new AssertionError("validation should have passed without exception", e);
3456
}
3557
}
3658

3759
public void testInvalidType() throws ParseException {
60+
final JwtTypeValidator validator = randomFrom(JwtTypeValidator.ID_TOKEN_INSTANCE, JwtTypeValidator.ACCESS_TOKEN_INSTANCE);
3861

3962
final JWSHeader jwsHeader = JWSHeader.parse(
4063
Map.of("typ", randomAlphaOfLengthBetween(4, 8), "alg", randomAlphaOfLengthBetween(3, 8))
4164
);
4265

4366
final IllegalArgumentException e = expectThrows(
4467
IllegalArgumentException.class,
45-
() -> JwtTypeValidator.INSTANCE.validate(jwsHeader, JWTClaimsSet.parse(Map.of()))
68+
() -> validator.validate(jwsHeader, JWTClaimsSet.parse(Map.of()))
4669
);
4770
assertThat(e.getMessage(), containsString("invalid jwt typ header"));
4871
}

0 commit comments

Comments
 (0)