From 19ba450c086de761e31e6991a3c6c80d2b77d418 Mon Sep 17 00:00:00 2001 From: Micah Silverman Date: Fri, 24 Jun 2016 12:14:11 -0400 Subject: [PATCH 01/15] Created jjwt tutorial --- jjwt/.gitignore | 3 + jjwt/pom.xml | 59 ++++++++++++++++++ .../jsonwebtoken/jjwtfun/DemoApplication.java | 12 ++++ .../controller/DynamicJWTController.java | 45 ++++++++++++++ .../controller/FixedJWTController.java | 61 +++++++++++++++++++ .../src/main/resources/application.properties | 0 .../jjwtfun/DemoApplicationTests.java | 18 ++++++ pom.xml | 1 + 8 files changed, 199 insertions(+) create mode 100644 jjwt/.gitignore create mode 100644 jjwt/pom.xml create mode 100644 jjwt/src/main/java/io/jsonwebtoken/jjwtfun/DemoApplication.java create mode 100644 jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/DynamicJWTController.java create mode 100644 jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/FixedJWTController.java create mode 100644 jjwt/src/main/resources/application.properties create mode 100644 jjwt/src/test/java/io/jsonwebtoken/jjwtfun/DemoApplicationTests.java diff --git a/jjwt/.gitignore b/jjwt/.gitignore new file mode 100644 index 0000000000..f83e8cf07c --- /dev/null +++ b/jjwt/.gitignore @@ -0,0 +1,3 @@ +.idea +target +*.iml diff --git a/jjwt/pom.xml b/jjwt/pom.xml new file mode 100644 index 0000000000..0764194803 --- /dev/null +++ b/jjwt/pom.xml @@ -0,0 +1,59 @@ + + + 4.0.0 + + io.jsonwebtoken + jjwtfun + 0.0.1-SNAPSHOT + jar + + jjwtfun + Exercising the JJWT + + + org.springframework.boot + spring-boot-starter-parent + 1.3.5.RELEASE + + + + + UTF-8 + 1.8 + + + + + org.springframework.boot + spring-boot-devtools + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + + org.springframework.boot + spring-boot-starter-test + test + + + + io.jsonwebtoken + jjwt + 0.6.0 + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + diff --git a/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/DemoApplication.java b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/DemoApplication.java new file mode 100644 index 0000000000..2d09c182b6 --- /dev/null +++ b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/DemoApplication.java @@ -0,0 +1,12 @@ +package io.jsonwebtoken.jjwtfun; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class DemoApplication { + + public static void main(String[] args) { + SpringApplication.run(DemoApplication.class, args); + } +} diff --git a/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/DynamicJWTController.java b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/DynamicJWTController.java new file mode 100644 index 0000000000..e1d98bb199 --- /dev/null +++ b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/DynamicJWTController.java @@ -0,0 +1,45 @@ +package io.jsonwebtoken.jjwtfun.controller; + +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +import java.io.UnsupportedEncodingException; +import java.util.AbstractMap.SimpleEntry; +import java.util.Collections; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@RestController +public class DynamicJWTController { + @Value("#{ @environment['jjwtfun.secret'] ?: 'secret' }") + String secret; + + Map map = Collections.unmodifiableMap(Stream.of( + new SimpleEntry<>("iss", ""), + new SimpleEntry<>("a", ""), + new SimpleEntry<>("b", ""), + new SimpleEntry<>("c", ""), + new SimpleEntry<>("d", ""), + new SimpleEntry<>("e", ""), + new SimpleEntry<>("f", ""), + new SimpleEntry<>("g", ""), + new SimpleEntry<>("h", "")) + .collect(Collectors.toMap((e) -> e.getKey(), (e) -> e.getValue()))); + + @RequestMapping(value = "/dynamic-builder", method = RequestMethod.POST) + public String dynamicBuilder(@RequestBody Map claims) throws UnsupportedEncodingException { + return Jwts.builder() + .setClaims(claims) + .signWith( + SignatureAlgorithm.HS256, + secret.getBytes("UTF-8") + ) + .compact(); + } +} diff --git a/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/FixedJWTController.java b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/FixedJWTController.java new file mode 100644 index 0000000000..7a648063ac --- /dev/null +++ b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/FixedJWTController.java @@ -0,0 +1,61 @@ +package io.jsonwebtoken.jjwtfun.controller; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.SignatureException; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import java.io.UnsupportedEncodingException; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +@RestController +public class FixedJWTController { + + @RequestMapping("/fixed-builder") + public String fixedBuilder() throws UnsupportedEncodingException { + + String jws = Jwts.builder() + .setSubject("msilverman") + .setExpiration(Date.from(Instant.now().plus(1, ChronoUnit.DAYS))) + .claim("name", "Micah Silverman") + .claim("scope", "admins") + .signWith( + SignatureAlgorithm.HS256, + "secret".getBytes("UTF-8") + ) + .compact(); + + return jws; + } + + @RequestMapping("/fixed-parser") + public Jws fixedParser(@RequestParam String jws) throws UnsupportedEncodingException { + Jws claims = Jwts.parser() + .setSigningKey("secret".getBytes("UTF-8")) + .parseClaimsJws(jws); + + return claims; + } + + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler({SignatureException.class, MalformedJwtException.class}) + public Map exception(Exception e) { + Map response = new HashMap<>(); + response.put("status", "ERROR"); + response.put("message", e.getMessage()); + response.put("exception-type", e.getClass().getName()); + return response; + } +} diff --git a/jjwt/src/main/resources/application.properties b/jjwt/src/main/resources/application.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jjwt/src/test/java/io/jsonwebtoken/jjwtfun/DemoApplicationTests.java b/jjwt/src/test/java/io/jsonwebtoken/jjwtfun/DemoApplicationTests.java new file mode 100644 index 0000000000..7e5b9b78f1 --- /dev/null +++ b/jjwt/src/test/java/io/jsonwebtoken/jjwtfun/DemoApplicationTests.java @@ -0,0 +1,18 @@ +package io.jsonwebtoken.jjwtfun; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; + +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = DemoApplication.class) +@WebAppConfiguration +public class DemoApplicationTests { + + @Test + public void contextLoads() { + } + +} diff --git a/pom.xml b/pom.xml index 75281ce80d..9e1d7e44c2 100644 --- a/pom.xml +++ b/pom.xml @@ -22,6 +22,7 @@ httpclient jackson javaxval + jjwt jooq-spring json-path mockito From bf9c5b3c917a261d0f026932acb8e6ee4240480b Mon Sep 17 00:00:00 2001 From: Micah Silverman Date: Fri, 24 Jun 2016 14:53:14 -0400 Subject: [PATCH 02/15] Added HomeController with usage. Updated static and dynamic jwt builders. --- ...plication.java => JJWTFunApplication.java} | 4 +- .../controller/DynamicJWTController.java | 63 +++++++++++++------ .../jjwtfun/controller/HomeController.java | 27 ++++++++ ...ntroller.java => StaticJWTController.java} | 12 ++-- .../jjwtfun/DemoApplicationTests.java | 2 +- 5 files changed, 82 insertions(+), 26 deletions(-) rename jjwt/src/main/java/io/jsonwebtoken/jjwtfun/{DemoApplication.java => JJWTFunApplication.java} (71%) create mode 100644 jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/HomeController.java rename jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/{FixedJWTController.java => StaticJWTController.java} (84%) diff --git a/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/DemoApplication.java b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/JJWTFunApplication.java similarity index 71% rename from jjwt/src/main/java/io/jsonwebtoken/jjwtfun/DemoApplication.java rename to jjwt/src/main/java/io/jsonwebtoken/jjwtfun/JJWTFunApplication.java index 2d09c182b6..7189617d55 100644 --- a/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/DemoApplication.java +++ b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/JJWTFunApplication.java @@ -4,9 +4,9 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication -public class DemoApplication { +public class JJWTFunApplication { public static void main(String[] args) { - SpringApplication.run(DemoApplication.class, args); + SpringApplication.run(JJWTFunApplication.class, args); } } diff --git a/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/DynamicJWTController.java b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/DynamicJWTController.java index e1d98bb199..fff7e1d6a2 100644 --- a/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/DynamicJWTController.java +++ b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/DynamicJWTController.java @@ -1,39 +1,27 @@ package io.jsonwebtoken.jjwtfun.controller; +import io.jsonwebtoken.JwtBuilder; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; import java.io.UnsupportedEncodingException; -import java.util.AbstractMap.SimpleEntry; -import java.util.Collections; +import java.time.Instant; +import java.util.Date; import java.util.Map; -import java.util.stream.Collectors; -import java.util.stream.Stream; + +import static org.springframework.web.bind.annotation.RequestMethod.POST; @RestController public class DynamicJWTController { @Value("#{ @environment['jjwtfun.secret'] ?: 'secret' }") String secret; - Map map = Collections.unmodifiableMap(Stream.of( - new SimpleEntry<>("iss", ""), - new SimpleEntry<>("a", ""), - new SimpleEntry<>("b", ""), - new SimpleEntry<>("c", ""), - new SimpleEntry<>("d", ""), - new SimpleEntry<>("e", ""), - new SimpleEntry<>("f", ""), - new SimpleEntry<>("g", ""), - new SimpleEntry<>("h", "")) - .collect(Collectors.toMap((e) -> e.getKey(), (e) -> e.getValue()))); - - @RequestMapping(value = "/dynamic-builder", method = RequestMethod.POST) - public String dynamicBuilder(@RequestBody Map claims) throws UnsupportedEncodingException { + @RequestMapping(value = "/dynamic-builder-general", method = POST) + public String dynamicBuilderGeneric(@RequestBody Map claims) throws UnsupportedEncodingException { return Jwts.builder() .setClaims(claims) .signWith( @@ -42,4 +30,41 @@ public class DynamicJWTController { ) .compact(); } + + @RequestMapping(value = "/dynamic-builder-specific", method = POST) + public String dynamicBuilderSpecific(@RequestBody Map claims) throws UnsupportedEncodingException { + JwtBuilder builder = Jwts.builder(); + + claims.forEach((key, value) -> { + switch (key) { + case "iss": + builder.setIssuer((String)value); + break; + case "sub": + builder.setSubject((String)value); + break; + case "aud": + builder.setAudience((String)value); + break; + case "exp": + builder.setExpiration(Date.from(Instant.ofEpochSecond(Long.parseLong((String)value)))); + break; + case "nbf": + builder.setNotBefore(Date.from(Instant.ofEpochSecond(Long.parseLong((String)value)))); + break; + case "iat": + builder.setIssuedAt(Date.from(Instant.ofEpochSecond(Long.parseLong((String)value)))); + break; + case "jti": + builder.setId((String)value); + break; + default: + builder.claim(key, value); + } + }); + + builder.signWith(SignatureAlgorithm.HS256, secret.getBytes("UTF-8")); + + return builder.compact(); + } } diff --git a/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/HomeController.java b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/HomeController.java new file mode 100644 index 0000000000..d6717e383f --- /dev/null +++ b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/HomeController.java @@ -0,0 +1,27 @@ +package io.jsonwebtoken.jjwtfun.controller; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletRequest; + +@RestController +public class HomeController { + + @RequestMapping("/") + public String home(HttpServletRequest req) { + String requestUrl = getUrl(req); + return "Available commands (assumes httpie - https://github.com/jkbrzt/httpie):\n" + + " http " + requestUrl + "/\n\tThis usage\n" + + " http " + requestUrl + "/static-builder\n\tbuild JWT from hardcoded claims\n" + + " http " + requestUrl + "/dynamic-builder-general claim-1=value-1 ... [claim-n=value-n]\n\tbuild JWT from passed in claims (using general claims map)\n" + + " http " + requestUrl + "/dynamic-builder-specific claim-1=value-1 ... [claim-n=value-n]\n\tbuild JWT from passed in claims (using specific claims methods)\n" + + " http " + requestUrl + "/parser?jwt=\n\tParse passed in JWT\n"; + } + + private String getUrl(HttpServletRequest req) { + return req.getScheme() + "://" + + req.getServerName() + + ((req.getServerPort() == 80 || req.getServerPort() == 443) ? "" : ":" + req.getServerPort()); + } +} diff --git a/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/FixedJWTController.java b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/StaticJWTController.java similarity index 84% rename from jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/FixedJWTController.java rename to jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/StaticJWTController.java index 7a648063ac..6f98ffcd94 100644 --- a/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/FixedJWTController.java +++ b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/StaticJWTController.java @@ -9,6 +9,7 @@ import io.jsonwebtoken.SignatureException; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; @@ -20,10 +21,13 @@ import java.util.Date; import java.util.HashMap; import java.util.Map; -@RestController -public class FixedJWTController { +import static org.springframework.web.bind.annotation.RequestMethod.GET; +import static org.springframework.web.bind.annotation.RequestMethod.POST; - @RequestMapping("/fixed-builder") +@RestController +public class StaticJWTController { + + @RequestMapping(value = "/static-builder", method = POST) public String fixedBuilder() throws UnsupportedEncodingException { String jws = Jwts.builder() @@ -40,7 +44,7 @@ public class FixedJWTController { return jws; } - @RequestMapping("/fixed-parser") + @RequestMapping(value = "/parser", method = GET) public Jws fixedParser(@RequestParam String jws) throws UnsupportedEncodingException { Jws claims = Jwts.parser() .setSigningKey("secret".getBytes("UTF-8")) diff --git a/jjwt/src/test/java/io/jsonwebtoken/jjwtfun/DemoApplicationTests.java b/jjwt/src/test/java/io/jsonwebtoken/jjwtfun/DemoApplicationTests.java index 7e5b9b78f1..357d91ed73 100644 --- a/jjwt/src/test/java/io/jsonwebtoken/jjwtfun/DemoApplicationTests.java +++ b/jjwt/src/test/java/io/jsonwebtoken/jjwtfun/DemoApplicationTests.java @@ -7,7 +7,7 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; @RunWith(SpringJUnit4ClassRunner.class) -@SpringApplicationConfiguration(classes = DemoApplication.class) +@SpringApplicationConfiguration(classes = JJWTFunApplication.class) @WebAppConfiguration public class DemoApplicationTests { From df9e4d7ef5d74b5d76a2daef792450a056dc9651 Mon Sep 17 00:00:00 2001 From: Micah Silverman Date: Sat, 25 Jun 2016 21:11:56 -0400 Subject: [PATCH 03/15] Updated date method calls. --- .../io/jsonwebtoken/jjwtfun/controller/HomeController.java | 2 +- .../jjwtfun/controller/StaticJWTController.java | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/HomeController.java b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/HomeController.java index d6717e383f..fabc6f1f2a 100644 --- a/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/HomeController.java +++ b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/HomeController.java @@ -12,7 +12,7 @@ public class HomeController { public String home(HttpServletRequest req) { String requestUrl = getUrl(req); return "Available commands (assumes httpie - https://github.com/jkbrzt/httpie):\n" + - " http " + requestUrl + "/\n\tThis usage\n" + + " http " + requestUrl + "/\n\tThis usage message\n" + " http " + requestUrl + "/static-builder\n\tbuild JWT from hardcoded claims\n" + " http " + requestUrl + "/dynamic-builder-general claim-1=value-1 ... [claim-n=value-n]\n\tbuild JWT from passed in claims (using general claims map)\n" + " http " + requestUrl + "/dynamic-builder-specific claim-1=value-1 ... [claim-n=value-n]\n\tbuild JWT from passed in claims (using specific claims methods)\n" + diff --git a/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/StaticJWTController.java b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/StaticJWTController.java index 6f98ffcd94..114900285f 100644 --- a/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/StaticJWTController.java +++ b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/StaticJWTController.java @@ -27,14 +27,16 @@ import static org.springframework.web.bind.annotation.RequestMethod.POST; @RestController public class StaticJWTController { - @RequestMapping(value = "/static-builder", method = POST) + @RequestMapping(value = "/static-builder", method = GET) public String fixedBuilder() throws UnsupportedEncodingException { String jws = Jwts.builder() + .setIssuer("Stormpath") .setSubject("msilverman") - .setExpiration(Date.from(Instant.now().plus(1, ChronoUnit.DAYS))) .claim("name", "Micah Silverman") .claim("scope", "admins") + .setIssuedAt(Date.from(Instant.ofEpochSecond(1466796822))) // Fri Jun 24 2016 15:33:42 GMT-0400 (EDT) + .setExpiration(Date.from(Instant.ofEpochSecond(1466883222))) // Sat Jun 25 2016 15:33:42 GMT-0400 (EDT) .signWith( SignatureAlgorithm.HS256, "secret".getBytes("UTF-8") From 29a33c3407035423a79ead55f14692647ccead0f Mon Sep 17 00:00:00 2001 From: Micah Silverman Date: Mon, 27 Jun 2016 02:30:09 -0400 Subject: [PATCH 04/15] Made BaseController to provide uniform exception response. Made JwtResponse for uniform responses. Added ensureType method to enforce type on registered claims. --- .../jjwtfun/JJWTFunApplication.java | 6 +- .../jjwtfun/controller/BaseController.java | 23 ++++++ .../controller/DynamicJWTController.java | 51 ++++++++++---- .../controller/StaticJWTController.java | 35 +++------- .../jjwtfun/model/JwtResponse.java | 70 +++++++++++++++++++ 5 files changed, 143 insertions(+), 42 deletions(-) create mode 100644 jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/BaseController.java create mode 100644 jjwt/src/main/java/io/jsonwebtoken/jjwtfun/model/JwtResponse.java diff --git a/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/JJWTFunApplication.java b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/JJWTFunApplication.java index 7189617d55..5c106aac70 100644 --- a/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/JJWTFunApplication.java +++ b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/JJWTFunApplication.java @@ -6,7 +6,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class JJWTFunApplication { - public static void main(String[] args) { - SpringApplication.run(JJWTFunApplication.class, args); - } + public static void main(String[] args) { + SpringApplication.run(JJWTFunApplication.class, args); + } } diff --git a/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/BaseController.java b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/BaseController.java new file mode 100644 index 0000000000..e1e195c6ab --- /dev/null +++ b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/BaseController.java @@ -0,0 +1,23 @@ +package io.jsonwebtoken.jjwtfun.controller; + +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.SignatureException; +import io.jsonwebtoken.jjwtfun.model.JwtResponse; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; + +public class BaseController { + + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler({SignatureException.class, MalformedJwtException.class, JwtException.class}) + public JwtResponse exception(Exception e) { + JwtResponse response = new JwtResponse(); + response.setStatus(JwtResponse.Status.ERROR); + response.setMessage(e.getMessage()); + response.setExceptionType(e.getClass().getName()); + + return response; + } +} diff --git a/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/DynamicJWTController.java b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/DynamicJWTController.java index fff7e1d6a2..72bc491b00 100644 --- a/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/DynamicJWTController.java +++ b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/DynamicJWTController.java @@ -1,8 +1,10 @@ package io.jsonwebtoken.jjwtfun.controller; import io.jsonwebtoken.JwtBuilder; +import io.jsonwebtoken.JwtException; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.jjwtfun.model.JwtResponse; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -16,47 +18,55 @@ import java.util.Map; import static org.springframework.web.bind.annotation.RequestMethod.POST; @RestController -public class DynamicJWTController { +public class DynamicJWTController extends BaseController { @Value("#{ @environment['jjwtfun.secret'] ?: 'secret' }") String secret; @RequestMapping(value = "/dynamic-builder-general", method = POST) - public String dynamicBuilderGeneric(@RequestBody Map claims) throws UnsupportedEncodingException { - return Jwts.builder() + public JwtResponse dynamicBuilderGeneric(@RequestBody Map claims) throws UnsupportedEncodingException { + String jws = Jwts.builder() .setClaims(claims) .signWith( SignatureAlgorithm.HS256, secret.getBytes("UTF-8") ) .compact(); + return new JwtResponse(jws); } @RequestMapping(value = "/dynamic-builder-specific", method = POST) - public String dynamicBuilderSpecific(@RequestBody Map claims) throws UnsupportedEncodingException { + public JwtResponse dynamicBuilderSpecific(@RequestBody Map claims) throws UnsupportedEncodingException { JwtBuilder builder = Jwts.builder(); claims.forEach((key, value) -> { switch (key) { case "iss": - builder.setIssuer((String)value); + ensureType(key, value, String.class); + builder.setIssuer((String) value); break; case "sub": - builder.setSubject((String)value); + ensureType(key, value, String.class); + builder.setSubject((String) value); break; case "aud": - builder.setAudience((String)value); + ensureType(key, value, String.class); + builder.setAudience((String) value); break; case "exp": - builder.setExpiration(Date.from(Instant.ofEpochSecond(Long.parseLong((String)value)))); + value = ensureType(key, value, Long.class); + builder.setExpiration(Date.from(Instant.ofEpochSecond((Long) value))); break; case "nbf": - builder.setNotBefore(Date.from(Instant.ofEpochSecond(Long.parseLong((String)value)))); + value = ensureType(key, value, Long.class); + builder.setNotBefore(Date.from(Instant.ofEpochSecond((Long) value))); break; case "iat": - builder.setIssuedAt(Date.from(Instant.ofEpochSecond(Long.parseLong((String)value)))); + value = ensureType(key, value, Long.class); + builder.setIssuedAt(Date.from(Instant.ofEpochSecond((Long) value))); break; case "jti": - builder.setId((String)value); + ensureType(key, value, String.class); + builder.setId((String) value); break; default: builder.claim(key, value); @@ -65,6 +75,23 @@ public class DynamicJWTController { builder.signWith(SignatureAlgorithm.HS256, secret.getBytes("UTF-8")); - return builder.compact(); + return new JwtResponse(builder.compact()); + } + + private Object ensureType(String registeredClaim, Object value, Class expectedType) { + // we want to promote Integers to Longs in this case + if (expectedType == Long.class && value instanceof Integer) { + value = ((Integer) value).longValue(); + } + + boolean isCorrectType = expectedType.isInstance(value); + + if (!isCorrectType) { + String msg = "Expected type: " + expectedType.getCanonicalName() + " for registered claim: '" + + registeredClaim + "', but got value: " + value + " of type: " + value.getClass().getCanonicalName(); + throw new JwtException(msg); + } + + return value; } } diff --git a/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/StaticJWTController.java b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/StaticJWTController.java index 114900285f..960ac46043 100644 --- a/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/StaticJWTController.java +++ b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/StaticJWTController.java @@ -3,65 +3,46 @@ package io.jsonwebtoken.jjwtfun.controller; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jws; import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.MalformedJwtException; import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.SignatureException; -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ExceptionHandler; +import io.jsonwebtoken.jjwtfun.model.JwtResponse; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import java.io.UnsupportedEncodingException; import java.time.Instant; -import java.time.temporal.ChronoUnit; import java.util.Date; -import java.util.HashMap; -import java.util.Map; import static org.springframework.web.bind.annotation.RequestMethod.GET; -import static org.springframework.web.bind.annotation.RequestMethod.POST; @RestController -public class StaticJWTController { +public class StaticJWTController extends BaseController { @RequestMapping(value = "/static-builder", method = GET) - public String fixedBuilder() throws UnsupportedEncodingException { + public JwtResponse fixedBuilder() throws UnsupportedEncodingException { String jws = Jwts.builder() .setIssuer("Stormpath") .setSubject("msilverman") .claim("name", "Micah Silverman") .claim("scope", "admins") - .setIssuedAt(Date.from(Instant.ofEpochSecond(1466796822))) // Fri Jun 24 2016 15:33:42 GMT-0400 (EDT) - .setExpiration(Date.from(Instant.ofEpochSecond(1466883222))) // Sat Jun 25 2016 15:33:42 GMT-0400 (EDT) + .setIssuedAt(Date.from(Instant.ofEpochSecond(1466796822L))) // Fri Jun 24 2016 15:33:42 GMT-0400 (EDT) + .setExpiration(Date.from(Instant.ofEpochSecond(4622470422L))) // Sat Jun 24 2116 15:33:42 GMT-0400 (EDT) .signWith( SignatureAlgorithm.HS256, "secret".getBytes("UTF-8") ) .compact(); - return jws; + return new JwtResponse(jws); } @RequestMapping(value = "/parser", method = GET) - public Jws fixedParser(@RequestParam String jws) throws UnsupportedEncodingException { + public JwtResponse fixedParser(@RequestParam String jws) throws UnsupportedEncodingException { Jws claims = Jwts.parser() .setSigningKey("secret".getBytes("UTF-8")) .parseClaimsJws(jws); - return claims; - } - - @ResponseStatus(HttpStatus.BAD_REQUEST) - @ExceptionHandler({SignatureException.class, MalformedJwtException.class}) - public Map exception(Exception e) { - Map response = new HashMap<>(); - response.put("status", "ERROR"); - response.put("message", e.getMessage()); - response.put("exception-type", e.getClass().getName()); - return response; + return new JwtResponse(claims); } } diff --git a/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/model/JwtResponse.java b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/model/JwtResponse.java new file mode 100644 index 0000000000..4c664bc5f7 --- /dev/null +++ b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/model/JwtResponse.java @@ -0,0 +1,70 @@ +package io.jsonwebtoken.jjwtfun.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public class JwtResponse { + private String message; + private Status status; + private String exceptionType; + private String jwt; + private Jws claims; + + public enum Status { + SUCCESS, ERROR + } + + public JwtResponse() {} + + public JwtResponse(String jwt) { + this.jwt = jwt; + this.status = Status.SUCCESS; + } + + public JwtResponse(Jws claims) { + this.claims = claims; + this.status = Status.SUCCESS; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public Status getStatus() { + return status; + } + + public void setStatus(Status status) { + this.status = status; + } + + public String getExceptionType() { + return exceptionType; + } + + public void setExceptionType(String exceptionType) { + this.exceptionType = exceptionType; + } + + public String getJwt() { + return jwt; + } + + public void setJwt(String jwt) { + this.jwt = jwt; + } + + public Jws getClaims() { + return claims; + } + + public void setClaims(Jws claims) { + this.claims = claims; + } +} From 14905fae11672ad54eee5fdaf2bddee28abccc7a Mon Sep 17 00:00:00 2001 From: Micah Silverman Date: Mon, 27 Jun 2016 09:05:44 -0400 Subject: [PATCH 05/15] Simplified type checking. --- .../controller/DynamicJWTController.java | 33 ++++++++----------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/DynamicJWTController.java b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/DynamicJWTController.java index 72bc491b00..184b4b1055 100644 --- a/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/DynamicJWTController.java +++ b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/DynamicJWTController.java @@ -41,31 +41,31 @@ public class DynamicJWTController extends BaseController { claims.forEach((key, value) -> { switch (key) { case "iss": - ensureType(key, value, String.class); + ensureType(key, value, String.class); builder.setIssuer((String) value); break; case "sub": - ensureType(key, value, String.class); + ensureType(key, value, String.class); builder.setSubject((String) value); break; case "aud": - ensureType(key, value, String.class); + ensureType(key, value, String.class); builder.setAudience((String) value); break; case "exp": - value = ensureType(key, value, Long.class); - builder.setExpiration(Date.from(Instant.ofEpochSecond((Long) value))); + ensureType(key, value, Long.class); + builder.setExpiration(Date.from(Instant.ofEpochSecond(Long.parseLong(value.toString())))); break; case "nbf": - value = ensureType(key, value, Long.class); - builder.setNotBefore(Date.from(Instant.ofEpochSecond((Long) value))); + ensureType(key, value, Long.class); + builder.setNotBefore(Date.from(Instant.ofEpochSecond(Long.parseLong(value.toString())))); break; case "iat": - value = ensureType(key, value, Long.class); - builder.setIssuedAt(Date.from(Instant.ofEpochSecond((Long) value))); + ensureType(key, value, Long.class); + builder.setIssuedAt(Date.from(Instant.ofEpochSecond(Long.parseLong(value.toString())))); break; case "jti": - ensureType(key, value, String.class); + ensureType(key, value, String.class); builder.setId((String) value); break; default: @@ -78,20 +78,15 @@ public class DynamicJWTController extends BaseController { return new JwtResponse(builder.compact()); } - private Object ensureType(String registeredClaim, Object value, Class expectedType) { - // we want to promote Integers to Longs in this case - if (expectedType == Long.class && value instanceof Integer) { - value = ((Integer) value).longValue(); - } - - boolean isCorrectType = expectedType.isInstance(value); + private void ensureType(String registeredClaim, Object value, Class expectedType) { + boolean isCorrectType = + expectedType.isInstance(value) || + expectedType == Long.class && value instanceof Integer; if (!isCorrectType) { String msg = "Expected type: " + expectedType.getCanonicalName() + " for registered claim: '" + registeredClaim + "', but got value: " + value + " of type: " + value.getClass().getCanonicalName(); throw new JwtException(msg); } - - return value; } } From 6a057f33b1e8cc2d717997cf82979eeb57a6f491 Mon Sep 17 00:00:00 2001 From: Micah Silverman Date: Mon, 27 Jun 2016 09:21:42 -0400 Subject: [PATCH 06/15] Set configurable secret for parsing. --- .../jjwtfun/controller/StaticJWTController.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/StaticJWTController.java b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/StaticJWTController.java index 960ac46043..9bf4ab2e45 100644 --- a/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/StaticJWTController.java +++ b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/StaticJWTController.java @@ -5,6 +5,7 @@ import io.jsonwebtoken.Jws; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.jjwtfun.model.JwtResponse; +import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @@ -18,6 +19,9 @@ import static org.springframework.web.bind.annotation.RequestMethod.GET; @RestController public class StaticJWTController extends BaseController { + @Value("#{ @environment['jjwtfun.secret'] ?: 'secret' }") + String secret; + @RequestMapping(value = "/static-builder", method = GET) public JwtResponse fixedBuilder() throws UnsupportedEncodingException { @@ -38,10 +42,10 @@ public class StaticJWTController extends BaseController { } @RequestMapping(value = "/parser", method = GET) - public JwtResponse fixedParser(@RequestParam String jws) throws UnsupportedEncodingException { + public JwtResponse parser(@RequestParam String jwt) throws UnsupportedEncodingException { Jws claims = Jwts.parser() - .setSigningKey("secret".getBytes("UTF-8")) - .parseClaimsJws(jws); + .setSigningKey(secret.getBytes("UTF-8")) + .parseClaimsJws(jwt); return new JwtResponse(claims); } From f1630a0199f96fb179c1279e9db1b32311262509 Mon Sep 17 00:00:00 2001 From: Micah Silverman Date: Mon, 27 Jun 2016 13:26:58 -0400 Subject: [PATCH 07/15] Added support for JWT CSRF in Spring Security --- jjwt/pom.xml | 6 ++ .../jjwtfun/config/CSRFConfig.java | 25 ++++++ .../config/JWTCsrfTokenRepository.java | 76 +++++++++++++++++++ .../jjwtfun/config/WebSecurityConfig.java | 23 ++++++ .../jjwtfun/controller/FormController.java | 26 +++++++ .../resources/templates/fragments/head.html | 21 +++++ .../templates/jwt-csrf-form-result.html | 18 +++++ .../resources/templates/jwt-csrf-form.html | 18 +++++ 8 files changed, 213 insertions(+) create mode 100644 jjwt/src/main/java/io/jsonwebtoken/jjwtfun/config/CSRFConfig.java create mode 100644 jjwt/src/main/java/io/jsonwebtoken/jjwtfun/config/JWTCsrfTokenRepository.java create mode 100644 jjwt/src/main/java/io/jsonwebtoken/jjwtfun/config/WebSecurityConfig.java create mode 100644 jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/FormController.java create mode 100644 jjwt/src/main/resources/templates/fragments/head.html create mode 100644 jjwt/src/main/resources/templates/jwt-csrf-form-result.html create mode 100644 jjwt/src/main/resources/templates/jwt-csrf-form.html diff --git a/jjwt/pom.xml b/jjwt/pom.xml index 0764194803..24f1c5c5ab 100644 --- a/jjwt/pom.xml +++ b/jjwt/pom.xml @@ -28,11 +28,17 @@ org.springframework.boot spring-boot-devtools + org.springframework.boot spring-boot-starter-thymeleaf + + org.springframework.boot + spring-boot-starter-security + + org.springframework.boot spring-boot-starter-test diff --git a/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/config/CSRFConfig.java b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/config/CSRFConfig.java new file mode 100644 index 0000000000..1a8f05359c --- /dev/null +++ b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/config/CSRFConfig.java @@ -0,0 +1,25 @@ +package io.jsonwebtoken.jjwtfun.config; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.web.csrf.CsrfTokenRepository; + +@Configuration +public class CSRFConfig { + + + private static final Logger log = LoggerFactory.getLogger(CSRFConfig.class); + + @Value("#{ @environment['jjwtfun.secret'] ?: 'secret' }") + String secret; + + @Bean + @ConditionalOnMissingBean + public CsrfTokenRepository jwtCsrfTokenRepository() { + return new JWTCsrfTokenRepository(secret); + } +} diff --git a/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/config/JWTCsrfTokenRepository.java b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/config/JWTCsrfTokenRepository.java new file mode 100644 index 0000000000..2489beb76e --- /dev/null +++ b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/config/JWTCsrfTokenRepository.java @@ -0,0 +1,76 @@ +package io.jsonwebtoken.jjwtfun.config; + +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.web.csrf.CsrfToken; +import org.springframework.security.web.csrf.CsrfTokenRepository; +import org.springframework.security.web.csrf.DefaultCsrfToken; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.io.UnsupportedEncodingException; +import java.util.Date; +import java.util.UUID; + +public class JWTCsrfTokenRepository implements CsrfTokenRepository { + + private static final Logger log = LoggerFactory.getLogger(JWTCsrfTokenRepository.class); + private static final String DEFAULT_CSRF_TOKEN_ATTR_NAME = CSRFConfig.class.getName().concat(".CSRF_TOKEN"); + + private String secret; + + public JWTCsrfTokenRepository(String secret) { + this.secret = secret; + } + + @Override + public CsrfToken generateToken(HttpServletRequest request) { + + String id = UUID.randomUUID().toString().replace("-", ""); + + Date now = new Date(); + Date exp = new Date(System.currentTimeMillis() + (1000*60)); // 1 minute + + String token; + try { + token = Jwts.builder() + .setId(id) + .setIssuedAt(now) + .setNotBefore(now) + .setExpiration(exp) + .signWith(SignatureAlgorithm.HS256, secret.getBytes("UTF-8")) + .compact(); + } catch (UnsupportedEncodingException e) { + log.error("Unable to create CSRf JWT: {}", e.getMessage(), e); + token = id; + } + + return new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", token); + } + + @Override + public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) { + if (token == null) { + HttpSession session = request.getSession(false); + if (session != null) { + session.removeAttribute(DEFAULT_CSRF_TOKEN_ATTR_NAME); + } + } + else { + HttpSession session = request.getSession(); + session.setAttribute(DEFAULT_CSRF_TOKEN_ATTR_NAME, token); + } + } + + @Override + public CsrfToken loadToken(HttpServletRequest request) { + HttpSession session = request.getSession(false); + if (session == null) { + return null; + } + return (CsrfToken) session.getAttribute(DEFAULT_CSRF_TOKEN_ATTR_NAME); + } +} diff --git a/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/config/WebSecurityConfig.java b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/config/WebSecurityConfig.java new file mode 100644 index 0000000000..2715990d38 --- /dev/null +++ b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/config/WebSecurityConfig.java @@ -0,0 +1,23 @@ +package io.jsonwebtoken.jjwtfun.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.web.csrf.CsrfTokenRepository; + +@Configuration +public class WebSecurityConfig extends WebSecurityConfigurerAdapter { + + @Autowired + CsrfTokenRepository jwtCsrfTokenRepository; + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .csrf().csrfTokenRepository(jwtCsrfTokenRepository).and() + .authorizeRequests() + .antMatchers("/**") + .permitAll(); + } +} diff --git a/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/FormController.java b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/FormController.java new file mode 100644 index 0000000000..178af2f48d --- /dev/null +++ b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/FormController.java @@ -0,0 +1,26 @@ +package io.jsonwebtoken.jjwtfun.controller; + +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; + +import static org.springframework.web.bind.annotation.RequestMethod.GET; +import static org.springframework.web.bind.annotation.RequestMethod.POST; + +@Controller +public class FormController { + + @RequestMapping(value = "/jwt-csrf-form", method = GET) + public String csrfFormGet() { + return "jwt-csrf-form"; + } + + @RequestMapping(value = "/jwt-csrf-form", method = POST) + public String csrfFormPost(@RequestParam(name = "_csrf") String csrf, Model model) { + + model.addAttribute("csrf", csrf); + + return "jwt-csrf-form-result"; + } +} diff --git a/jjwt/src/main/resources/templates/fragments/head.html b/jjwt/src/main/resources/templates/fragments/head.html new file mode 100644 index 0000000000..ce76b0e655 --- /dev/null +++ b/jjwt/src/main/resources/templates/fragments/head.html @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + +

Nothing to see here, move along.

+ + \ No newline at end of file diff --git a/jjwt/src/main/resources/templates/jwt-csrf-form-result.html b/jjwt/src/main/resources/templates/jwt-csrf-form-result.html new file mode 100644 index 0000000000..6547459c27 --- /dev/null +++ b/jjwt/src/main/resources/templates/jwt-csrf-form-result.html @@ -0,0 +1,18 @@ + + + + + + +
+
+
+

You made it!

+
+

BLARG

+
+
+
+
+ + \ No newline at end of file diff --git a/jjwt/src/main/resources/templates/jwt-csrf-form.html b/jjwt/src/main/resources/templates/jwt-csrf-form.html new file mode 100644 index 0000000000..dcdb664647 --- /dev/null +++ b/jjwt/src/main/resources/templates/jwt-csrf-form.html @@ -0,0 +1,18 @@ + + + + + + +
+
+
+

+

+ +
+
+
+
+ + \ No newline at end of file From 38e829ef35a09e705c95c639da6410a157b90fdc Mon Sep 17 00:00:00 2001 From: Micah Silverman Date: Mon, 27 Jun 2016 18:51:07 -0400 Subject: [PATCH 08/15] Added JWT CSRF expired handling. --- .../jjwtfun/config/CSRFConfig.java | 5 -- .../config/JWTCsrfTokenRepository.java | 6 +- .../jjwtfun/config/WebSecurityConfig.java | 57 +++++++++++++++++-- .../jjwtfun/controller/FormController.java | 7 ++- .../main/resources/templates/expired-jwt.html | 17 ++++++ .../resources/templates/fragments/head.html | 11 ++++ .../templates/jwt-csrf-form-result.html | 2 +- .../resources/templates/jwt-csrf-form.html | 2 +- 8 files changed, 91 insertions(+), 16 deletions(-) create mode 100644 jjwt/src/main/resources/templates/expired-jwt.html diff --git a/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/config/CSRFConfig.java b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/config/CSRFConfig.java index 1a8f05359c..b3d3bdedfd 100644 --- a/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/config/CSRFConfig.java +++ b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/config/CSRFConfig.java @@ -1,7 +1,5 @@ package io.jsonwebtoken.jjwtfun.config; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; @@ -11,9 +9,6 @@ import org.springframework.security.web.csrf.CsrfTokenRepository; @Configuration public class CSRFConfig { - - private static final Logger log = LoggerFactory.getLogger(CSRFConfig.class); - @Value("#{ @environment['jjwtfun.secret'] ?: 'secret' }") String secret; diff --git a/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/config/JWTCsrfTokenRepository.java b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/config/JWTCsrfTokenRepository.java index 2489beb76e..0a68e4624d 100644 --- a/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/config/JWTCsrfTokenRepository.java +++ b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/config/JWTCsrfTokenRepository.java @@ -17,9 +17,9 @@ import java.util.UUID; public class JWTCsrfTokenRepository implements CsrfTokenRepository { - private static final Logger log = LoggerFactory.getLogger(JWTCsrfTokenRepository.class); private static final String DEFAULT_CSRF_TOKEN_ATTR_NAME = CSRFConfig.class.getName().concat(".CSRF_TOKEN"); + private static final Logger log = LoggerFactory.getLogger(JWTCsrfTokenRepository.class); private String secret; public JWTCsrfTokenRepository(String secret) { @@ -32,7 +32,7 @@ public class JWTCsrfTokenRepository implements CsrfTokenRepository { String id = UUID.randomUUID().toString().replace("-", ""); Date now = new Date(); - Date exp = new Date(System.currentTimeMillis() + (1000*60)); // 1 minute + Date exp = new Date(System.currentTimeMillis() + (1000*30)); // 30 seconds String token; try { @@ -68,7 +68,7 @@ public class JWTCsrfTokenRepository implements CsrfTokenRepository { @Override public CsrfToken loadToken(HttpServletRequest request) { HttpSession session = request.getSession(false); - if (session == null) { + if (session == null || "GET".equals(request.getMethod())) { return null; } return (CsrfToken) session.getAttribute(DEFAULT_CSRF_TOKEN_ATTR_NAME); diff --git a/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/config/WebSecurityConfig.java b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/config/WebSecurityConfig.java index 2715990d38..c09e8cd179 100644 --- a/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/config/WebSecurityConfig.java +++ b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/config/WebSecurityConfig.java @@ -1,23 +1,72 @@ package io.jsonwebtoken.jjwtfun.config; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.web.csrf.CsrfFilter; +import org.springframework.security.web.csrf.CsrfToken; import org.springframework.security.web.csrf.CsrfTokenRepository; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.RequestDispatcher; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; @Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { + @Value("#{ @environment['jjwtfun.secret'] ?: 'secret' }") + String secret; + @Autowired CsrfTokenRepository jwtCsrfTokenRepository; @Override protected void configure(HttpSecurity http) throws Exception { http - .csrf().csrfTokenRepository(jwtCsrfTokenRepository).and() - .authorizeRequests() - .antMatchers("/**") - .permitAll(); + .addFilterAfter(new JwtCsrfValidatorFilter(), CsrfFilter.class) + .csrf() + .csrfTokenRepository(jwtCsrfTokenRepository) + .ignoringAntMatchers("/dynamic-builder-general") + .ignoringAntMatchers("/dynamic-builder-specific") + .and().authorizeRequests() + .antMatchers("/**") + .permitAll(); + } + + private class JwtCsrfValidatorFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + // NOTE: A real implementation should have a nonce cache so the token cannot be reused + + CsrfToken token = (CsrfToken) request.getAttribute("_csrf"); + + // CsrfFilter already made sure the token matched. + // Here, we'll make sure it's not expired + if ("POST".equals(request.getMethod()) && token != null) { + try { + Jwts.parser() + .setSigningKey(secret.getBytes("UTF-8")) + .parseClaimsJws(token.getToken()); + } catch (JwtException e) { + // most likely an ExpiredJwtException, but this will handle any + request.setAttribute("exception", e); + response.setStatus(HttpServletResponse.SC_BAD_REQUEST); + RequestDispatcher dispatcher = request.getRequestDispatcher("expired-jwt"); + dispatcher.forward(request, response); + } + } + + filterChain.doFilter(request, response); + } } } diff --git a/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/FormController.java b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/FormController.java index 178af2f48d..54123c63cb 100644 --- a/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/FormController.java +++ b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/FormController.java @@ -18,9 +18,12 @@ public class FormController { @RequestMapping(value = "/jwt-csrf-form", method = POST) public String csrfFormPost(@RequestParam(name = "_csrf") String csrf, Model model) { - model.addAttribute("csrf", csrf); - return "jwt-csrf-form-result"; } + + @RequestMapping("/expired-jwt") + public String expiredJwt() { + return "expired-jwt"; + } } diff --git a/jjwt/src/main/resources/templates/expired-jwt.html b/jjwt/src/main/resources/templates/expired-jwt.html new file mode 100644 index 0000000000..7bf9ff258e --- /dev/null +++ b/jjwt/src/main/resources/templates/expired-jwt.html @@ -0,0 +1,17 @@ + + + + + +
+
+
+

JWT CSRF Token expired

+

+ + Back +
+
+
+ + \ No newline at end of file diff --git a/jjwt/src/main/resources/templates/fragments/head.html b/jjwt/src/main/resources/templates/fragments/head.html index ce76b0e655..2d5f54e5a0 100644 --- a/jjwt/src/main/resources/templates/fragments/head.html +++ b/jjwt/src/main/resources/templates/fragments/head.html @@ -8,6 +8,17 @@ + +