1 package com.atlassian.asap.service.core.impl;
2
3 import com.atlassian.asap.api.Jwt;
4 import com.atlassian.asap.api.JwtBuilder;
5 import com.atlassian.asap.api.JwtClaims;
6 import com.google.common.base.MoreObjects;
7 import com.google.common.collect.ImmutableSet;
8 import org.hamcrest.Description;
9 import org.hamcrest.TypeSafeMatcher;
10
11 import javax.json.JsonObject;
12 import javax.json.JsonValue;
13 import java.time.Duration;
14 import java.util.HashMap;
15 import java.util.Map;
16 import java.util.Optional;
17 import java.util.Set;
18
19 import static java.util.Objects.requireNonNull;
20
21
22
23
24
25
26
27 final class JwtMatcher extends TypeSafeMatcher<Jwt> {
28 private Optional<String> issuer = Optional.empty();
29 private Optional<String> keyId = Optional.empty();
30 private Optional<Optional<String>> subject = Optional.empty();
31 private Optional<Set<String>> audience = Optional.empty();
32 private Map<String, JsonValue> customClaims = new HashMap<>();
33 private Duration expiration = JwtBuilder.DEFAULT_LIFETIME;
34
35 private JwtMatcher() {
36 }
37
38 static JwtMatcher jwtMatcher() {
39 return new JwtMatcher();
40 }
41
42 JwtMatcher issuer(String issuer) {
43 this.issuer = Optional.of(issuer);
44 return this;
45 }
46
47 JwtMatcher keyId(String keyId) {
48 this.keyId = Optional.of(keyId);
49 return this;
50 }
51
52 JwtMatcher subject(Optional<String> subject) {
53 this.subject = Optional.of(subject);
54 return this;
55 }
56
57 JwtMatcher audience(String... audience) {
58 this.audience = Optional.of(ImmutableSet.copyOf(audience));
59 return this;
60 }
61
62 JwtMatcher expiration(Duration duration) {
63 this.expiration = requireNonNull(duration);
64 return this;
65 }
66
67 JwtMatcher customClaim(String key, JsonValue expectedValue) {
68 requireNonNull(key, "key");
69 requireNonNull(expectedValue, "expectedValue");
70 this.customClaims.put(key, expectedValue);
71 return this;
72 }
73
74 @Override
75 @SuppressWarnings("checkstyle:BooleanExpressionComplexity")
76 protected boolean matchesSafely(Jwt jwt) {
77 final JwtClaims claims = jwt.getClaims();
78 final Duration expiration = Duration.between(claims.getIssuedAt(), claims.getExpiry());
79 return matches(keyId, jwt.getHeader().getKeyId())
80 && matches(issuer, claims.getIssuer())
81 && matches(subject, claims.getSubject())
82 && matches(audience, claims.getAudience())
83 && this.expiration.equals(expiration)
84 && hasAllExpectedCustomClaims(jwt);
85 }
86
87 private boolean hasAllExpectedCustomClaims(Jwt jwt) {
88 final JsonObject json = jwt.getClaims().getJson();
89 return customClaims.entrySet().stream()
90 .allMatch(entry -> entry.getValue().equals(json.get(entry.getKey())));
91 }
92
93 @Override
94 public void describeTo(Description description) {
95 MoreObjects.ToStringHelper toString = MoreObjects.toStringHelper("Jwt");
96 keyId.ifPresent(kid -> toString.add("kid", kid));
97 issuer.ifPresent(iss -> toString.add("iss", iss));
98 subject.ifPresent(sub -> toString.add("sub", sub));
99 audience.ifPresent(aud -> toString.add("aud", aud));
100 toString.add("exp-iat", expiration);
101 if (!customClaims.isEmpty()) {
102 toString.add("custom", customClaims);
103 }
104 description.appendText("Jwt with constraints ").appendValue(toString.toString());
105 }
106
107 private static <T> boolean matches(Optional<T> optionalExpectedValue, T actualValue) {
108 return optionalExpectedValue
109 .map(expected -> expected.equals(actualValue))
110 .orElse(true);
111 }
112 }