Skip to content

Add ability to verify audience contains at least one of those expected #472

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Feb 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 32 additions & 14 deletions lib/src/main/java/com/auth0/jwt/JWTVerifier.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import com.auth0.jwt.interfaces.Verification;

import java.util.*;
import java.util.stream.Collectors;

/**
* The JWTVerifier class holds the verify method to assert that a given Token has not only a proper JWT format, but also it's signature matches.
Expand All @@ -24,6 +25,9 @@ public final class JWTVerifier implements com.auth0.jwt.interfaces.JWTVerifier {
private final Clock clock;
private final JWTParser parser;

static final String AUDIENCE_EXACT = "AUDIENCE_EXACT";
static final String AUDIENCE_CONTAINS = "AUDIENCE_CONTAINS";

JWTVerifier(Algorithm algorithm, Map<String, Object> claims, Clock clock) {
this.algorithm = algorithm;
this.claims = Collections.unmodifiableMap(claims);
Expand Down Expand Up @@ -72,7 +76,15 @@ public Verification withSubject(String subject) {

@Override
public Verification withAudience(String... audience) {
requireClaim(PublicClaims.AUDIENCE, isNullOrEmpty(audience) ? null : Arrays.asList(audience));
claims.remove(AUDIENCE_CONTAINS);
requireClaim(AUDIENCE_EXACT, isNullOrEmpty(audience) ? null : Arrays.asList(audience));
return this;
}

@Override
public Verification withAnyOfAudience(String... audience) {
claims.remove(AUDIENCE_EXACT);
requireClaim(AUDIENCE_CONTAINS, isNullOrEmpty(audience) ? null : Arrays.asList(audience));
return this;
}

Expand Down Expand Up @@ -305,31 +317,36 @@ private void verifyClaims(DecodedJWT jwt, Map<String, Object> claims) throws Tok
}
}

private void verifyClaimValues(DecodedJWT jwt, Map.Entry<String, Object> entry) {
switch (entry.getKey()) {
case PublicClaims.AUDIENCE:
assertValidAudienceClaim(jwt.getAudience(), (List<String>) entry.getValue());
private void verifyClaimValues(DecodedJWT jwt, Map.Entry<String, Object> expectedClaim) {
switch (expectedClaim.getKey()) {
// We use custom keys for audience in the expected claims to differentiate between validating that the audience
// contains all expected values, or validating that the audience contains at least one of the expected values.
case AUDIENCE_EXACT:
assertValidAudienceClaim(jwt.getAudience(), (List<String>) expectedClaim.getValue(), true);
break;
case AUDIENCE_CONTAINS:
assertValidAudienceClaim(jwt.getAudience(), (List<String>) expectedClaim.getValue(), false);
break;
case PublicClaims.EXPIRES_AT:
assertValidDateClaim(jwt.getExpiresAt(), (Long) entry.getValue(), true);
assertValidDateClaim(jwt.getExpiresAt(), (Long) expectedClaim.getValue(), true);
break;
case PublicClaims.ISSUED_AT:
assertValidDateClaim(jwt.getIssuedAt(), (Long) entry.getValue(), false);
assertValidDateClaim(jwt.getIssuedAt(), (Long) expectedClaim.getValue(), false);
break;
case PublicClaims.NOT_BEFORE:
assertValidDateClaim(jwt.getNotBefore(), (Long) entry.getValue(), false);
assertValidDateClaim(jwt.getNotBefore(), (Long) expectedClaim.getValue(), false);
break;
case PublicClaims.ISSUER:
assertValidIssuerClaim(jwt.getIssuer(), (List<String>) entry.getValue());
assertValidIssuerClaim(jwt.getIssuer(), (List<String>) expectedClaim.getValue());
break;
case PublicClaims.JWT_ID:
assertValidStringClaim(entry.getKey(), jwt.getId(), (String) entry.getValue());
assertValidStringClaim(expectedClaim.getKey(), jwt.getId(), (String) expectedClaim.getValue());
break;
case PublicClaims.SUBJECT:
assertValidStringClaim(entry.getKey(), jwt.getSubject(), (String) entry.getValue());
assertValidStringClaim(expectedClaim.getKey(), jwt.getSubject(), (String) expectedClaim.getValue());
break;
default:
assertValidClaim(jwt.getClaim(entry.getKey()), entry.getKey(), entry.getValue());
assertValidClaim(jwt.getClaim(expectedClaim.getKey()), expectedClaim.getKey(), expectedClaim.getValue());
break;
}
}
Expand Down Expand Up @@ -411,8 +428,9 @@ private void assertDateIsPast(Date date, long leeway, Date today) {
}
}

private void assertValidAudienceClaim(List<String> audience, List<String> value) {
if (audience == null || !audience.containsAll(value)) {
private void assertValidAudienceClaim(List<String> audience, List<String> values, boolean shouldContainAll) {
if (audience == null || (shouldContainAll && !audience.containsAll(values)) ||
(!shouldContainAll && Collections.disjoint(audience, values))) {
throw new InvalidClaimException("The Claim 'aud' value doesn't contain the required audience.");
}
}
Expand Down
26 changes: 25 additions & 1 deletion lib/src/main/java/com/auth0/jwt/interfaces/Verification.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,37 @@ public interface Verification {
Verification withSubject(String subject);

/**
* Require a specific Audience ("aud") claim.
* Require a specific Audience ("aud") claim. If multiple audiences are specified, they must all be present
* in the "aud" claim.
*
* If this is used in conjunction with {@link #withAnyOfAudience(String...)}, whichever one is configured last will
* determine the audience validation behavior.
*
* @param audience the required Audience value
* @return this same Verification instance.
*/
Verification withAudience(String... audience);

/**
* Require that the Audience ("aud") claim contain at least one of the specified audiences.
*
* If this is used in conjunction with {@link #withAudience(String...)}, whichever one is configured last will
* determine the audience validation behavior.
*
* Note: This method was added after the interface was released.
* It is defined as a default method for compatibility reasons.
* From version 4.0 on, the method will be abstract and all implementations of this interface
* will have to provide their own implementation.
*
* The default implementation throws an {@linkplain UnsupportedOperationException}.
*
* @param audience the required Audience value for which the "aud" claim must contain at least one value.
* @return this same Verification instance.
*/
default Verification withAnyOfAudience(String... audience) {
throw new UnsupportedOperationException("withAnyOfAudience");
}

/**
* Define the default window in seconds in which the Not Before, Issued At and Expires At Claims will still be valid.
* Setting a specific leeway value on a given Claim will override this value for that Claim.
Expand Down
169 changes: 161 additions & 8 deletions lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.Clock;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.auth0.jwt.interfaces.Verification;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
Expand Down Expand Up @@ -110,7 +111,8 @@ public void shouldThrowOnInvalidSubject() throws Exception {
}

@Test
public void shouldValidateAudience() throws Exception {
public void shouldAcceptAudienceWhenWithAudienceContainsAll() throws Exception {
// Token 'aud': ["Mark"]
String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJNYXJrIn0.xWB6czYI0XObbVhLAxe55TwChWZg7zO08RxONWU2iY4";
DecodedJWT jwt = JWTVerifier.init(Algorithm.HMAC256("secret"))
.withAudience("Mark")
Expand All @@ -119,6 +121,7 @@ public void shouldValidateAudience() throws Exception {

assertThat(jwt, is(notNullValue()));

// Token 'aud': ["Mark", "David"]
String tokenArr = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiTWFyayIsIkRhdmlkIl19.6WfbIt8m61f9WlCYIQn5CThvw4UNyC66qrPaoinfssw";
DecodedJWT jwtArr = JWTVerifier.init(Algorithm.HMAC256("secret"))
.withAudience("Mark", "David")
Expand All @@ -129,8 +132,52 @@ public void shouldValidateAudience() throws Exception {
}

@Test
public void shouldAcceptPartialAudience() throws Exception {
//Token 'aud' = ["Mark", "David", "John"]
public void shouldAllowWithAnyOfAudienceVerificationToOverrideWithAudience() {
// Token 'aud' = ["Mark", "David", "John"]
String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiTWFyayIsIkRhdmlkIiwiSm9obiJdfQ.DX5xXiCaYvr54x_iL0LZsJhK7O6HhAdHeDYkgDeb0Rw";
Verification verification = JWTVerifier.init(Algorithm.HMAC256("secret")).withAudience("Mark", "Jim");

Exception exception = null;
try {
verification.build().verify(token);
} catch (Exception e) {
exception = e;

}

assertThat(exception, is(notNullValue()));
assertThat(exception, is(instanceOf(InvalidClaimException.class)));
assertThat(exception.getMessage(), is("The Claim 'aud' value doesn't contain the required audience."));

DecodedJWT jwt = verification.withAnyOfAudience("Mark", "Jim").build().verify(token);
assertThat(jwt, is(notNullValue()));
}

@Test
public void shouldAllowWithAudienceVerificationToOverrideWithAnyOfAudience() {
// Token 'aud' = ["Mark", "David", "John"]
String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiTWFyayIsIkRhdmlkIiwiSm9obiJdfQ.DX5xXiCaYvr54x_iL0LZsJhK7O6HhAdHeDYkgDeb0Rw";
Verification verification = JWTVerifier.init(Algorithm.HMAC256("secret")).withAnyOfAudience("Jim");

Exception exception = null;
try {
verification.build().verify(token);
} catch (Exception e) {
exception = e;

}

assertThat(exception, is(notNullValue()));
assertThat(exception, is(instanceOf(InvalidClaimException.class)));
assertThat(exception.getMessage(), is("The Claim 'aud' value doesn't contain the required audience."));

DecodedJWT jwt = verification.withAudience("Mark").build().verify(token);
assertThat(jwt, is(notNullValue()));
}

@Test
public void shouldAcceptAudienceWhenWithAudienceAndPartialExpected() throws Exception {
// Token 'aud' = ["Mark", "David", "John"]
String tokenArr = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiTWFyayIsIkRhdmlkIiwiSm9obiJdfQ.DX5xXiCaYvr54x_iL0LZsJhK7O6HhAdHeDYkgDeb0Rw";
DecodedJWT jwtArr = JWTVerifier.init(Algorithm.HMAC256("secret"))
.withAudience("John")
Expand All @@ -141,16 +188,69 @@ public void shouldAcceptPartialAudience() throws Exception {
}

@Test
public void shouldThrowOnInvalidAudience() throws Exception {
public void shouldAcceptAudienceWhenAnyOfAudienceAndAllContained() {
// Token 'aud' = ["Mark", "David", "John"]
String tokenArr = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiTWFyayIsIkRhdmlkIiwiSm9obiJdfQ.DX5xXiCaYvr54x_iL0LZsJhK7O6HhAdHeDYkgDeb0Rw";
DecodedJWT jwtArr = JWTVerifier.init(Algorithm.HMAC256("secret"))
.withAnyOfAudience("Mark", "David", "John")
.build()
.verify(tokenArr);

assertThat(jwtArr, is(notNullValue()));
}

@Test
public void shouldThrowWhenAudienceHasNoneOfExpectedAnyOfAudience() {
exception.expect(InvalidClaimException.class);
exception.expectMessage("The Claim 'aud' value doesn't contain the required audience.");

// Token 'aud' = ["Mark", "David", "John"]
String tokenArr = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiTWFyayIsIkRhdmlkIiwiSm9obiJdfQ.DX5xXiCaYvr54x_iL0LZsJhK7O6HhAdHeDYkgDeb0Rw";
DecodedJWT jwtArr = JWTVerifier.init(Algorithm.HMAC256("secret"))
.withAnyOfAudience("Joe", "Jim")
.build()
.verify(tokenArr);
}

@Test
public void shouldThrowWhenAudienceClaimDoesNotContainAllExpected() throws Exception {
exception.expect(InvalidClaimException.class);
exception.expectMessage("The Claim 'aud' value doesn't contain the required audience.");

// Token 'aud' = ["Mark", "David", "John"]
String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiTWFyayIsIkRhdmlkIiwiSm9obiJdfQ.DX5xXiCaYvr54x_iL0LZsJhK7O6HhAdHeDYkgDeb0Rw";
JWTVerifier.init(Algorithm.HMAC256("secret"))
.withAudience("Mark", "Joe")
.build()
.verify(token);
}

@Test
public void shouldThrowWhenAudienceClaimIsNull() throws Exception {
exception.expect(InvalidClaimException.class);
exception.expectMessage("The Claim 'aud' value doesn't contain the required audience.");

// Token 'aud': null
String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.Rq8IxqeX7eA6GgYxlcHdPFVRNFFZc5rEI3MQTZZbK3I";
JWTVerifier.init(Algorithm.HMAC256("secret"))
.withAudience("nope")
.build()
.verify(token);
}

@Test
public void shouldThrowWhenAudienceClaimIsNullWithAnAudience() throws Exception {
exception.expect(InvalidClaimException.class);
exception.expectMessage("The Claim 'aud' value doesn't contain the required audience.");

// Token 'aud': null
String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.Rq8IxqeX7eA6GgYxlcHdPFVRNFFZc5rEI3MQTZZbK3I";
JWTVerifier.init(Algorithm.HMAC256("secret"))
.withAnyOfAudience("nope")
.build()
.verify(token);
}

@Test
public void shouldRemoveAudienceWhenPassingNullReference() throws Exception {
Algorithm algorithm = mock(Algorithm.class);
Expand All @@ -159,29 +259,62 @@ public void shouldRemoveAudienceWhenPassingNullReference() throws Exception {
.build();

assertThat(verifier.claims, is(notNullValue()));
assertThat(verifier.claims, not(hasKey("aud")));
assertThat(verifier.claims, not(hasKey(JWTVerifier.AUDIENCE_EXACT)));

verifier = JWTVerifier.init(algorithm)
.withAudience((String[]) null)
.build();

assertThat(verifier.claims, is(notNullValue()));
assertThat(verifier.claims, not(hasKey("aud")));
assertThat(verifier.claims, not(hasKey(JWTVerifier.AUDIENCE_EXACT)));

verifier = JWTVerifier.init(algorithm)
.withAudience()
.build();

assertThat(verifier.claims, is(notNullValue()));
assertThat(verifier.claims, not(hasKey("aud")));
assertThat(verifier.claims, not(hasKey(JWTVerifier.AUDIENCE_EXACT)));

String emptyAud = " ";
verifier = JWTVerifier.init(algorithm)
.withAudience(emptyAud)
.build();

assertThat(verifier.claims, is(notNullValue()));
assertThat(verifier.claims, hasEntry("aud", Collections.singletonList(emptyAud)));
assertThat(verifier.claims, hasEntry(JWTVerifier.AUDIENCE_EXACT, Collections.singletonList(emptyAud)));
}

@Test
public void shouldRemoveAudienceWhenPassingNullReferenceWithAnyOfAudience() throws Exception {
Algorithm algorithm = mock(Algorithm.class);
JWTVerifier verifier = JWTVerifier.init(algorithm)
.withAnyOfAudience((String) null)
.build();

assertThat(verifier.claims, is(notNullValue()));
assertThat(verifier.claims, not(hasKey(JWTVerifier.AUDIENCE_CONTAINS)));

verifier = JWTVerifier.init(algorithm)
.withAnyOfAudience((String[]) null)
.build();

assertThat(verifier.claims, is(notNullValue()));
assertThat(verifier.claims, not(hasKey(JWTVerifier.AUDIENCE_CONTAINS)));

verifier = JWTVerifier.init(algorithm)
.withAnyOfAudience()
.build();

assertThat(verifier.claims, is(notNullValue()));
assertThat(verifier.claims, not(hasKey(JWTVerifier.AUDIENCE_CONTAINS)));

String emptyAud = " ";
verifier = JWTVerifier.init(algorithm)
.withAnyOfAudience(emptyAud)
.build();

assertThat(verifier.claims, is(notNullValue()));
assertThat(verifier.claims, hasEntry(JWTVerifier.AUDIENCE_CONTAINS, Collections.singletonList(emptyAud)));
}

@Test
Expand All @@ -204,6 +337,26 @@ public void shouldRemoveAudienceWhenPassingNull() throws Exception {
assertThat(verifier.claims, not(hasKey("aud")));
}

@Test
public void shouldRemoveAudienceWhenPassingNullWithAnyAudience() throws Exception {
Algorithm algorithm = mock(Algorithm.class);
JWTVerifier verifier = JWTVerifier.init(algorithm)
.withAnyOfAudience("John")
.withAnyOfAudience((String) null)
.build();

assertThat(verifier.claims, is(notNullValue()));
assertThat(verifier.claims, not(hasKey("aud")));

verifier = JWTVerifier.init(algorithm)
.withAnyOfAudience("John")
.withAnyOfAudience((String[]) null)
.build();

assertThat(verifier.claims, is(notNullValue()));
assertThat(verifier.claims, not(hasKey("aud")));
}

@Test
public void shouldThrowOnNullCustomClaimName() throws Exception {
exception.expect(IllegalArgumentException.class);
Expand Down