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
187
188
189
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);
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 }