View Javadoc

1   package com.atlassian.asap.nimbus.parser;
2   
3   import com.atlassian.asap.api.Jwt;
4   import com.atlassian.asap.api.SigningAlgorithm;
5   import com.atlassian.asap.core.exception.JwtParseException;
6   import com.atlassian.asap.core.exception.MissingRequiredClaimException;
7   import com.atlassian.asap.core.exception.MissingRequiredHeaderException;
8   import com.atlassian.asap.core.exception.UnsupportedAlgorithmException;
9   import com.google.common.collect.ImmutableMap;
10  import com.google.common.collect.ImmutableSet;
11  import com.nimbusds.jose.util.Base64;
12  import com.nimbusds.jose.util.Base64URL;
13  import net.minidev.json.JSONObject;
14  import org.junit.Test;
15  import org.junit.runner.RunWith;
16  import org.mockito.InjectMocks;
17  import org.mockito.runners.MockitoJUnitRunner;
18  
19  import javax.annotation.Nullable;
20  import javax.json.JsonValue;
21  import java.util.Collections;
22  import java.util.HashMap;
23  import java.util.Map;
24  import java.util.Optional;
25  import java.util.Set;
26  
27  import static org.junit.Assert.assertEquals;
28  import static org.junit.Assert.assertFalse;
29  
30  @RunWith(MockitoJUnitRunner.class)
31  public class NimbusJwtParserTest {
32      private static final String VALID_KID = "my-kid";
33      private static final String VALID_TOKEN_ID = "my-token-id";
34      private static final String VALID_ISSUER = "my-issuer";
35      private static final String VALID_SUBJECT = "my-subject";
36      private static final String VALID_AUDIENCE = "my-audience";
37      private static final long VALID_NBF = 1L;
38      private static final long VALID_IAT = 2L;
39      private static final long VALID_EXPIRY = 3L;
40  
41      private static final Map<String, Object> VALID_HEADERS = ImmutableMap.<String, Object>of(
42              "kid", VALID_KID,
43              "alg", "RS256"
44      );
45      private static final Map<String, Object> VALID_CLAIMS = ImmutableMap.<String, Object>of(
46              "jti", VALID_TOKEN_ID,
47              "iss", VALID_ISSUER,
48              "aud", VALID_AUDIENCE,
49              "iat", VALID_IAT,
50              "exp", VALID_EXPIRY
51      );
52      private static final Base64URL SIGNATURE = Base64URL.encode("some-signature");
53  
54      @InjectMocks
55      private NimbusJwtParser parser;
56  
57      @Test
58      public void shouldAcceptValidSignedToken() throws Exception {
59          Jwt jwt = parser.parse(serialisedJwt(VALID_HEADERS, VALID_CLAIMS, SIGNATURE));
60  
61          assertEquals(SigningAlgorithm.RS256, jwt.getHeader().getAlgorithm());
62          assertEquals(VALID_KID, jwt.getHeader().getKeyId());
63          assertEquals(VALID_ISSUER, jwt.getClaims().getIssuer());
64          assertFalse(jwt.getClaims().getSubject().isPresent());
65          assertEquals(ImmutableSet.of(VALID_AUDIENCE), jwt.getClaims().getAudience());
66          assertEquals(VALID_IAT, jwt.getClaims().getIssuedAt().getEpochSecond());
67          assertEquals(VALID_EXPIRY, jwt.getClaims().getExpiry().getEpochSecond());
68          assertFalse(jwt.getClaims().getNotBefore().isPresent());
69      }
70  
71      @Test
72      public void shouldParseStringPrivateClaimIfAvailable() throws Exception {
73          Map<String, Object> claims = add(VALID_CLAIMS, "privateClaim", "privateClaimValue");
74          Jwt jwt = parser.parse(serialisedJwt(VALID_HEADERS, claims, SIGNATURE));
75  
76          assertEquals("privateClaimValue", jwt.getClaims().getJson().getString("privateClaim"));
77      }
78  
79      @Test
80      public void shouldParseNullPrivateClaimIfAvailable() throws Exception {
81          Map<String, Object> claims = add(VALID_CLAIMS, "privateClaim", null);
82          Jwt jwt = parser.parse(serialisedJwt(VALID_HEADERS, claims, SIGNATURE));
83  
84          assertEquals(JsonValue.NULL, jwt.getClaims().getJson().get("privateClaim"));
85      }
86  
87      @Test
88      public void shouldParseSubjectIfAvailable() throws Exception {
89          Map<String, Object> claims = add(VALID_CLAIMS, "sub", VALID_SUBJECT);
90          Jwt jwt = parser.parse(serialisedJwt(VALID_HEADERS, claims, SIGNATURE));
91  
92          assertEquals(VALID_SUBJECT, jwt.getClaims().getSubject().get());
93      }
94  
95      @Test
96      public void shouldParseNotBeforeDateIfAvailable() throws Exception {
97          Map<String, Object> claims = add(VALID_CLAIMS, "nbf", VALID_NBF);
98          Jwt jwt = parser.parse(serialisedJwt(VALID_HEADERS, claims, SIGNATURE));
99  
100         assertEquals(VALID_NBF, jwt.getClaims().getNotBefore().get().getEpochSecond());
101     }
102 
103     @Test
104     public void shouldParseMultipleAudiences() throws Exception {
105         Set<String> audiences = ImmutableSet.of(VALID_AUDIENCE, "another-audience");
106         Map<String, Object> claims = add(remove(VALID_CLAIMS, "aud"), "aud", audiences);
107 
108         Jwt jwt = parser.parse(serialisedJwt(VALID_HEADERS, claims, SIGNATURE));
109 
110         assertEquals(audiences, jwt.getClaims().getAudience());
111     }
112 
113     @Test(expected = MissingRequiredHeaderException.class)
114     public void shouldRequireKidHeader() throws Exception {
115         Map<String, Object> headers = remove(VALID_HEADERS, "kid");
116 
117         parser.parse(serialisedJwt(headers, VALID_CLAIMS, SIGNATURE));
118     }
119 
120     @Test(expected = JwtParseException.class)
121     public void shouldRequireAlgHeader() throws Exception {
122         Map<String, Object> headers = remove(VALID_HEADERS, "alg");
123 
124         parser.parse(serialisedJwt(headers, VALID_CLAIMS, SIGNATURE));
125     }
126 
127     @Test(expected = MissingRequiredClaimException.class)
128     public void shouldRequireIssClaim() throws Exception {
129         Map<String, Object> claims = remove(VALID_CLAIMS, "iss");
130 
131         parser.parse(serialisedJwt(VALID_HEADERS, claims, SIGNATURE));
132     }
133 
134     @Test(expected = MissingRequiredClaimException.class)
135     public void shouldRequireAudClaim() throws Exception {
136         Map<String, Object> claims = remove(VALID_CLAIMS, "aud");
137 
138         parser.parse(serialisedJwt(VALID_HEADERS, claims, SIGNATURE));
139     }
140 
141     @Test(expected = MissingRequiredClaimException.class)
142     public void shouldRequireJtiClaim() throws Exception {
143         Map<String, Object> claims = remove(VALID_CLAIMS, "jti");
144 
145         parser.parse(serialisedJwt(VALID_HEADERS, claims, SIGNATURE));
146     }
147 
148     @Test(expected = MissingRequiredClaimException.class)
149     public void shouldRequireIatClaim() throws Exception {
150         Map<String, Object> claims = remove(VALID_CLAIMS, "iat");
151 
152         parser.parse(serialisedJwt(VALID_HEADERS, claims, SIGNATURE));
153     }
154 
155     @Test(expected = MissingRequiredClaimException.class)
156     public void shouldRequireExpClaim() throws Exception {
157         Map<String, Object> claims = remove(VALID_CLAIMS, "exp");
158 
159         parser.parse(serialisedJwt(VALID_HEADERS, claims, SIGNATURE));
160     }
161 
162     @Test(expected = JwtParseException.class)
163     public void shouldRejectTokenIfAlgorithmIsNone() throws Exception {
164         Map<String, Object> headers = add(remove(VALID_HEADERS, "alg"), "alg", "none");
165 
166         parser.parse(serialisedJwt(headers, VALID_CLAIMS, SIGNATURE));
167     }
168 
169     @Test
170     public void shouldDetermineIssuerSucceedIfAvailable() {
171         Optional<String> issuer = parser.determineUnverifiedIssuer(serialisedJwt(VALID_HEADERS, VALID_CLAIMS, SIGNATURE));
172 
173         assertEquals(VALID_ISSUER, issuer.get());
174     }
175 
176     @Test
177     public void shouldDetermineIssuerReturnEmptyIfNotAvailable() {
178         Optional<String> issuerClaimMissing = parser.determineUnverifiedIssuer(serialisedJwt(VALID_HEADERS, remove(VALID_CLAIMS, "iss"), SIGNATURE));
179         Optional<String> issuerNonParseable = parser.determineUnverifiedIssuer("some-invalid-authorization-header");
180 
181         assertFalse(issuerClaimMissing.isPresent());
182         assertFalse(issuerNonParseable.isPresent());
183     }
184 
185     /**
186      * ASAP requires asymmetric cryptography, otherwise we cannot ensure that the client is the exclusive
187      * owner of the key used to sign the token. Therefore HMAC algorithms must be rejected. This is in
188      * contradiction to <a href="https://tools.ietf.org/html/rfc7518">JWA, sect 3.1</a>,
189      * which specifies that HS256 is a "required" algorithm for implementations.
190      */
191     @Test(expected = UnsupportedAlgorithmException.class)
192     public void shouldRejectTokenIfAlgorithmUsesSymmetricCryptography() throws Exception {
193         Map<String, Object> headers = add(remove(VALID_HEADERS, "alg"), "alg", "hs256");
194 
195         parser.parse(serialisedJwt(headers, VALID_CLAIMS, SIGNATURE));
196     }
197 
198     private static String serialisedJwt(Map<String, Object> headers, Map<String, Object> payload, Base64URL signature) {
199         return String.join(".", Base64.encode(JSONObject.toJSONString(headers)).toString(),
200                 Base64.encode(JSONObject.toJSONString(payload)).toString(), signature.toString());
201     }
202 
203     private static Map<String, Object> add(Map<String, Object> map, String key, @Nullable Object value) {
204         HashMap<String, Object> mutableMap = new HashMap<>(map);  // cannot use Guava's because needs to support null values
205         mutableMap.put(key, value);
206         return Collections.unmodifiableMap(mutableMap);
207     }
208 
209     private static Map<String, Object> remove(Map<String, Object> map, String key) {
210         HashMap<String, Object> mutableMap = new HashMap<>(map);
211         mutableMap.remove(key);
212         return ImmutableMap.copyOf(mutableMap);
213     }
214 }