Validate JWT 'typ' header. (#62)

* Validate JWT 'typ' header.

Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
This commit is contained in:
Max Lambrecht 2021-02-08 16:05:36 -03:00 committed by GitHub
parent e33417b10b
commit 0ee9ae28fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 192 additions and 59 deletions

View File

@ -1,6 +1,7 @@
package io.spiffe.svid.jwtsvid; package io.spiffe.svid.jwtsvid;
import com.nimbusds.jose.JOSEException; import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JOSEObjectType;
import com.nimbusds.jose.JWSAlgorithm; import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSHeader; import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jose.JWSVerifier; import com.nimbusds.jose.JWSVerifier;
@ -8,12 +9,12 @@ import com.nimbusds.jose.crypto.ECDSAVerifier;
import com.nimbusds.jose.crypto.RSASSAVerifier; import com.nimbusds.jose.crypto.RSASSAVerifier;
import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT; import com.nimbusds.jwt.SignedJWT;
import io.spiffe.internal.JwtSignatureAlgorithm;
import io.spiffe.bundle.BundleSource; import io.spiffe.bundle.BundleSource;
import io.spiffe.bundle.jwtbundle.JwtBundle; import io.spiffe.bundle.jwtbundle.JwtBundle;
import io.spiffe.exception.AuthorityNotFoundException; import io.spiffe.exception.AuthorityNotFoundException;
import io.spiffe.exception.BundleNotFoundException; import io.spiffe.exception.BundleNotFoundException;
import io.spiffe.exception.JwtSvidException; import io.spiffe.exception.JwtSvidException;
import io.spiffe.internal.JwtSignatureAlgorithm;
import io.spiffe.spiffeid.SpiffeId; import io.spiffe.spiffeid.SpiffeId;
import lombok.NonNull; import lombok.NonNull;
import lombok.Value; import lombok.Value;
@ -62,11 +63,14 @@ public class JwtSvid {
*/ */
String token; String token;
public static final String HEADER_TYP_JWT = "JWT";
public static final String HEADER_TYP_JOSE = "JOSE";
private JwtSvid(final SpiffeId spiffeId, private JwtSvid(final SpiffeId spiffeId,
final Set<String> audience, final Set<String> audience,
final Date expiry, final Date expiry,
final Map<String, Object> claims, final Map<String, Object> claims,
final String token) { final String token) {
this.spiffeId = spiffeId; this.spiffeId = spiffeId;
this.audience = audience; this.audience = audience;
this.expiry = expiry; this.expiry = expiry;
@ -85,18 +89,18 @@ public class JwtSvid {
* @param audience audience as a list of strings used to validate the 'aud' claim * @param audience audience as a list of strings used to validate the 'aud' claim
* @return an instance of a {@link JwtSvid} with a SPIFFE ID parsed from the 'sub', audience from 'aud', and expiry * @return an instance of a {@link JwtSvid} with a SPIFFE ID parsed from the 'sub', audience from 'aud', and expiry
* from 'exp' claim. * from 'exp' claim.
* @throws JwtSvidException when the token expired or the expiration claim is missing, * @throws JwtSvidException when the token expired or the expiration claim is missing,
* when the algorithm is not supported (See {@link JwtSignatureAlgorithm}), * when the algorithm is not supported (See {@link JwtSignatureAlgorithm}),
* when the header 'kid' is missing, * when the header 'kid' is missing,
* when the signature cannot be verified, or * when the header 'typ' is present and is not 'JWT' or 'JOSE'
* when the 'aud' claim has an audience that is not in the audience list * when the signature cannot be verified,
* provided as parameter * when the 'aud' claim has an audience that is not in the audience list
* @throws IllegalArgumentException when the token is blank or cannot be parsed * provided as parameter
* @throws BundleNotFoundException if the bundle for the trust domain of the spiffe id from the 'sub' * @throws IllegalArgumentException when the token is blank or cannot be parsed
* cannot be found * @throws BundleNotFoundException if the bundle for the trust domain of the spiffe id from the 'sub'
* in the JwtBundleSource * cannot be found in the JwtBundleSource
* @throws AuthorityNotFoundException if the authority cannot be found in the bundle using the value from * @throws AuthorityNotFoundException if the authority cannot be found in the bundle using the value from
* the 'kid' header * the 'kid' header
*/ */
public static JwtSvid parseAndValidate(@NonNull final String token, public static JwtSvid parseAndValidate(@NonNull final String token,
@NonNull final BundleSource<JwtBundle> jwtBundleSource, @NonNull final BundleSource<JwtBundle> jwtBundleSource,
@ -108,6 +112,8 @@ public class JwtSvid {
} }
val signedJwt = getSignedJWT(token); val signedJwt = getSignedJWT(token);
validateTypeHeader(signedJwt.getHeader());
JwtSignatureAlgorithm algorithm = parseAlgorithm(signedJwt.getHeader().getAlgorithm()); JwtSignatureAlgorithm algorithm = parseAlgorithm(signedJwt.getHeader().getAlgorithm());
val claimsSet = getJwtClaimsSet(signedJwt); val claimsSet = getJwtClaimsSet(signedJwt);
@ -137,10 +143,11 @@ public class JwtSvid {
* @param audience audience as a list of strings used to validate the 'aud' claim * @param audience audience as a list of strings used to validate the 'aud' claim
* @return an instance of a {@link JwtSvid} with a SPIFFE ID parsed from the 'sub', audience from 'aud', and expiry * @return an instance of a {@link JwtSvid} with a SPIFFE ID parsed from the 'sub', audience from 'aud', and expiry
* from 'exp' claim. * from 'exp' claim.
* @throws JwtSvidException when the token expired or the expiration claim is missing, or when * @throws JwtSvidException when the token expired or the expiration claim is missing,
* the 'aud' has an audience that is not in the audience provided as parameter, * when the 'aud' has an audience that is not in the audience provided as parameter,
* or when the 'alg' is not supported (See {@link JwtSignatureAlgorithm}). * when the 'alg' is not supported (See {@link JwtSignatureAlgorithm}),
* @throws IllegalArgumentException when the token cannot be parsed * when the header 'typ' is present and is not 'JWT' or 'JOSE'.
* @throws IllegalArgumentException when the token cannot be parsed
*/ */
public static JwtSvid parseInsecure(@NonNull final String token, @NonNull final Set<String> audience) throws JwtSvidException { public static JwtSvid parseInsecure(@NonNull final String token, @NonNull final Set<String> audience) throws JwtSvidException {
if (StringUtils.isBlank(token)) { if (StringUtils.isBlank(token)) {
@ -148,6 +155,8 @@ public class JwtSvid {
} }
val signedJwt = getSignedJWT(token); val signedJwt = getSignedJWT(token);
validateTypeHeader(signedJwt.getHeader());
parseAlgorithm(signedJwt.getHeader().getAlgorithm()); parseAlgorithm(signedJwt.getHeader().getAlgorithm());
val claimsSet = getJwtClaimsSet(signedJwt); val claimsSet = getJwtClaimsSet(signedJwt);
@ -290,7 +299,7 @@ public class JwtSvid {
private static JwtSignatureAlgorithm parseAlgorithm(JWSAlgorithm algorithm) throws JwtSvidException { private static JwtSignatureAlgorithm parseAlgorithm(JWSAlgorithm algorithm) throws JwtSvidException {
if (algorithm == null) { if (algorithm == null) {
throw new JwtSvidException("jwt header 'alg' is required"); throw new JwtSvidException("JWT header 'alg' is required");
} }
try { try {
@ -299,4 +308,16 @@ public class JwtSvid {
throw new JwtSvidException(e.getMessage(), e); throw new JwtSvidException(e.getMessage(), e);
} }
} }
private static void validateTypeHeader(JWSHeader headers) throws JwtSvidException {
final JOSEObjectType type = headers.getType();
// if it's not present -> OK
if (type == null || StringUtils.isBlank(type.toString())) {
return;
}
final String typValue = type.toString();
if (!HEADER_TYP_JWT.equals(typValue) && !HEADER_TYP_JOSE.equals(typValue)) {
throw new JwtSvidException(String.format("If JWT header 'typ' is present, it must be either 'JWT' or 'JOSE'. Got: '%s'.", type.toString()));
}
}
} }

View File

@ -26,6 +26,7 @@ import java.util.stream.Stream;
import static io.spiffe.svid.jwtsvid.JwtSvidParseInsecureTest.newJwtSvidInstance; import static io.spiffe.svid.jwtsvid.JwtSvidParseInsecureTest.newJwtSvidInstance;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
class JwtSvidParseAndValidateTest { class JwtSvidParseAndValidateTest {
@ -34,9 +35,8 @@ class JwtSvidParseAndValidateTest {
"dCI6MTUxNjIzOTAyMiwiYXVkIjoiYXVkaWVuY2UifQ.wNm5pQGSLCw5N9ddgSF2hkgmQpGnG9le_gpiFmyBhao"; "dCI6MTUxNjIzOTAyMiwiYXVkIjoiYXVkaWVuY2UifQ.wNm5pQGSLCw5N9ddgSF2hkgmQpGnG9le_gpiFmyBhao";
@ParameterizedTest @ParameterizedTest
@MethodSource("provideJwtScenarios") @MethodSource("provideSuccessScenarios")
void parseAndValidateJwt(TestCase testCase) { void parseAndValidateValidJwt(TestCase testCase) {
try { try {
String token = testCase.generateToken.get(); String token = testCase.generateToken.get();
JwtSvid jwtSvid = JwtSvid.parseAndValidate(token, testCase.jwtBundle, testCase.audience); JwtSvid jwtSvid = JwtSvid.parseAndValidate(token, testCase.jwtBundle, testCase.audience);
@ -46,6 +46,18 @@ class JwtSvidParseAndValidateTest {
assertEquals(testCase.expectedJwtSvid.getExpiry().toInstant().getEpochSecond(), jwtSvid.getExpiry().toInstant().getEpochSecond()); assertEquals(testCase.expectedJwtSvid.getExpiry().toInstant().getEpochSecond(), jwtSvid.getExpiry().toInstant().getEpochSecond());
assertEquals(token, jwtSvid.getToken()); assertEquals(token, jwtSvid.getToken());
assertEquals(token, jwtSvid.marshal()); assertEquals(token, jwtSvid.marshal());
} catch (Exception e) {
fail(e);
}
}
@ParameterizedTest
@MethodSource("provideFailureScenarios")
void parseAndValidateInvalidJwt(TestCase testCase) {
try {
String token = testCase.generateToken.get();
JwtSvid.parseAndValidate(token, testCase.jwtBundle, testCase.audience);
fail("expected error: " + testCase.expectedException.getMessage());
} catch (Exception e) { } catch (Exception e) {
assertEquals(testCase.expectedException.getClass(), e.getClass()); assertEquals(testCase.expectedException.getClass(), e.getClass());
assertEquals(testCase.expectedException.getMessage(), e.getMessage()); assertEquals(testCase.expectedException.getMessage(), e.getMessage());
@ -100,7 +112,7 @@ class JwtSvidParseAndValidateTest {
} }
} }
static Stream<Arguments> provideJwtScenarios() { static Stream<Arguments> provideSuccessScenarios() {
KeyPair key1 = TestUtils.generateECKeyPair(Curve.P_521); KeyPair key1 = TestUtils.generateECKeyPair(Curve.P_521);
KeyPair key2 = TestUtils.generateECKeyPair(Curve.P_521); KeyPair key2 = TestUtils.generateECKeyPair(Curve.P_521);
KeyPair key3 = TestUtils.generateRSAKeyPair(2048); KeyPair key3 = TestUtils.generateRSAKeyPair(2048);
@ -112,126 +124,166 @@ class JwtSvidParseAndValidateTest {
jwtBundle.putJwtAuthority("authority3", key3.getPublic()); jwtBundle.putJwtAuthority("authority3", key3.getPublic());
SpiffeId spiffeId = trustDomain.newSpiffeId("host"); SpiffeId spiffeId = trustDomain.newSpiffeId("host");
Date expiration = new Date(System.currentTimeMillis() + 3600000); Date expiration = new Date(System.currentTimeMillis() + (60 * 60 * 1000));
Set<String> audience = new HashSet<String>() {{add("audience1"); add("audience2");}}; Set<String> audience = new HashSet<String>() {{add("audience1"); add("audience2");}};
JWTClaimsSet claims = TestUtils.buildJWTClaimSet(audience, spiffeId.toString(), expiration); JWTClaimsSet claims = TestUtils.buildJWTClaimSet(audience, spiffeId.toString(), expiration);
return Stream.of( return Stream.of(
Arguments.of(TestCase.builder() Arguments.of(TestCase.builder()
.name("1. success using EC signature") .name("using EC signature")
.jwtBundle(jwtBundle) .jwtBundle(jwtBundle)
.expectedAudience(Collections.singleton("audience1")) .expectedAudience(Collections.singleton("audience1"))
.generateToken(() -> TestUtils.generateToken(claims, key1, "authority1")) .generateToken(() -> TestUtils.generateToken(claims, key1, "authority1", JwtSvid.HEADER_TYP_JOSE))
.expectedException(null) .expectedException(null)
.expectedJwtSvid(newJwtSvidInstance( .expectedJwtSvid(newJwtSvidInstance(
trustDomain.newSpiffeId("host"), trustDomain.newSpiffeId("host"),
audience, audience,
expiration, expiration,
claims.getClaims(), TestUtils.generateToken(claims, key1, "authority1") )) claims.getClaims(), TestUtils.generateToken(claims, key1, "authority1", JwtSvid.HEADER_TYP_JOSE) ))
.build()), .build()),
Arguments.of(TestCase.builder() Arguments.of(TestCase.builder()
.name("2. success using RSA signature") .name("using RSA signature")
.jwtBundle(jwtBundle) .jwtBundle(jwtBundle)
.expectedAudience(audience) .expectedAudience(audience)
.generateToken(() -> TestUtils.generateToken(claims, key3, "authority3")) .generateToken(() -> TestUtils.generateToken(claims, key3, "authority3", JwtSvid.HEADER_TYP_JWT))
.expectedException(null)
.expectedJwtSvid(newJwtSvidInstance(
trustDomain.newSpiffeId("host"),
audience,
expiration,
claims.getClaims(), TestUtils.generateToken(claims, key3, "authority3", JwtSvid.HEADER_TYP_JWT)))
.build()),
Arguments.of(TestCase.builder()
.name("using empty typ")
.jwtBundle(jwtBundle)
.expectedAudience(audience)
.generateToken(() -> TestUtils.generateToken(claims, key3, "authority3", ""))
.expectedException(null) .expectedException(null)
.expectedJwtSvid(newJwtSvidInstance( .expectedJwtSvid(newJwtSvidInstance(
trustDomain.newSpiffeId("host"), trustDomain.newSpiffeId("host"),
audience, audience,
expiration, expiration,
claims.getClaims(), TestUtils.generateToken(claims, key3, "authority3"))) claims.getClaims(), TestUtils.generateToken(claims, key3, "authority3")))
.build()), .build())
);
}
static Stream<Arguments> provideFailureScenarios() {
KeyPair key1 = TestUtils.generateECKeyPair(Curve.P_521);
KeyPair key2 = TestUtils.generateECKeyPair(Curve.P_521);
KeyPair key3 = TestUtils.generateRSAKeyPair(2048);
TrustDomain trustDomain = TrustDomain.of("test.domain");
JwtBundle jwtBundle = new JwtBundle(trustDomain);
jwtBundle.putJwtAuthority("authority1", key1.getPublic());
jwtBundle.putJwtAuthority("authority2", key2.getPublic());
jwtBundle.putJwtAuthority("authority3", key3.getPublic());
SpiffeId spiffeId = trustDomain.newSpiffeId("host");
Date expiration = new Date(System.currentTimeMillis() + (60 * 60 * 1000));
Set<String> audience = new HashSet<String>() {{add("audience1"); add("audience2");}};
JWTClaimsSet claims = TestUtils.buildJWTClaimSet(audience, spiffeId.toString(), expiration);
return Stream.of(
Arguments.of(TestCase.builder() Arguments.of(TestCase.builder()
.name("3. malformed") .name("malformed")
.jwtBundle(jwtBundle) .jwtBundle(jwtBundle)
.expectedAudience(audience) .expectedAudience(audience)
.generateToken(() -> "invalid token") .generateToken(() -> "invalid token")
.expectedException(new IllegalArgumentException("Unable to parse JWT token")) .expectedException(new IllegalArgumentException("Unable to parse JWT token"))
.build()), .build()),
Arguments.of(TestCase.builder() Arguments.of(TestCase.builder()
.name("4. unsupported algorithm") .name("unsupported algorithm")
.jwtBundle(jwtBundle) .jwtBundle(jwtBundle)
.expectedAudience(Collections.singleton("audience")) .expectedAudience(Collections.singleton("audience"))
.generateToken(() -> HS256TOKEN) .generateToken(() -> HS256TOKEN)
.expectedException(new JwtSvidException("Unsupported JWT algorithm: HS256")) .expectedException(new JwtSvidException("Unsupported JWT algorithm: HS256"))
.build()), .build()),
Arguments.of(TestCase.builder() Arguments.of(TestCase.builder()
.name("5. missing subject") .name("missing subject")
.jwtBundle(jwtBundle) .jwtBundle(jwtBundle)
.expectedAudience(audience) .expectedAudience(audience)
.generateToken(() -> TestUtils.generateToken(TestUtils.buildJWTClaimSet(audience, "", expiration), key1, "authority1")) .generateToken(() -> TestUtils.generateToken(TestUtils.buildJWTClaimSet(audience, "", expiration), key1, "authority1"))
.expectedException(new JwtSvidException("Token missing subject claim")) .expectedException(new JwtSvidException("Token missing subject claim"))
.build()), .build()),
Arguments.of(TestCase.builder() Arguments.of(TestCase.builder()
.name("6. missing expiration") .name("missing expiration")
.jwtBundle(jwtBundle) .jwtBundle(jwtBundle)
.expectedAudience(audience) .expectedAudience(audience)
.generateToken(() -> TestUtils.generateToken(TestUtils.buildJWTClaimSet(audience, spiffeId.toString(), null), key1, "authority1")) .generateToken(() -> TestUtils.generateToken(TestUtils.buildJWTClaimSet(audience, spiffeId.toString(), null), key1, "authority1"))
.expectedException(new JwtSvidException("Token missing expiration claim")) .expectedException(new JwtSvidException("Token missing expiration claim"))
.build()), .build()),
Arguments.of(TestCase.builder() Arguments.of(TestCase.builder()
.name("7. token has expired") .name("token has expired")
.jwtBundle(jwtBundle) .jwtBundle(jwtBundle)
.expectedAudience(audience) .expectedAudience(audience)
.generateToken(() -> TestUtils.generateToken(TestUtils.buildJWTClaimSet(audience, spiffeId.toString(), new Date()), key1, "authority1")) .generateToken(() -> TestUtils.generateToken(TestUtils.buildJWTClaimSet(audience, spiffeId.toString(), new Date()), key1, "authority1"))
.expectedException(new JwtSvidException("Token has expired")) .expectedException(new JwtSvidException("Token has expired"))
.build()), .build()),
Arguments.of(TestCase.builder() Arguments.of(TestCase.builder()
.name("8. unexpected audience") .name("unexpected audience")
.jwtBundle(jwtBundle) .jwtBundle(jwtBundle)
.expectedAudience(Collections.singleton("another")) .expectedAudience(Collections.singleton("another"))
.generateToken(() -> TestUtils.generateToken(claims, key1, "authority1")) .generateToken(() -> TestUtils.generateToken(claims, key1, "authority1"))
.expectedException(new JwtSvidException("expected audience in [another] (audience=[audience2, audience1])")) .expectedException(new JwtSvidException("expected audience in [another] (audience=[audience2, audience1])"))
.build()), .build()),
Arguments.of(TestCase.builder() Arguments.of(TestCase.builder()
.name("9. invalid subject claim") .name("invalid subject claim")
.jwtBundle(jwtBundle) .jwtBundle(jwtBundle)
.expectedAudience(audience) .expectedAudience(audience)
.generateToken(() -> TestUtils.generateToken(TestUtils.buildJWTClaimSet(audience, "non-spiffe-subject", expiration), key1, "authority1")) .generateToken(() -> TestUtils.generateToken(TestUtils.buildJWTClaimSet(audience, "non-spiffe-subject", expiration), key1, "authority1"))
.expectedException(new JwtSvidException("Subject non-spiffe-subject cannot be parsed as a SPIFFE ID")) .expectedException(new JwtSvidException("Subject non-spiffe-subject cannot be parsed as a SPIFFE ID"))
.build()), .build()),
Arguments.of(TestCase.builder() Arguments.of(TestCase.builder()
.name("10. missing key id") .name("missing key id")
.jwtBundle(jwtBundle) .jwtBundle(jwtBundle)
.expectedAudience(audience) .expectedAudience(audience)
.generateToken(() -> TestUtils.generateToken(claims, key1, null)) .generateToken(() -> TestUtils.generateToken(claims, key1, null))
.expectedException(new JwtSvidException("Token header missing key id")) .expectedException(new JwtSvidException("Token header missing key id"))
.build()), .build()),
Arguments.of(TestCase.builder() Arguments.of(TestCase.builder()
.name("11. key id contains an empty value") .name("key id contains an empty value")
.jwtBundle(jwtBundle) .jwtBundle(jwtBundle)
.expectedAudience(audience) .expectedAudience(audience)
.generateToken(() -> TestUtils.generateToken(claims, key1, " ")) .generateToken(() -> TestUtils.generateToken(claims, key1, " "))
.expectedException(new JwtSvidException("Token header key id contains an empty value")) .expectedException(new JwtSvidException("Token header key id contains an empty value"))
.build()), .build()),
Arguments.of(TestCase.builder() Arguments.of(TestCase.builder()
.name("12. no bundle for trust domain") .name("no bundle for trust domain")
.jwtBundle(new JwtBundle(TrustDomain.of("other.domain"))) .jwtBundle(new JwtBundle(TrustDomain.of("other.domain")))
.expectedAudience(audience) .expectedAudience(audience)
.generateToken(() -> TestUtils.generateToken(claims, key1, "authority1")) .generateToken(() -> TestUtils.generateToken(claims, key1, "authority1"))
.expectedException(new BundleNotFoundException("No JWT bundle found for trust domain test.domain")) .expectedException(new BundleNotFoundException("No JWT bundle found for trust domain test.domain"))
.build()), .build()),
Arguments.of(TestCase.builder() Arguments.of(TestCase.builder()
.name("13. no authority found for key id") .name("no authority found for key id")
.jwtBundle(new JwtBundle(TrustDomain.of("test.domain"))) .jwtBundle(new JwtBundle(TrustDomain.of("test.domain")))
.expectedAudience(audience) .expectedAudience(audience)
.generateToken(() -> TestUtils.generateToken(claims, key1, "authority1")) .generateToken(() -> TestUtils.generateToken(claims, key1, "authority1"))
.expectedException(new AuthorityNotFoundException("No authority found for the trust domain test.domain and key id authority1")) .expectedException(new AuthorityNotFoundException("No authority found for the trust domain test.domain and key id authority1"))
.build()), .build()),
Arguments.of(TestCase.builder() Arguments.of(TestCase.builder()
.name("14. signature cannot be verified with authority") .name("signature cannot be verified with authority")
.jwtBundle(jwtBundle) .jwtBundle(jwtBundle)
.expectedAudience(audience) .expectedAudience(audience)
.generateToken(() -> TestUtils.generateToken(claims, key2, "authority1")) .generateToken(() -> TestUtils.generateToken(claims, key2, "authority1"))
.expectedException(new JwtSvidException("Signature invalid: cannot be verified with the authority with keyId=authority1")) .expectedException(new JwtSvidException("Signature invalid: cannot be verified with the authority with keyId=authority1"))
.build()), .build()),
Arguments.of(TestCase.builder() Arguments.of(TestCase.builder()
.name("15. authority algorithm mismatch") .name("authority algorithm mismatch")
.jwtBundle(jwtBundle) .jwtBundle(jwtBundle)
.expectedAudience(audience) .expectedAudience(audience)
.generateToken(() -> TestUtils.generateToken(claims, key3, "authority1")) .generateToken(() -> TestUtils.generateToken(claims, key3, "authority1"))
.expectedException(new JwtSvidException("Error verifying signature with the authority with keyId=authority1")) .expectedException(new JwtSvidException("Error verifying signature with the authority with keyId=authority1"))
.build()),
Arguments.of(TestCase.builder()
.name("not valid header 'typ'")
.jwtBundle(jwtBundle)
.expectedAudience(audience)
.generateToken(() -> TestUtils.generateToken(claims, key1, "authority1", "OTHER"))
.expectedException(new JwtSvidException("If JWT header 'typ' is present, it must be either 'JWT' or 'JOSE'. Got: 'OTHER'."))
.build()) .build())
); );
} }

View File

@ -25,6 +25,7 @@ import java.util.function.Supplier;
import java.util.stream.Stream; import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
class JwtSvidParseInsecureTest { class JwtSvidParseInsecureTest {
@ -33,9 +34,8 @@ class JwtSvidParseInsecureTest {
"dCI6MTUxNjIzOTAyMiwiYXVkIjoiYXVkaWVuY2UifQ.wNm5pQGSLCw5N9ddgSF2hkgmQpGnG9le_gpiFmyBhao"; "dCI6MTUxNjIzOTAyMiwiYXVkIjoiYXVkaWVuY2UifQ.wNm5pQGSLCw5N9ddgSF2hkgmQpGnG9le_gpiFmyBhao";
@ParameterizedTest @ParameterizedTest
@MethodSource("provideJwtScenarios") @MethodSource("provideSuccessScenarios")
void parseJwt(TestCase testCase) { void parseValidJwt(TestCase testCase) {
try { try {
String token = testCase.generateToken.get(); String token = testCase.generateToken.get();
JwtSvid jwtSvid = JwtSvid.parseInsecure(token, testCase.audience); JwtSvid jwtSvid = JwtSvid.parseInsecure(token, testCase.audience);
@ -44,6 +44,18 @@ class JwtSvidParseInsecureTest {
assertEquals(testCase.expectedJwtSvid.getAudience(), jwtSvid.getAudience()); assertEquals(testCase.expectedJwtSvid.getAudience(), jwtSvid.getAudience());
assertEquals(testCase.expectedJwtSvid.getExpiry().toInstant().getEpochSecond(), jwtSvid.getExpiry().toInstant().getEpochSecond()); assertEquals(testCase.expectedJwtSvid.getExpiry().toInstant().getEpochSecond(), jwtSvid.getExpiry().toInstant().getEpochSecond());
assertEquals(token, jwtSvid.getToken()); assertEquals(token, jwtSvid.getToken());
} catch (Exception e) {
fail(e);
}
}
@ParameterizedTest
@MethodSource("provideFailureScenarios")
void parseInvalidJwt(TestCase testCase) {
try {
String token = testCase.generateToken.get();
JwtSvid.parseInsecure(token, testCase.audience);
fail("expected error: " + testCase.expectedException.getMessage());
} catch (Exception e) { } catch (Exception e) {
assertEquals(testCase.expectedException.getClass(), e.getClass()); assertEquals(testCase.expectedException.getClass(), e.getClass());
assertEquals(testCase.expectedException.getMessage(), e.getMessage()); assertEquals(testCase.expectedException.getMessage(), e.getMessage());
@ -89,7 +101,7 @@ class JwtSvidParseInsecureTest {
} }
} }
static Stream<Arguments> provideJwtScenarios() { static Stream<Arguments> provideSuccessScenarios() {
KeyPair key1 = TestUtils.generateECKeyPair(Curve.P_521); KeyPair key1 = TestUtils.generateECKeyPair(Curve.P_521);
KeyPair key2 = TestUtils.generateECKeyPair(Curve.P_521); KeyPair key2 = TestUtils.generateECKeyPair(Curve.P_521);
@ -106,16 +118,56 @@ class JwtSvidParseInsecureTest {
return Stream.of( return Stream.of(
Arguments.of(TestCase.builder() Arguments.of(TestCase.builder()
.name("success") .name("using typ as JWT")
.expectedAudience(audience) .expectedAudience(audience)
.generateToken(() -> TestUtils.generateToken(claims, key1, "authority1")) .generateToken(() -> TestUtils.generateToken(claims, key1, "authority1", JwtSvid.HEADER_TYP_JWT))
.expectedException(null) .expectedException(null)
.expectedJwtSvid(newJwtSvidInstance( .expectedJwtSvid(newJwtSvidInstance(
trustDomain.newSpiffeId("host"), trustDomain.newSpiffeId("host"),
audience, audience,
expiration, expiration,
claims.getClaims(), TestUtils.generateToken(claims, key1, "authority1"))) claims.getClaims(), TestUtils.generateToken(claims, key1, "authority1", JwtSvid.HEADER_TYP_JWT)))
.build()), .build()),
Arguments.of(TestCase.builder()
.name("using typ as JOSE")
.expectedAudience(audience)
.generateToken(() -> TestUtils.generateToken(claims, key1, "authority1", JwtSvid.HEADER_TYP_JOSE))
.expectedException(null)
.expectedJwtSvid(newJwtSvidInstance(
trustDomain.newSpiffeId("host"),
audience,
expiration,
claims.getClaims(), TestUtils.generateToken(claims, key1, "authority1", JwtSvid.HEADER_TYP_JWT)))
.build()),
Arguments.of(TestCase.builder()
.name("using empty typ")
.expectedAudience(audience)
.generateToken(() -> TestUtils.generateToken(claims, key1, "authority1", ""))
.expectedException(null)
.expectedJwtSvid(newJwtSvidInstance(
trustDomain.newSpiffeId("host"),
audience,
expiration,
claims.getClaims(), TestUtils.generateToken(claims, key1, "authority1", "")))
.build()));
}
static Stream<Arguments> provideFailureScenarios() {
KeyPair key1 = TestUtils.generateECKeyPair(Curve.P_521);
KeyPair key2 = TestUtils.generateECKeyPair(Curve.P_521);
TrustDomain trustDomain = TrustDomain.of("test.domain");
JwtBundle jwtBundle = new JwtBundle(trustDomain);
jwtBundle.putJwtAuthority("authority1", key1.getPublic());
jwtBundle.putJwtAuthority("authority2", key2.getPublic());
SpiffeId spiffeId = trustDomain.newSpiffeId("host");
Date expiration = new Date(System.currentTimeMillis() + 3600000);
Set<String> audience = Collections.singleton("audience");
JWTClaimsSet claims = TestUtils.buildJWTClaimSet(audience, spiffeId.toString(), expiration);
return Stream.of(
Arguments.of(TestCase.builder() Arguments.of(TestCase.builder()
.name("malformed") .name("malformed")
.expectedAudience(audience) .expectedAudience(audience)
@ -153,10 +205,10 @@ class JwtSvidParseInsecureTest {
.expectedException(new JwtSvidException("Subject non-spiffe-subject cannot be parsed as a SPIFFE ID")) .expectedException(new JwtSvidException("Subject non-spiffe-subject cannot be parsed as a SPIFFE ID"))
.build()), .build()),
Arguments.of(TestCase.builder() Arguments.of(TestCase.builder()
.name("unsupported algorithm") .name("not valid header 'typ'")
.expectedAudience(Collections.singleton("audience")) .expectedAudience(audience)
.generateToken(() -> HS256TOKEN) .generateToken(() -> TestUtils.generateToken(claims, key1, "authority1", "OTHER"))
.expectedException(new JwtSvidException("Unsupported JWT algorithm: HS256")) .expectedException(new JwtSvidException("If JWT header 'typ' is present, it must be either 'JWT' or 'JOSE'. Got: 'OTHER'."))
.build()) .build())
); );
} }

View File

@ -1,6 +1,7 @@
package io.spiffe.utils; package io.spiffe.utils;
import com.nimbusds.jose.JOSEException; import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JOSEObjectType;
import com.nimbusds.jose.JWSAlgorithm; import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSHeader; import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jose.JWSSigner; import com.nimbusds.jose.JWSSigner;
@ -9,6 +10,7 @@ import com.nimbusds.jose.crypto.RSASSASigner;
import com.nimbusds.jose.jwk.Curve; import com.nimbusds.jose.jwk.Curve;
import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT; import com.nimbusds.jwt.SignedJWT;
import io.spiffe.svid.jwtsvid.JwtSvid;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
@ -57,10 +59,14 @@ public class TestUtils {
public static String generateToken(Map<String, Object> claims, KeyPair keyPair, String keyId) { public static String generateToken(Map<String, Object> claims, KeyPair keyPair, String keyId) {
JWTClaimsSet jwtClaimsSet = buildJWTClaimSetFromClaimsMap(claims); JWTClaimsSet jwtClaimsSet = buildJWTClaimSetFromClaimsMap(claims);
return generateToken(jwtClaimsSet, keyPair, keyId); return generateToken(jwtClaimsSet, keyPair, keyId, JwtSvid.HEADER_TYP_JWT);
} }
public static String generateToken(JWTClaimsSet claims, KeyPair keyPair, String keyId) { public static String generateToken(JWTClaimsSet claims, KeyPair keyPair, String keyId) {
return generateToken(claims, keyPair, keyId, JwtSvid.HEADER_TYP_JWT);
}
public static String generateToken(JWTClaimsSet claims, KeyPair keyPair, String keyId, String typ) {
try { try {
JWSAlgorithm algorithm; JWSAlgorithm algorithm;
JWSSigner signer; JWSSigner signer;
@ -74,7 +80,9 @@ public class TestUtils {
throw new IllegalArgumentException("Algorithm not supported"); throw new IllegalArgumentException("Algorithm not supported");
} }
SignedJWT signedJWT = new SignedJWT(new JWSHeader.Builder(algorithm).keyID(keyId).build(), claims); final JOSEObjectType joseTyp = new JOSEObjectType(typ);
final JWSHeader header = new JWSHeader.Builder(algorithm).keyID(keyId).type(joseTyp).build();
SignedJWT signedJWT = new SignedJWT(header, claims);
signedJWT.sign(signer); signedJWT.sign(signer);
return signedJWT.serialize(); return signedJWT.serialize();
} catch (JOSEException e) { } catch (JOSEException e) {