1 package media.pepperpot.tca.authenticator;
2
3 import java.io.File;
4 import java.io.IOException;
5 import java.net.MalformedURLException;
6 import java.net.URL;
7 import java.nio.file.Files;
8 import java.nio.file.Paths;
9 import java.security.Principal;
10 import java.text.ParseException;
11 import java.util.ArrayList;
12 import java.util.Arrays;
13 import java.util.Enumeration;
14 import java.util.List;
15 import java.util.Properties;
16 import java.util.Set;
17 import java.util.stream.Collectors;
18
19 import javax.servlet.http.HttpServletResponse;
20
21 import org.apache.catalina.authenticator.AuthenticatorBase;
22 import org.apache.catalina.connector.Request;
23 import org.apache.juli.logging.Log;
24 import org.apache.juli.logging.LogFactory;
25
26 import com.nimbusds.jose.EncryptionMethod;
27 import com.nimbusds.jose.JOSEException;
28 import com.nimbusds.jose.JWEAlgorithm;
29 import com.nimbusds.jose.JWSAlgorithm;
30 import com.nimbusds.jose.jwk.JWKSet;
31 import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
32 import com.nimbusds.jose.jwk.source.ImmutableSecret;
33 import com.nimbusds.jose.jwk.source.JWKSource;
34 import com.nimbusds.jose.jwk.source.JWKSourceBuilder;
35 import com.nimbusds.jose.proc.BadJOSEException;
36 import com.nimbusds.jose.proc.JWEDecryptionKeySelector;
37 import com.nimbusds.jose.proc.JWEKeySelector;
38 import com.nimbusds.jose.proc.JWSKeySelector;
39 import com.nimbusds.jose.proc.JWSVerificationKeySelector;
40 import com.nimbusds.jose.proc.SecurityContext;
41 import com.nimbusds.jwt.EncryptedJWT;
42 import com.nimbusds.jwt.JWT;
43 import com.nimbusds.jwt.JWTClaimsSet;
44 import com.nimbusds.jwt.JWTParser;
45 import com.nimbusds.jwt.PlainJWT;
46 import com.nimbusds.jwt.SignedJWT;
47 import com.nimbusds.jwt.proc.ConfigurableJWTProcessor;
48 import com.nimbusds.jwt.proc.DefaultJWTClaimsVerifier;
49 import com.nimbusds.jwt.proc.DefaultJWTProcessor;
50
51
52
53
54
55
56 public class JwtAuthenticator extends AuthenticatorBase {
57
58 private final static Log log = LogFactory.getLog(JwtAuthenticator.class);
59
60
61 public static final String BEARER = "BEARER";
62
63
64 public static final String ALLOW_PLAIN = "media.pepperpot.jwt.allowPlain";
65
66 public static final String DEFAULT_ALLOW_PLAIN = Boolean.FALSE.toString();
67
68 public static final String JWS_SECRET_FILE = "media.pepperpot.jws.SecretFile";
69
70 public static final String JWS_JWK_SET_FILE = "media.pepperpot.jws.JwkSetFile";
71
72 public static final String JWS_REMOTE_JWK_SET_URL = "media.pepperpot.jws.RemoteJwkSetUrl";
73
74 public static final String JWS_ALGORITHM = "media.pepperpot.jwt.JWSAlgorithm";
75
76 public static final String JWE_SECRET_FILE = "media.pepperpot.jwe.SecretFile";
77
78 public static final String JWE_JWK_SET_FILE = "media.pepperpot.jwe.JwkSetFile";
79
80 public static final String JWE_REMOTE_JWK_SET_URL = "media.pepperpot.jwe.RemoteJwkSetUrl";
81
82 public static final String JWE_ALGORITHM = "media.pepperpot.jwt.JWEAlgorithm";
83
84 public static final String JWE_ENCRYPTION_METHOD = "media.pepperpot.jwt.JWEEncryptionMethod";
85
86 public static final String JWT_MAX_CLOCK_SKEW = "media.pepperpot.jwt.MaxClockSkew";
87
88 public static final String JWT_MAX_CLOCK_SKEW_DEFAULT = "60";
89
90 public static final String JWT_ACCEPTED_AUDIENCE = "media.pepperpot.jwt.AcceptedAudience";
91
92 public static final String JWT_REQUIRED_CLAIMS = "media.pepperpot.jwt.RequiredClaims";
93
94 public static final String JWT_PROHIBITED_CLAIMS = "media.pepperpot.jwt.ProhibitedClaims";
95
96
97 protected Properties properties = null;
98
99
100
101
102
103
104 public JwtAuthenticator() {
105 super();
106 properties = System.getProperties();
107 }
108
109
110
111
112
113
114
115
116
117 public JwtAuthenticator(Properties properties) {
118 super();
119 this.properties = properties;
120 }
121
122
123
124
125
126
127
128
129
130
131
132
133
134 protected static String parseAuthorization(Properties properties, Request request, String claimName) {
135
136 Enumeration<String> authorizations = request.getHeaders("Authorization");
137 if (authorizations == null || !authorizations.hasMoreElements()) {
138 if (log.isFatalEnabled()) {
139 log.fatal("No Authorization header found in request: request=" + request + ", remoteHost="
140 + request.getRemoteHost() + ", remoteIP=" + request.getRemoteAddr());
141 }
142 return null;
143 }
144
145 List<String> statuses = new ArrayList<String>();
146
147 while (authorizations.hasMoreElements()) {
148 String authorization = authorizations.nextElement();
149
150 String[] split = authorization.split(" ", 2);
151 if (split == null || split.length != 2 || !"Bearer".equals(split[0])) {
152 statuses.add("Authorization header found, but not a Bearer token");
153 continue;
154 }
155 String jwt = split[1];
156
157 if (jwt == null) {
158 statuses.add("No JWT token found in Bearer Authentication header");
159 continue;
160 }
161
162 try {
163
164 String jwsSecretFile = properties.getProperty(JWS_SECRET_FILE);
165 String jwsJwkSetFile = properties.getProperty(JWS_JWK_SET_FILE);
166 String jwsRemoteJwkSetUrl = properties.getProperty(JWS_REMOTE_JWK_SET_URL);
167
168 String jweSecretFile = properties.getProperty(JWE_SECRET_FILE);
169 String jweJwkSetFile = properties.getProperty(JWE_JWK_SET_FILE);
170 String jweRemoteJwkSetUrl = properties.getProperty(JWE_REMOTE_JWK_SET_URL);
171
172 JWT parsed = JWTParser.parse(jwt);
173
174 JWTClaimsSet claims = null;
175
176 if (parsed instanceof PlainJWT) {
177
178 String allow = properties.getProperty(ALLOW_PLAIN, DEFAULT_ALLOW_PLAIN);
179 if (!Boolean.TRUE.toString().equals(allow)) {
180 statuses.add(
181 "Plain JWT tokens are not allowed by parameter -D'\" + ALLOW_PLAIN + \"', ignoring");
182 continue;
183 }
184
185 claims = parsed.getJWTClaimsSet();
186
187 } else if (parsed instanceof SignedJWT) {
188
189 ConfigurableJWTProcessor<SecurityContext> jwtProcessor = new DefaultJWTProcessor<SecurityContext>();
190
191 JWKSource<SecurityContext> keySource = null;
192
193 if (!isBlank(jwsSecretFile)) {
194 byte[] secret;
195 try {
196 secret = Files.readAllBytes(Paths.get(jwsSecretFile));
197 } catch (IOException e) {
198 statuses.add("JWT token is signed, but secret '" + jwsSecretFile + "' could not be read: "
199 + e.toString());
200 continue;
201 }
202 keySource = new ImmutableSecret<SecurityContext>(secret);
203 } else if (!isBlank(jwsJwkSetFile)) {
204 try {
205 keySource = new ImmutableJWKSet<SecurityContext>(JWKSet.load(new File(jwsJwkSetFile)));
206 } catch (IOException e) {
207 statuses.add("JWT token is signed, but remote JWK set '" + jwsJwkSetFile
208 + "' could not be read: " + e.toString());
209 continue;
210 }
211 } else if (!isBlank(jwsRemoteJwkSetUrl)) {
212 try {
213 keySource = JWKSourceBuilder.<SecurityContext> create(new URL(jwsRemoteJwkSetUrl)).build();
214 } catch (MalformedURLException e) {
215 statuses.add("JWT token is signed, but remote JWK set '" + jwsRemoteJwkSetUrl
216 + "' was a malformed URL: " + e.toString());
217 continue;
218 }
219 } else {
220 statuses.add(
221 "JWT token is signed, but we have no secret or remote jwk set to verify it against");
222 continue;
223 }
224
225 String jwsAlgorithmString = properties.getProperty(JWS_ALGORITHM);
226 JWSAlgorithm expectedJWSAlg = JWSAlgorithm.parse(jwsAlgorithmString);
227 if (expectedJWSAlg == null) {
228 statuses.add(
229 "JWT token is signed, but JWS algorithm was not recognised: " + jwsAlgorithmString);
230 continue;
231 }
232
233 JWSKeySelector<SecurityContext> keySelector = new JWSVerificationKeySelector<SecurityContext>(
234 expectedJWSAlg, keySource);
235 jwtProcessor.setJWSKeySelector(keySelector);
236
237 String jwtMaxClockSkewString = properties.getProperty(JWT_MAX_CLOCK_SKEW,
238 JWT_MAX_CLOCK_SKEW_DEFAULT);
239 String jwtAcceptedAudienceString = properties.getProperty(JWT_ACCEPTED_AUDIENCE);
240 String jwtRequiredClaimsString = properties.getProperty(JWT_REQUIRED_CLAIMS);
241 String jwtProhibitedClaimsString = properties.getProperty(JWT_PROHIBITED_CLAIMS);
242 try {
243 Set<String> jwtAcceptedAudience = jwtAcceptedAudienceString == null ? null
244 : Arrays.stream(jwtAcceptedAudienceString.split(",")).map(String::trim)
245 .collect(Collectors.toSet());
246 Set<String> jwtRequiredClaims = jwtRequiredClaimsString == null ? null
247 : Arrays.stream(jwtRequiredClaimsString.split(",")).map(String::trim)
248 .collect(Collectors.toSet());
249 Set<String> jwtProhibitedClaims = jwtProhibitedClaimsString == null ? null
250 : Arrays.stream(jwtProhibitedClaimsString.split(",")).map(String::trim)
251 .collect(Collectors.toSet());
252 DefaultJWTClaimsVerifier<SecurityContext> claimsVerifier = new DefaultJWTClaimsVerifier<SecurityContext>(
253 jwtAcceptedAudience, null, jwtRequiredClaims, jwtProhibitedClaims);
254 claimsVerifier.setMaxClockSkew(Integer.parseInt(jwtMaxClockSkewString));
255 jwtProcessor.setJWTClaimsSetVerifier(claimsVerifier);
256 } catch (NumberFormatException e) {
257 statuses.add("JWT token is signed, but " + JWT_MAX_CLOCK_SKEW + " could not be parsed: "
258 + jwsAlgorithmString + " :" + e.toString());
259 continue;
260 }
261
262 SecurityContext ctx = null;
263
264 try {
265 claims = jwtProcessor.process(parsed, ctx);
266 } catch (BadJOSEException e) {
267 statuses.add("JWT token was signed, but was formatted badly: " + e.toString());
268 continue;
269 } catch (JOSEException e) {
270 statuses.add("JWT token was signed, but was could not be verified: " + e.toString());
271 continue;
272 }
273
274 } else if (parsed instanceof EncryptedJWT) {
275
276 ConfigurableJWTProcessor<SecurityContext> jwtProcessor = new DefaultJWTProcessor<SecurityContext>();
277
278 JWKSource<SecurityContext> keySource = null;
279
280
281 if (!isBlank(jwsSecretFile)) {
282 byte[] secret;
283 try {
284 secret = Files.readAllBytes(Paths.get(jwsSecretFile));
285 } catch (IOException e) {
286 statuses.add("JWT token is encrypted, but signature secret '" + jwsSecretFile
287 + "' could not be read: " + e.toString());
288 continue;
289 }
290 keySource = new ImmutableSecret<SecurityContext>(secret);
291 } else if (!isBlank(jwsJwkSetFile)) {
292 try {
293 keySource = new ImmutableJWKSet<SecurityContext>(JWKSet.load(new File(jwsJwkSetFile)));
294 } catch (IOException e) {
295 statuses.add("JWT token is encrypted, but signature remote JWK set '" + jwsJwkSetFile
296 + "' could not be read: " + e.toString());
297 continue;
298 }
299 } else if (!isBlank(jwsRemoteJwkSetUrl)) {
300 try {
301 keySource = JWKSourceBuilder.<SecurityContext> create(new URL(jwsRemoteJwkSetUrl)).build();
302 } catch (MalformedURLException e) {
303 statuses.add("JWT token is encrypted, but signature remote JWK set '" + jwsRemoteJwkSetUrl
304 + "' could not be read: " + e.toString());
305 continue;
306 }
307 }
308
309 JWSAlgorithm expectedJWSAlg = null;
310 String jwsAlgorithmString = properties.getProperty(JWS_ALGORITHM);
311 if (!isBlank(jwsAlgorithmString)) {
312 expectedJWSAlg = JWSAlgorithm.parse(jwsAlgorithmString);
313 if (expectedJWSAlg == null) {
314 statuses.add("JWT token is encrypted, but signature JWS algorithm was not recognised: "
315 + jwsAlgorithmString);
316 continue;
317 }
318 }
319
320 if (expectedJWSAlg != null && keySource == null) {
321 statuses.add("JWT token is encrypted and signature JWS algorithm was set to '"
322 + jwsAlgorithmString + "', but none of '" + JWS_SECRET_FILE + "', '" + JWS_JWK_SET_FILE
323 + "', or '" + JWS_REMOTE_JWK_SET_URL + "' is set");
324 continue;
325 }
326
327 if (expectedJWSAlg != null) {
328 JWSKeySelector<SecurityContext> keySelector = new JWSVerificationKeySelector<SecurityContext>(
329 expectedJWSAlg, keySource);
330 jwtProcessor.setJWSKeySelector(keySelector);
331 }
332
333 if (!isBlank(jweSecretFile)) {
334 byte[] secret;
335 try {
336 secret = Files.readAllBytes(Paths.get(jweSecretFile));
337 } catch (IOException e) {
338 statuses.add("JWT token is encrypted, but encryption secret '" + jweSecretFile
339 + "' could not be read: " + e.toString());
340 continue;
341 }
342 keySource = new ImmutableSecret<SecurityContext>(secret);
343 } else if (!isBlank(jweJwkSetFile)) {
344 try {
345 keySource = new ImmutableJWKSet<SecurityContext>(JWKSet.load(new File(jweJwkSetFile)));
346 } catch (IOException e) {
347 statuses.add("JWT token is encrypted, but encryption remote JWK set '" + jweJwkSetFile
348 + "' could not be read: " + e.toString());
349 continue;
350 }
351 } else if (!isBlank(jweRemoteJwkSetUrl)) {
352 try {
353 keySource = JWKSourceBuilder.<SecurityContext> create(new URL(jweRemoteJwkSetUrl)).build();
354 } catch (MalformedURLException e) {
355 statuses.add("JWT token is encrypted, but encryption remote JWK set '" + jweRemoteJwkSetUrl
356 + "' could not be read: " + e.toString());
357 continue;
358 }
359 } else {
360 statuses.add("JWT token is encrypted, but we have no '" + JWE_SECRET_FILE + "'. '"
361 + JWE_JWK_SET_FILE + "' or '" + JWE_REMOTE_JWK_SET_URL + "' to verify it against");
362 continue;
363 }
364
365 JWEAlgorithm expectedJWEAlg = null;
366 String jweAlgorithmString = properties.getProperty(JWE_ALGORITHM);
367 if (!isBlank(jweAlgorithmString)) {
368 expectedJWEAlg = JWEAlgorithm.parse(jweAlgorithmString);
369 if (expectedJWEAlg == null) {
370 statuses.add("JWT token is encrypted, but JWE algorithm was not recognised: "
371 + jweAlgorithmString);
372 continue;
373 }
374 } else {
375 statuses.add(
376 "JWT token is encrypted, but JWE algorithm '" + JWE_ALGORITHM + "' was not specified");
377 continue;
378 }
379
380 EncryptionMethod expectedJWEEnc = null;
381 String jweEncryptedMethodString = properties.getProperty(JWE_ENCRYPTION_METHOD);
382 if (!isBlank(jweEncryptedMethodString)) {
383 expectedJWEEnc = EncryptionMethod.parse(jweEncryptedMethodString);
384 if (expectedJWEEnc == null) {
385 statuses.add("JWT token is encrypted, but JWE encryption method was not recognised: "
386 + jweEncryptedMethodString);
387 continue;
388 }
389 } else {
390 statuses.add("JWT token is encrypted, but JWE encryption method '" + JWE_ENCRYPTION_METHOD
391 + "' was not specified");
392 continue;
393 }
394
395 JWEKeySelector<SecurityContext> jweKeySelector = new JWEDecryptionKeySelector<SecurityContext>(
396 expectedJWEAlg, expectedJWEEnc, keySource);
397 jwtProcessor.setJWEKeySelector(jweKeySelector);
398
399 String jwtMaxClockSkewString = properties.getProperty(JWT_MAX_CLOCK_SKEW,
400 JWT_MAX_CLOCK_SKEW_DEFAULT);
401 String jwtAcceptedAudienceString = properties.getProperty(JWT_ACCEPTED_AUDIENCE);
402 String jwtRequiredClaimsString = properties.getProperty(JWT_REQUIRED_CLAIMS);
403 String jwtProhibitedClaimsString = properties.getProperty(JWT_PROHIBITED_CLAIMS);
404 try {
405 Set<String> jwtAcceptedAudience = jwtAcceptedAudienceString == null ? null
406 : Arrays.stream(jwtAcceptedAudienceString.split(",")).map(String::trim)
407 .collect(Collectors.toSet());
408 Set<String> jwtRequiredClaims = jwtRequiredClaimsString == null ? null
409 : Arrays.stream(jwtRequiredClaimsString.split(",")).map(String::trim)
410 .collect(Collectors.toSet());
411 Set<String> jwtProhibitedClaims = jwtProhibitedClaimsString == null ? null
412 : Arrays.stream(jwtProhibitedClaimsString.split(",")).map(String::trim)
413 .collect(Collectors.toSet());
414 DefaultJWTClaimsVerifier<SecurityContext> claimsVerifier = new DefaultJWTClaimsVerifier<SecurityContext>(
415 jwtAcceptedAudience, null, jwtRequiredClaims, jwtProhibitedClaims);
416 claimsVerifier.setMaxClockSkew(Integer.parseInt(jwtMaxClockSkewString));
417 jwtProcessor.setJWTClaimsSetVerifier(claimsVerifier);
418 } catch (NumberFormatException e) {
419 statuses.add("JWT token is signed, but " + JWT_MAX_CLOCK_SKEW + " could not be parsed: "
420 + jwsAlgorithmString + " :" + e.toString());
421 continue;
422 }
423
424 SecurityContext ctx = null;
425
426 try {
427 claims = jwtProcessor.process(parsed, ctx);
428 } catch (BadJOSEException e) {
429 statuses.add("JWT token was signed, but was formatted badly: " + e.toString());
430 continue;
431 } catch (JOSEException e) {
432 statuses.add("JWT token was signed, but was could not be verified: " + e.toString());
433 continue;
434 }
435
436 } else {
437 statuses.add("JWT token is neither plain, signed, not encrypted");
438 continue;
439 }
440
441 if (claims == null) {
442 statuses.add("JWT token found, but claims were missing");
443 continue;
444 }
445
446 final String claim = claims.getStringClaim(claimName);
447
448 if (claim == null) {
449 statuses.add("JWT token found, but claim '" + claimName + "' was missing");
450 continue;
451 }
452
453 if (log.isInfoEnabled()) {
454 log.info("JWT token accepted with " + claimName + " '" + claim + "': request=" + request
455 + ", remoteHost=" + request.getRemoteHost() + ", remoteIP=" + request.getRemoteAddr());
456 }
457
458 return claim;
459
460 } catch (ParseException e) {
461 statuses.add("JWT token found but could not be parsed: " + e.toString());
462 }
463
464 }
465
466
467 for (String status : statuses) {
468 if (log.isFatalEnabled()) {
469 log.fatal(status + ": request=" + request + ", remoteHost=" + request.getRemoteHost() + ", remoteIP="
470 + request.getRemoteAddr());
471 }
472 }
473
474 return null;
475
476 }
477
478
479 @Override
480 protected String getAuthMethod() {
481 return BEARER;
482 }
483
484
485 @Override
486 protected boolean doAuthenticate(Request request, HttpServletResponse response) throws IOException {
487
488 if (checkForCachedAuthentication(request, response, false)) {
489 return true;
490 }
491
492 if (log.isDebugEnabled()) {
493 log.debug("...attempting to process JWT token in request: request=" + request + ", remoteHost="
494 + request.getRemoteHost() + ", remoteIP=" + request.getRemoteAddr());
495 }
496
497 final String sub = parseAuthorization(properties, request, "sub");
498
499 if (sub != null) {
500
501 Principal principal = context.getRealm().authenticate(sub);
502 if (principal != null) {
503 register(request, response, principal, BEARER, sub, null);
504
505 return true;
506 }
507
508 }
509
510 return false;
511
512 }
513
514 private static boolean isBlank(String str) {
515 int strLen;
516 if (str == null || (strLen = str.length()) == 0) {
517 return true;
518 }
519 for (int i = 0; i < strLen; i++) {
520 if (Character.isWhitespace(str.charAt(i)) == false) {
521 return false;
522 }
523 }
524 return true;
525 }
526
527 }