1 package com.atlassian.asap.core.validator;
2
3 import com.atlassian.asap.api.Jwt;
4 import com.atlassian.asap.api.JwtClaims;
5 import com.atlassian.asap.api.exception.InvalidTokenException;
6 import com.atlassian.asap.core.exception.InvalidClaimException;
7 import com.atlassian.asap.core.exception.TokenExpiredException;
8 import com.atlassian.asap.core.exception.TokenTooEarlyException;
9 import org.apache.commons.lang3.StringUtils;
10 import org.slf4j.Logger;
11 import org.slf4j.LoggerFactory;
12
13 import java.time.Clock;
14 import java.time.Duration;
15 import java.time.Instant;
16 import java.util.Collections;
17 import java.util.Objects;
18 import java.util.Optional;
19 import java.util.Set;
20
21
22
23
24
25 public class JwtClaimsValidator {
26
27
28
29
30
31 public static final Duration TIME_CLAIM_LEEWAY = Duration.ofSeconds(Long.parseLong(
32 System.getProperty("asap.resource.server.leeway.seconds", "30")));
33
34
35
36
37 public static final Duration DEFAULT_MAX_LIFETIME = Duration.ofHours(1);
38
39 private static final Logger logger = LoggerFactory.getLogger(JwtClaimsValidator.class);
40
41 private final Clock clock;
42 private final Duration maxLifetime;
43
44 public JwtClaimsValidator(Clock clock) {
45 this(clock, DEFAULT_MAX_LIFETIME);
46 }
47
48 public JwtClaimsValidator(Clock clock, Duration maxTokenLifetime) {
49 this.clock = Objects.requireNonNull(clock);
50 this.maxLifetime = Objects.requireNonNull(maxTokenLifetime);
51 }
52
53
54
55
56
57
58
59
60 public void validate(Jwt jwt,
61 Set<String> resourceServerAudiences)
62 throws InvalidTokenException {
63 final JwtClaims claims = jwt.getClaims();
64 final String issuer = claims.getIssuer();
65 final Set<String> audience = claims.getAudience();
66 final Instant issuedAt = claims.getIssuedAt();
67 final Instant expiry = claims.getExpiry();
68 final Optional<Instant> mayBeNotBefore = claims.getNotBefore();
69 final String keyId = jwt.getHeader().getKeyId();
70
71 issuerValidation(issuer, keyId);
72 audienceValidation(audience, resourceServerAudiences);
73 formalTimeClaimsValidation(issuedAt, expiry, mayBeNotBefore);
74 relativeTimeValidation(issuedAt, expiry, mayBeNotBefore);
75
76 }
77
78 private void issuerValidation(String issuer, String keyId)
79 throws InvalidClaimException {
80
81 if (StringUtils.isBlank(issuer)) {
82 logger.debug("Rejecting blank issuer");
83 throw new InvalidClaimException(JwtClaims.RegisteredClaim.ISSUER, "Issuer cannot be blank");
84 }
85
86
87
88
89 if (!keyId.startsWith(issuer + "/")) {
90 logger.debug("The issuer {} does not match the key id {}", issuer, keyId);
91 throw new InvalidClaimException(JwtClaims.RegisteredClaim.ISSUER, "The issuer claim does not match the key id");
92 }
93 }
94
95 private void audienceValidation(Set<String> audience,
96 Set<String> resourceServerAudiences) throws InvalidClaimException {
97
98 if (Collections.disjoint(audience, resourceServerAudiences)) {
99 logger.debug("Rejected unrecognised audience {}", audience);
100 throw new InvalidClaimException(JwtClaims.RegisteredClaim.AUDIENCE, "Unrecognised audience");
101 }
102 }
103
104
105
106
107 private void formalTimeClaimsValidation(Instant issuedAt, Instant expiry, Optional<Instant> mayBeNotBefore)
108 throws InvalidClaimException {
109
110 if (expiry.isBefore(issuedAt)) {
111 logger.debug("Expiry time {} set before issue time {}", expiry, issuedAt);
112 throw new InvalidClaimException(JwtClaims.RegisteredClaim.ISSUED_AT, "Expiry time set before issue time");
113 }
114
115
116 if (issuedAt.plus(maxLifetime).isBefore(expiry)) {
117 logger.debug("Token exceeds lifetime limit, issued at {} and expires at {}", issuedAt, expiry);
118 throw new InvalidClaimException(JwtClaims.RegisteredClaim.EXPIRY, "Token exceeds lifetime limit");
119 }
120
121 if (mayBeNotBefore.isPresent()) {
122 Instant nbf = mayBeNotBefore.get();
123
124
125 if (nbf.isAfter(expiry)) {
126 logger.debug("The expiry time {} must be after the not-before time {}", expiry, nbf);
127 throw new InvalidClaimException(JwtClaims.RegisteredClaim.NOT_BEFORE, "The expiry time must be after the not-before time");
128 }
129
130
131 if (nbf.isBefore(issuedAt)) {
132 logger.debug("The token was valid since {} but was issued later at {}", nbf, issuedAt);
133 throw new InvalidClaimException(JwtClaims.RegisteredClaim.NOT_BEFORE, "The token must not be valid before it was issued");
134 }
135 }
136 }
137
138
139
140
141 private void relativeTimeValidation(Instant issuedAt, Instant expiry, Optional<Instant> mayBeNotBefore)
142 throws TokenExpiredException, TokenTooEarlyException {
143 Instant now = Instant.now(clock);
144
145
146 final Instant nowMinusLeeway = now.minus(TIME_CLAIM_LEEWAY);
147 final Instant nowPlusLeeway = now.plus(TIME_CLAIM_LEEWAY);
148
149
150 if (expiry.isBefore(nowMinusLeeway)) {
151 logger.info("Rejecting expired token, now={}, expiry={}, leeway={}", now, expiry, TIME_CLAIM_LEEWAY);
152 throw new TokenExpiredException(expiry, now);
153 }
154
155
156 Instant effectiveNbf = mayBeNotBefore.orElse(issuedAt);
157 if (effectiveNbf.isAfter(nowPlusLeeway)) {
158 logger.info("Rejecting token that arrives too early, now={}, not before={}, leeway={}", now, effectiveNbf, TIME_CLAIM_LEEWAY);
159 throw new TokenTooEarlyException(effectiveNbf, now);
160 }
161 }
162 }