Skip to content

Commit a967a8e

Browse files
richard-dennehyelasticsearchmachine
andauthored
permit at+jwt typ header value in jwt access tokens (elastic#126687) (elastic#126832)
* permit at+jwt typ header value in jwt access tokens * Update docs/changelog/126687.yaml * address review comments * [CI] Auto commit changes from spotless * update Type Validator tests for parser ignoring case --------- Co-authored-by: elasticsearchmachine <[email protected]>
1 parent f9fd055 commit a967a8e

File tree

8 files changed

+92
-27
lines changed

8 files changed

+92
-27
lines changed

docs/changelog/126687.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
pr: 126687
2+
summary: Permit at+jwt typ header value in jwt access tokens
3+
area: Authentication
4+
type: enhancement
5+
issues:
6+
- 119370

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

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -456,13 +456,13 @@ public void testFailureOnInvalidHMACSignature() throws Exception {
456456

457457
{
458458
// This is the correct HMAC passphrase (from build.gradle)
459-
final SignedJWT jwt = signHmacJwt(claimsSet, HMAC_PASSPHRASE);
459+
final SignedJWT jwt = signHmacJwt(claimsSet, HMAC_PASSPHRASE, false);
460460
final TestSecurityClient client = getSecurityClient(jwt, Optional.of(VALID_SHARED_SECRET));
461461
assertThat(client.authenticate(), hasEntry(User.Fields.USERNAME.getPreferredName(), username));
462462
}
463463
{
464464
// This is not the correct HMAC passphrase
465-
final SignedJWT invalidJwt = signHmacJwt(claimsSet, "invalid-HMAC-passphrase-" + randomAlphaOfLength(12));
465+
final SignedJWT invalidJwt = signHmacJwt(claimsSet, "invalid-HMAC-passphrase-" + randomAlphaOfLength(12), false);
466466
final TestSecurityClient client = getSecurityClient(invalidJwt, Optional.of(VALID_SHARED_SECRET));
467467
// This fails because the HMAC is wrong
468468
final ResponseException exception = expectThrows(ResponseException.class, client::authenticate);
@@ -487,7 +487,7 @@ public void testFailureOnRequiredClaims() throws JOSEException, IOException {
487487
data.put("token_use", randomValueOtherThan("access", () -> randomAlphaOfLengthBetween(3, 10)));
488488
}
489489
final JWTClaimsSet claimsSet = buildJwt(data, Instant.now(), false, false);
490-
final SignedJWT jwt = signHmacJwt(claimsSet, "test-HMAC/secret passphrase-value");
490+
final SignedJWT jwt = signHmacJwt(claimsSet, "test-HMAC/secret passphrase-value", false);
491491
final TestSecurityClient client = getSecurityClient(jwt, Optional.of(VALID_SHARED_SECRET));
492492
final ResponseException exception = expectThrows(ResponseException.class, client::authenticate);
493493
assertThat(exception.getResponse(), hasStatusCode(RestStatus.UNAUTHORIZED));
@@ -747,18 +747,18 @@ private SignedJWT buildAndSignJwtForRealm3(String principal, Instant issueTime)
747747

748748
private SignedJWT signJwtForRealm1(JWTClaimsSet claimsSet) throws IOException, JOSEException, ParseException {
749749
final RSASSASigner signer = loadRsaSigner();
750-
return signJWT(signer, "RS256", claimsSet);
750+
return signJWT(signer, "RS256", claimsSet, false);
751751
}
752752

753-
private SignedJWT signJwtForRealm2(JWTClaimsSet claimsSet) throws JOSEException, ParseException {
753+
private SignedJWT signJwtForRealm2(JWTClaimsSet claimsSet) throws JOSEException {
754754
// Input string is configured in build.gradle
755-
return signHmacJwt(claimsSet, "test-HMAC/secret passphrase-value");
755+
return signHmacJwt(claimsSet, "test-HMAC/secret passphrase-value", true);
756756
}
757757

758758
private SignedJWT signJwtForRealm3(JWTClaimsSet claimsSet) throws JOSEException, ParseException, IOException {
759759
final int bitSize = randomFrom(384, 512);
760760
final MACSigner signer = loadHmacSigner("test-hmac-" + bitSize);
761-
return signJWT(signer, "HS" + bitSize, claimsSet);
761+
return signJWT(signer, "HS" + bitSize, claimsSet, false);
762762
}
763763

764764
private RSASSASigner loadRsaSigner() throws IOException, ParseException, JOSEException {
@@ -781,10 +781,10 @@ private MACSigner loadHmacSigner(String keyId) throws IOException, ParseExceptio
781781
}
782782
}
783783

784-
private SignedJWT signHmacJwt(JWTClaimsSet claimsSet, String hmacPassphrase) throws JOSEException {
784+
private SignedJWT signHmacJwt(JWTClaimsSet claimsSet, String hmacPassphrase, boolean allowAtJwtType) throws JOSEException {
785785
final OctetSequenceKey hmac = JwkValidateUtil.buildHmacKeyFromString(hmacPassphrase);
786786
final JWSSigner signer = new MACSigner(hmac);
787-
return signJWT(signer, "HS256", claimsSet);
787+
return signJWT(signer, "HS256", claimsSet, allowAtJwtType);
788788
}
789789

790790
// JWT construction
@@ -822,10 +822,14 @@ static JWTClaimsSet buildJwt(Map<String, Object> claims, Instant issueTime, bool
822822
return builder.build();
823823
}
824824

825-
static SignedJWT signJWT(JWSSigner signer, String algorithm, JWTClaimsSet claimsSet) throws JOSEException {
825+
static SignedJWT signJWT(JWSSigner signer, String algorithm, JWTClaimsSet claimsSet, boolean allowAtJwtType) throws JOSEException {
826826
final JWSHeader.Builder builder = new JWSHeader.Builder(JWSAlgorithm.parse(algorithm));
827827
if (randomBoolean()) {
828-
builder.type(JOSEObjectType.JWT);
828+
if (allowAtJwtType && randomBoolean()) {
829+
builder.type(new JOSEObjectType("at+jwt"));
830+
} else {
831+
builder.type(JOSEObjectType.JWT);
832+
}
829833
}
830834
final JWSHeader jwtHeader = builder.build();
831835
final SignedJWT jwt = new SignedJWT(jwtHeader, claimsSet);

x-pack/plugin/security/qa/jwt-realm/src/javaRestTest/java/org/elasticsearch/xpack/security/authc/jwt/JwtWithUnavailableSecurityIndexRestIT.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,7 @@ private SignedJWT buildAndSignJwt(String principal, String dn, Instant issueTime
279279
issueTime
280280
);
281281
final RSASSASigner signer = loadRsaSigner();
282-
return JwtRestIT.signJWT(signer, "RS256", claimsSet);
282+
return JwtRestIT.signJWT(signer, "RS256", claimsSet, false);
283283
}
284284

285285
private RSASSASigner loadRsaSigner() throws IOException, ParseException, JOSEException {

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: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,17 @@
1717

1818
public class JwtTypeValidator implements JwtFieldValidator {
1919

20-
private static final JOSEObjectTypeVerifier<SecurityContext> JWT_HEADER_TYPE_VERIFIER = new DefaultJOSEObjectTypeVerifier<>(
21-
JOSEObjectType.JWT,
22-
null
23-
);
20+
private final JOSEObjectTypeVerifier<SecurityContext> JWT_HEADER_TYPE_VERIFIER;
21+
private static final JOSEObjectType AT_PLUS_JWT = new JOSEObjectType("at+jwt");
2422

25-
public static final JwtTypeValidator INSTANCE = new JwtTypeValidator();
23+
public static final JwtTypeValidator ID_TOKEN_INSTANCE = new JwtTypeValidator(JOSEObjectType.JWT, null);
2624

27-
private JwtTypeValidator() {}
25+
// strictly speaking, this should only permit `at+jwt`, but removing the other two options is a breaking change
26+
public static final JwtTypeValidator ACCESS_TOKEN_INSTANCE = new JwtTypeValidator(JOSEObjectType.JWT, AT_PLUS_JWT, null);
27+
28+
private JwtTypeValidator(JOSEObjectType... allowedTypes) {
29+
JWT_HEADER_TYPE_VERIFIER = new DefaultJOSEObjectTypeVerifier<>(allowedTypes);
30+
}
2831

2932
public void validate(JWSHeader jwsHeader, JWTClaimsSet jwtClaimsSet) {
3033
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: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,30 +19,54 @@
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+
Map.of("typ", "AT+JWT", "alg", algorithm)
50+
)
51+
);
52+
53+
try {
54+
JwtTypeValidator.ACCESS_TOKEN_INSTANCE.validate(jwsHeader, JWTClaimsSet.parse(Map.of()));
3255
} catch (Exception e) {
3356
throw new AssertionError("validation should have passed without exception", e);
3457
}
3558
}
3659

3760
public void testInvalidType() throws ParseException {
61+
final JwtTypeValidator validator = randomFrom(JwtTypeValidator.ID_TOKEN_INSTANCE, JwtTypeValidator.ACCESS_TOKEN_INSTANCE);
3862

3963
final JWSHeader jwsHeader = JWSHeader.parse(
4064
Map.of("typ", randomAlphaOfLengthBetween(4, 8), "alg", randomAlphaOfLengthBetween(3, 8))
4165
);
4266

4367
final IllegalArgumentException e = expectThrows(
4468
IllegalArgumentException.class,
45-
() -> JwtTypeValidator.INSTANCE.validate(jwsHeader, JWTClaimsSet.parse(Map.of()))
69+
() -> validator.validate(jwsHeader, JWTClaimsSet.parse(Map.of()))
4670
);
4771
assertThat(e.getMessage(), containsString("invalid jwt typ header"));
4872
}

0 commit comments

Comments
 (0)