View Javadoc

1   package com.atlassian.asap.core.validator;
2   
3   import com.atlassian.asap.api.Jwt;
4   import com.atlassian.asap.api.JwtBuilder;
5   import com.atlassian.asap.core.exception.InvalidClaimException;
6   import com.atlassian.asap.core.exception.TokenExpiredException;
7   import com.atlassian.asap.core.exception.TokenTooEarlyException;
8   import com.google.common.collect.ImmutableSet;
9   import org.junit.Before;
10  import org.junit.Test;
11  import org.junit.runner.RunWith;
12  import org.mockito.runners.MockitoJUnitRunner;
13  
14  import java.time.Clock;
15  import java.time.Duration;
16  import java.time.Instant;
17  import java.time.ZoneId;
18  import java.time.temporal.ChronoUnit;
19  import java.util.Optional;
20  
21  @RunWith(MockitoJUnitRunner.class)
22  public class JwtClaimsValidatorTest {
23      private static final String JWT_ID = "some-id";
24      private static final String VALID_AUDIENCE = "valid-audience";
25      private static final String VALID_AUDIENCE2 = "valid-audience-2";
26      private static final String VALID_ISSUER = "valid-issuer";
27      private static final String VALID_SUBJECT = "valid-subject";
28      private static final Instant NOW = Instant.EPOCH; // arbitrary date
29      private static final Instant THE_PAST = NOW.minus(1, ChronoUnit.DAYS);
30      private static final Instant THE_FUTURE = NOW.plus(1, ChronoUnit.DAYS);
31  
32      private static final Jwt VALID_SELF_ISSUED_TOKEN = JwtBuilder.newJwt()
33              .issuedAt(NOW)
34              .notBefore(Optional.of(NOW))
35              .expirationTime(NOW.plusSeconds(60))
36              .keyId(VALID_ISSUER + "/some-key")
37              .issuer(VALID_ISSUER)
38              .audience(VALID_AUDIENCE)
39              .jwtId(JWT_ID)
40              .build();
41  
42      private JwtClaimsValidator validator;
43  
44      @Before
45      public void createValidator() {
46          validator = new JwtClaimsValidator(Clock.fixed(NOW, ZoneId.of("UTC+0")));
47      }
48  
49      private void runValidate(Jwt jwt) throws Exception {
50          validator.validate(jwt, ImmutableSet.of(VALID_AUDIENCE));
51      }
52  
53      @Test
54      public void shouldAcceptSelfIssuedValidClaims() throws Exception {
55          runValidate(VALID_SELF_ISSUED_TOKEN);
56      }
57  
58      @Test
59      public void shouldAcceptJwtIfIssuerDoesNotMatchSubject() throws Exception {
60          Jwt jwt = JwtBuilder.copyJwt(VALID_SELF_ISSUED_TOKEN)
61                  .subject(Optional.of(VALID_SUBJECT))
62                  .build();
63          runValidate(jwt);
64      }
65  
66      // Tests related to the audience, the subject and the issuer
67  
68      @Test(expected = InvalidClaimException.class)
69      public void shouldRejectJwtIfIssuerIsBlank() throws Exception {
70          Jwt jwt = JwtBuilder.copyJwt(VALID_SELF_ISSUED_TOKEN)
71                  .issuer("   ")
72                  .build();
73          runValidate(jwt);
74      }
75  
76      @Test(expected = InvalidClaimException.class)
77      public void shouldRejectJwtIfKidDoesNotStartWithIssuer() throws Exception {
78          Jwt jwt = JwtBuilder.copyJwt(VALID_SELF_ISSUED_TOKEN)
79                  .keyId("some-other/key")
80                  .build();
81          runValidate(jwt);
82      }
83  
84      @Test
85      public void shouldAcceptJwtIfAudienceIsOneOfValidAudiences() throws Exception {
86          Jwt jwt = JwtBuilder.copyJwt(VALID_SELF_ISSUED_TOKEN)
87                  .subject(Optional.of(VALID_SUBJECT))
88                  .build();
89          validator.validate(jwt, ImmutableSet.of(VALID_AUDIENCE, VALID_AUDIENCE2));
90      }
91  
92      @Test(expected = InvalidClaimException.class)
93      public void shouldRejectJwtIfTheAudienceIsNotValid() throws Exception {
94          Jwt jwt = JwtBuilder.copyJwt(VALID_SELF_ISSUED_TOKEN)
95                  .audience("invalid-audience")
96                  .build();
97          runValidate(jwt);
98      }
99  
100     @Test(expected = InvalidClaimException.class)
101     public void shouldRejectJwtIfTheAudienceIsNotOneOfValidAudiences() throws Exception {
102         Jwt jwt = JwtBuilder.copyJwt(VALID_SELF_ISSUED_TOKEN)
103                 .audience("invalid-audience")
104                 .build();
105         validator.validate(jwt, ImmutableSet.of(VALID_AUDIENCE, VALID_AUDIENCE2));
106     }
107 
108     // Formal verifications of time claims
109 
110     @Test(expected = InvalidClaimException.class)
111     public void shouldRejectJwtThatWasIssuedImmediatelyAfterItsExpiry() throws Exception {
112         Jwt jwt = JwtBuilder.copyJwt(VALID_SELF_ISSUED_TOKEN)
113                 .issuedAt(VALID_SELF_ISSUED_TOKEN.getClaims().getExpiry().plusSeconds(1))
114                 .build();
115         runValidate(jwt);
116     }
117 
118     @Test(expected = InvalidClaimException.class)
119     public void shouldRejectJwtThatWasValidImmediatelyBeforeTheTimeItWasIssued() throws Exception {
120         Jwt jwt = JwtBuilder.copyJwt(VALID_SELF_ISSUED_TOKEN)
121                 .notBefore(Optional.of(VALID_SELF_ISSUED_TOKEN.getClaims().getIssuedAt().minusSeconds(1)))
122                 .build();
123         runValidate(jwt);
124     }
125 
126     @Test(expected = InvalidClaimException.class)
127     public void shouldRejectJwtThatIsNeverValid() throws Exception {
128         Jwt jwt = JwtBuilder.copyJwt(VALID_SELF_ISSUED_TOKEN)
129                 .notBefore(Optional.of(VALID_SELF_ISSUED_TOKEN.getClaims().getExpiry().plusSeconds(1)))
130                 .build();
131         runValidate(jwt);
132     }
133 
134     @Test(expected = InvalidClaimException.class)
135     public void shouldRejectJwtIfLifetimeExceedsOneHour() throws Exception {
136         Jwt jwt = JwtBuilder.copyJwt(VALID_SELF_ISSUED_TOKEN)
137                 .expirationTime(NOW.plus(2, ChronoUnit.HOURS)) // long lived token
138                 .build();
139         runValidate(jwt);
140     }
141 
142     @Test
143     public void shouldAcceptJwtIfLifetimeDoesntExceedsMaxLifetime() throws Exception {
144         JwtClaimsValidator validator = new JwtClaimsValidator(Clock.fixed(NOW, ZoneId.of("UTC+0")), Duration.ofDays(30));
145         Jwt jwt = JwtBuilder.copyJwt(VALID_SELF_ISSUED_TOKEN)
146                 .expirationTime(NOW.plus(29, ChronoUnit.DAYS))
147                 .build();
148         validator.validate(jwt, ImmutableSet.of(VALID_AUDIENCE));
149     }
150 
151     // Test that are relative to the current time
152 
153     @Test(expected = TokenExpiredException.class)
154     public void shouldRejectJwtThatHasExpired() throws Exception {
155         Jwt jwt = JwtBuilder.copyJwt(VALID_SELF_ISSUED_TOKEN)
156                 .expirationTime(THE_PAST)
157                 .issuedAt(THE_PAST) // because there is a formal validation that iat < exp
158                 .notBefore(Optional.<Instant>empty()) // because there is a formal verification that nfb < exp
159                 .build();
160         runValidate(jwt);
161     }
162 
163     @Test(expected = TokenTooEarlyException.class)
164     public void shouldRejectJwtThatIsNotValidYetWhenUsingNotBefore() throws Exception {
165         Jwt jwt = JwtBuilder.copyJwt(VALID_SELF_ISSUED_TOKEN)
166                 .issuedAt(THE_FUTURE)
167                 .notBefore(Optional.of(THE_FUTURE))
168                 .expirationTime(THE_FUTURE.plusSeconds(1)) // because there is formal validation that nbf < exp
169                 .build();
170         runValidate(jwt);
171     }
172 
173     @Test(expected = TokenTooEarlyException.class)
174     public void shouldRejectJwtThatIsNotValidYetWhenNotUsingNotBefore() throws Exception {
175         Jwt jwt = JwtBuilder.copyJwt(VALID_SELF_ISSUED_TOKEN)
176                 .issuedAt(THE_FUTURE)
177                 .notBefore(Optional.<Instant>empty())
178                 .expirationTime(THE_FUTURE.plusSeconds(1)) // because there is formal validation that nbf < exp
179                 .build();
180         runValidate(jwt);
181     }
182 }