├── .gitignore
├── roadstorm-jwt-microservices-tutorial
├── Procfile
├── src
│ └── main
│ │ ├── resources
│ │ ├── application.properties
│ │ └── logback.xml
│ │ └── java
│ │ └── com
│ │ └── stormpath
│ │ └── tutorial
│ │ ├── exception
│ │ └── UnauthorizedException.java
│ │ ├── model
│ │ ├── AccountResponse.java
│ │ ├── BaseResponse.java
│ │ ├── PublicCreds.java
│ │ ├── Account.java
│ │ └── JWTResponse.java
│ │ ├── controller
│ │ ├── HttpMicroServiceController.java
│ │ ├── RestrictedController.java
│ │ ├── MessagingMicroServiceController.java
│ │ ├── BaseController.java
│ │ ├── SecretServiceController.java
│ │ └── HomeController.java
│ │ ├── service
│ │ ├── SpringBootKafkaProducer.java
│ │ ├── SpringBootKafkaConsumer.java
│ │ ├── AccountService.java
│ │ └── SecretService.java
│ │ └── JJWTMicroservicesTutorial.java
├── app.json
├── pom.xml
└── README.md
├── roadstorm-jwt-csrf-tutorial
├── src
│ └── main
│ │ ├── resources
│ │ ├── application.properties
│ │ └── templates
│ │ │ ├── expired-jwt.html
│ │ │ ├── jwt-csrf-form.html
│ │ │ ├── jwt-csrf-form-result.html
│ │ │ └── fragments
│ │ │ └── head.html
│ │ └── java
│ │ └── io
│ │ └── jsonwebtoken
│ │ └── jjwtfun
│ │ ├── JJWTCSRFTutorial.java
│ │ ├── config
│ │ ├── CSRFConfig.java
│ │ ├── DefaultWebSecurityConfig.java
│ │ ├── JWTCSRFTokenRepository.java
│ │ └── JWTCSRFWebSecurityConfig.java
│ │ ├── controller
│ │ ├── BaseController.java
│ │ ├── FormController.java
│ │ ├── SecretsController.java
│ │ ├── HomeController.java
│ │ ├── StaticJWTController.java
│ │ └── DynamicJWTController.java
│ │ ├── model
│ │ └── JwtResponse.java
│ │ └── service
│ │ └── SecretService.java
├── README.md
└── pom.xml
├── README.md
└── pom.xml
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | target
3 | *.iml
4 | .idea
5 |
--------------------------------------------------------------------------------
/roadstorm-jwt-microservices-tutorial/Procfile:
--------------------------------------------------------------------------------
1 | web: java $JAVA_OPTS -Dserver.port=$PORT -jar target/*.jar
--------------------------------------------------------------------------------
/roadstorm-jwt-csrf-tutorial/src/main/resources/application.properties:
--------------------------------------------------------------------------------
1 | jwt.csrf.token.repository.disabled = false
--------------------------------------------------------------------------------
/roadstorm-jwt-microservices-tutorial/src/main/resources/application.properties:
--------------------------------------------------------------------------------
1 | kafka.enabled=false
2 | kafka.broker.address=localhost:9092
3 | zookeeper.address=localhost:2181
4 | topic=micro-services
--------------------------------------------------------------------------------
/roadstorm-jwt-microservices-tutorial/src/main/resources/logback.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/roadstorm-jwt-microservices-tutorial/src/main/java/com/stormpath/tutorial/exception/UnauthorizedException.java:
--------------------------------------------------------------------------------
1 | package com.stormpath.tutorial.exception;
2 |
3 | public class UnauthorizedException extends RuntimeException {
4 | public UnauthorizedException(String message) {
5 | super(message);
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/roadstorm-jwt-microservices-tutorial/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "JJWT Secure Microservices",
3 | "description": "Secure communication amongst microservices with JWTs",
4 | "repository": "https://github.com/stormpath/stormpath-jjwt-microservices-tutorial/tree/master",
5 | "keywords": ["spring boot", "identity", "security", "authentication"],
6 | "success_url": "/get-my-public-creds"
7 | }
--------------------------------------------------------------------------------
/roadstorm-jwt-csrf-tutorial/src/main/java/io/jsonwebtoken/jjwtfun/JJWTCSRFTutorial.java:
--------------------------------------------------------------------------------
1 | package io.jsonwebtoken.jjwtfun;
2 |
3 | import org.springframework.boot.SpringApplication;
4 | import org.springframework.boot.autoconfigure.SpringBootApplication;
5 |
6 | @SpringBootApplication
7 | public class JJWTCSRFTutorial {
8 |
9 | public static void main(String[] args) {
10 | SpringApplication.run(JJWTCSRFTutorial.class, args);
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/roadstorm-jwt-microservices-tutorial/src/main/java/com/stormpath/tutorial/model/AccountResponse.java:
--------------------------------------------------------------------------------
1 | package com.stormpath.tutorial.model;
2 |
3 | import com.fasterxml.jackson.annotation.JsonInclude;
4 |
5 | @JsonInclude(JsonInclude.Include.NON_NULL)
6 | public class AccountResponse extends BaseResponse {
7 | private Account account;
8 |
9 | public Account getAccount() {
10 | return account;
11 | }
12 |
13 | public void setAccount(Account account) {
14 | this.account = account;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/roadstorm-jwt-csrf-tutorial/src/main/resources/templates/expired-jwt.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
JWT CSRF Token expired
10 |
11 |
12 |
Back
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/roadstorm-jwt-microservices-tutorial/src/main/java/com/stormpath/tutorial/model/BaseResponse.java:
--------------------------------------------------------------------------------
1 | package com.stormpath.tutorial.model;
2 |
3 | public class BaseResponse {
4 | private String message;
5 | private Status status;
6 |
7 | public enum Status {
8 | SUCCESS, ERROR
9 | }
10 |
11 | public String getMessage() {
12 | return message;
13 | }
14 |
15 | public void setMessage(String message) {
16 | this.message = message;
17 | }
18 |
19 | public Status getStatus() {
20 | return status;
21 | }
22 |
23 | public void setStatus(Status status) {
24 | this.status = status;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/roadstorm-jwt-csrf-tutorial/src/main/resources/templates/jwt-csrf-form.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
17 |
18 |
--------------------------------------------------------------------------------
/roadstorm-jwt-microservices-tutorial/src/main/java/com/stormpath/tutorial/model/PublicCreds.java:
--------------------------------------------------------------------------------
1 | package com.stormpath.tutorial.model;
2 |
3 | import com.fasterxml.jackson.annotation.JsonCreator;
4 | import com.fasterxml.jackson.annotation.JsonProperty;
5 |
6 | public class PublicCreds {
7 | final String kid;
8 | final String b64UrlPublicKey;
9 |
10 | @JsonCreator
11 | public PublicCreds(@JsonProperty("kid") String kid, @JsonProperty("b64UrlPublicKey") String b64UrlPublicKey) {
12 | this.kid = kid;
13 | this.b64UrlPublicKey = b64UrlPublicKey;
14 | }
15 |
16 | public String getKid() {
17 | return kid;
18 | }
19 |
20 | public String getB64UrlPublicKey() {
21 | return b64UrlPublicKey;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/roadstorm-jwt-microservices-tutorial/src/main/java/com/stormpath/tutorial/controller/HttpMicroServiceController.java:
--------------------------------------------------------------------------------
1 | package com.stormpath.tutorial.controller;
2 |
3 | import com.stormpath.tutorial.model.JWTResponse;
4 | import org.springframework.web.bind.annotation.RequestBody;
5 | import org.springframework.web.bind.annotation.RequestMapping;
6 | import org.springframework.web.bind.annotation.RestController;
7 |
8 | import java.util.Map;
9 |
10 | @RestController
11 | public class HttpMicroServiceController extends BaseController {
12 |
13 | @RequestMapping("/account-request")
14 | public JWTResponse authBuilder(@RequestBody Map claims) {
15 | String jwt = createJwt(claims);
16 |
17 | return new JWTResponse(jwt);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/roadstorm-jwt-csrf-tutorial/src/main/resources/templates/jwt-csrf-form-result.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
You made it!
11 |
12 |
BLARG
13 |
14 |
Back
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/roadstorm-jwt-csrf-tutorial/src/main/java/io/jsonwebtoken/jjwtfun/config/CSRFConfig.java:
--------------------------------------------------------------------------------
1 | package io.jsonwebtoken.jjwtfun.config;
2 |
3 | import io.jsonwebtoken.jjwtfun.service.SecretService;
4 | import org.springframework.beans.factory.annotation.Autowired;
5 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
6 | import org.springframework.context.annotation.Bean;
7 | import org.springframework.context.annotation.Configuration;
8 | import org.springframework.security.web.csrf.CsrfTokenRepository;
9 |
10 | @Configuration
11 | public class CSRFConfig {
12 |
13 | @Autowired
14 | SecretService secretService;
15 |
16 | @Bean
17 | @ConditionalOnMissingBean
18 | public CsrfTokenRepository jwtCsrfTokenRepository() {
19 | return new JWTCSRFTokenRepository(secretService.getHS256SecretBytes());
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/roadstorm-jwt-microservices-tutorial/src/main/java/com/stormpath/tutorial/controller/RestrictedController.java:
--------------------------------------------------------------------------------
1 | package com.stormpath.tutorial.controller;
2 |
3 | import com.stormpath.tutorial.model.AccountResponse;
4 | import com.stormpath.tutorial.service.AccountService;
5 | import org.springframework.beans.factory.annotation.Autowired;
6 | import org.springframework.web.bind.annotation.RequestMapping;
7 | import org.springframework.web.bind.annotation.RestController;
8 |
9 | import javax.servlet.http.HttpServletRequest;
10 |
11 | @RestController
12 | public class RestrictedController extends BaseController {
13 |
14 | @Autowired
15 | AccountService accountService;
16 |
17 |
18 | @RequestMapping("/restricted")
19 | public AccountResponse restricted(HttpServletRequest req) {
20 | return accountService.getAccount(req);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/roadstorm-jwt-csrf-tutorial/src/main/java/io/jsonwebtoken/jjwtfun/config/DefaultWebSecurityConfig.java:
--------------------------------------------------------------------------------
1 | package io.jsonwebtoken.jjwtfun.config;
2 |
3 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
4 | import org.springframework.context.annotation.Configuration;
5 | import org.springframework.security.config.annotation.web.builders.HttpSecurity;
6 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
7 |
8 | @Configuration
9 | @ConditionalOnProperty(name = {"jwt.csrf.token.repository.disabled"}, havingValue = "true")
10 | public class DefaultWebSecurityConfig extends WebSecurityConfigurerAdapter {
11 |
12 | @Override
13 | protected void configure(HttpSecurity http) throws Exception {
14 | http
15 | .authorizeRequests()
16 | .antMatchers("/**")
17 | .permitAll();
18 |
19 | http.csrf().ignoringAntMatchers("/dynamic**", "/static**", "/parser**");
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/roadstorm-jwt-csrf-tutorial/src/main/java/io/jsonwebtoken/jjwtfun/controller/BaseController.java:
--------------------------------------------------------------------------------
1 | package io.jsonwebtoken.jjwtfun.controller;
2 |
3 | import io.jsonwebtoken.JwtException;
4 | import io.jsonwebtoken.MalformedJwtException;
5 | import io.jsonwebtoken.SignatureException;
6 | import io.jsonwebtoken.jjwtfun.model.JwtResponse;
7 | import org.springframework.http.HttpStatus;
8 | import org.springframework.web.bind.annotation.ExceptionHandler;
9 | import org.springframework.web.bind.annotation.ResponseStatus;
10 |
11 | public class BaseController {
12 |
13 | @ResponseStatus(HttpStatus.BAD_REQUEST)
14 | @ExceptionHandler({SignatureException.class, MalformedJwtException.class, JwtException.class})
15 | public JwtResponse exception(Exception e) {
16 | JwtResponse response = new JwtResponse();
17 | response.setStatus(JwtResponse.Status.ERROR);
18 | response.setMessage(e.getMessage());
19 | response.setExceptionType(e.getClass().getName());
20 |
21 | return response;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/roadstorm-jwt-microservices-tutorial/src/main/java/com/stormpath/tutorial/model/Account.java:
--------------------------------------------------------------------------------
1 | package com.stormpath.tutorial.model;
2 |
3 | public class Account {
4 |
5 | private String firstName;
6 | private String lastName;
7 | private String userName;
8 |
9 | public Account(String firstName, String lastName, String userName) {
10 | this.firstName = firstName;
11 | this.lastName = lastName;
12 | this.userName = userName;
13 | }
14 |
15 | public String getFirstName() {
16 | return firstName;
17 | }
18 |
19 | public void setFirstName(String firstName) {
20 | this.firstName = firstName;
21 | }
22 |
23 | public String getLastName() {
24 | return lastName;
25 | }
26 |
27 | public void setLastName(String lastName) {
28 | this.lastName = lastName;
29 | }
30 |
31 | public String getUserName() {
32 | return userName;
33 | }
34 |
35 | public void setUserName(String userName) {
36 | this.userName = userName;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/roadstorm-jwt-csrf-tutorial/src/main/java/io/jsonwebtoken/jjwtfun/controller/FormController.java:
--------------------------------------------------------------------------------
1 | package io.jsonwebtoken.jjwtfun.controller;
2 |
3 | import org.springframework.stereotype.Controller;
4 | import org.springframework.ui.Model;
5 | import org.springframework.web.bind.annotation.RequestMapping;
6 | import org.springframework.web.bind.annotation.RequestParam;
7 |
8 | import static org.springframework.web.bind.annotation.RequestMethod.GET;
9 | import static org.springframework.web.bind.annotation.RequestMethod.POST;
10 |
11 | @Controller
12 | public class FormController {
13 |
14 | @RequestMapping(value = "/jwt-csrf-form", method = GET)
15 | public String csrfFormGet() {
16 | return "jwt-csrf-form";
17 | }
18 |
19 | @RequestMapping(value = "/jwt-csrf-form", method = POST)
20 | public String csrfFormPost(@RequestParam(name = "_csrf") String csrf, Model model) {
21 | model.addAttribute("csrf", csrf);
22 | return "jwt-csrf-form-result";
23 | }
24 |
25 | @RequestMapping("/expired-jwt")
26 | public String expiredJwt() {
27 | return "expired-jwt";
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Stormpath Java RoadStorm Tour Code
2 |
3 | Check out the [Java RoadStorm Tour](http://roadstorm.stormpath.com) landing page for tour dates and information.
4 |
5 | The code included in this repo is the tutorial code used in talks on the Java RoadStorm Tour. There are two tutorials:
6 |
7 | * [JJWT CSRF Tutorial](roadstorm-jwt-csrf-tutorial) - This shows how replacing the default CSRF handler for Spring Security with a custom handler that uses JWTs can enhance CSRF protection.
8 | * [JJWT Microservices Tutorial](roadstorm-jwt-microservices-tutorial) - This is a demonstration of establishing trust between microservices using JWTs. It has both an HTTP mode and a messaging mode using Kafka.
9 |
10 | ## Resources
11 |
12 | * [JJWT](https://github.com/jwtk/jjwt) - Java JWT library used in the tutorials
13 | * [HTTPie](https://github.com/jkbrzt/httpie) - command line HTTP client (replaces curl)
14 | * [Kafka](http://kafka.apache.org/) - Messaging system used in the microservices tutorial. Note: All you need to do is follow the [quickstart](http://kafka.apache.org/documentation.html#quickstart)
15 |
16 | For more information, look at the README in each of the tutorials.
17 |
--------------------------------------------------------------------------------
/roadstorm-jwt-microservices-tutorial/src/main/java/com/stormpath/tutorial/model/JWTResponse.java:
--------------------------------------------------------------------------------
1 | package com.stormpath.tutorial.model;
2 |
3 | import com.fasterxml.jackson.annotation.JsonInclude;
4 | import io.jsonwebtoken.Claims;
5 | import io.jsonwebtoken.Jws;
6 |
7 | @JsonInclude(JsonInclude.Include.NON_NULL)
8 | public class JWTResponse extends BaseResponse {
9 | private String exceptionType;
10 | private String jwt;
11 | private Jws jwsClaims;
12 |
13 | public JWTResponse() {}
14 |
15 | public JWTResponse(String jwt) {
16 | this.jwt = jwt;
17 | setStatus(Status.SUCCESS);
18 | }
19 |
20 | public JWTResponse(Jws jwsClaims) {
21 | this.jwsClaims = jwsClaims;
22 | setStatus(Status.SUCCESS);
23 | }
24 |
25 | public String getExceptionType() {
26 | return exceptionType;
27 | }
28 |
29 | public void setExceptionType(String exceptionType) {
30 | this.exceptionType = exceptionType;
31 | }
32 |
33 | public String getJwt() {
34 | return jwt;
35 | }
36 |
37 | public void setJwt(String jwt) {
38 | this.jwt = jwt;
39 | }
40 |
41 | public Jws getJwsClaims() {
42 | return jwsClaims;
43 | }
44 |
45 | public void setJwsClaims(Jws jwsClaims) {
46 | this.jwsClaims = jwsClaims;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/roadstorm-jwt-csrf-tutorial/src/main/java/io/jsonwebtoken/jjwtfun/controller/SecretsController.java:
--------------------------------------------------------------------------------
1 | package io.jsonwebtoken.jjwtfun.controller;
2 |
3 | import io.jsonwebtoken.jjwtfun.service.SecretService;
4 | import org.springframework.beans.factory.annotation.Autowired;
5 | import org.springframework.web.bind.annotation.RequestBody;
6 | import org.springframework.web.bind.annotation.RequestMapping;
7 | import org.springframework.web.bind.annotation.RestController;
8 |
9 | import java.util.Map;
10 |
11 | import static org.springframework.web.bind.annotation.RequestMethod.GET;
12 | import static org.springframework.web.bind.annotation.RequestMethod.POST;
13 |
14 | @RestController
15 | public class SecretsController extends BaseController {
16 |
17 | @Autowired
18 | SecretService secretService;
19 |
20 | @RequestMapping(value = "/get-secrets", method = GET)
21 | public Map getSecrets() {
22 | return secretService.getSecrets();
23 | }
24 |
25 | @RequestMapping(value = "/refresh-secrets", method = GET)
26 | public Map refreshSecrets() {
27 | return secretService.refreshSecrets();
28 | }
29 |
30 | @RequestMapping(value = "/set-secrets", method = POST)
31 | public Map setSecrets(@RequestBody Map secrets) {
32 | secretService.setSecrets(secrets);
33 | return secretService.getSecrets();
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/roadstorm-jwt-microservices-tutorial/src/main/java/com/stormpath/tutorial/controller/MessagingMicroServiceController.java:
--------------------------------------------------------------------------------
1 | package com.stormpath.tutorial.controller;
2 |
3 | import com.stormpath.tutorial.model.JWTResponse;
4 | import com.stormpath.tutorial.service.SpringBootKafkaProducer;
5 | import org.slf4j.Logger;
6 | import org.slf4j.LoggerFactory;
7 | import org.springframework.beans.factory.annotation.Autowired;
8 | import org.springframework.web.bind.annotation.RequestBody;
9 | import org.springframework.web.bind.annotation.RequestMapping;
10 | import org.springframework.web.bind.annotation.RestController;
11 |
12 | import java.util.Map;
13 | import java.util.concurrent.ExecutionException;
14 |
15 | @RestController
16 | public class MessagingMicroServiceController extends BaseController {
17 |
18 | @Autowired(required = false)
19 | SpringBootKafkaProducer springBootKafkaProducer;
20 |
21 | private static final Logger log = LoggerFactory.getLogger(MessagingMicroServiceController.class);
22 |
23 | @RequestMapping("/msg-account-request")
24 | public JWTResponse authBuilder(@RequestBody Map claims) throws ExecutionException, InterruptedException {
25 | String jwt = createJwt(claims);
26 |
27 | if (springBootKafkaProducer != null) {
28 | springBootKafkaProducer.send(jwt);
29 | } else {
30 | log.warn("Kafka is disabled.");
31 | }
32 |
33 | return new JWTResponse(jwt);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/roadstorm-jwt-csrf-tutorial/src/main/resources/templates/fragments/head.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
21 |
22 |
26 |
27 |
28 |
29 |
30 | Nothing to see here, move along.
31 |
32 |
--------------------------------------------------------------------------------
/roadstorm-jwt-csrf-tutorial/README.md:
--------------------------------------------------------------------------------
1 | ## Spring Security CSRF using JJWT
2 |
3 | This tutorial walks you through the various features supported by the [JJWT](https://github.com/jwtk/jjwt) library - a fluent interface Java JWT building and parsing library.
4 |
5 | ### Build and Run
6 |
7 | It's super easy to build and exercise this tutorial.
8 |
9 | ```
10 | mvn clean install
11 | java -jar target/*.jar
12 | ```
13 |
14 | That's it!
15 |
16 | You can hit the home endpoint with your favorite command-line http client. My favorite is: [httpie](https://github.com/jkbrzt/httpie)
17 |
18 | `http localhost:8080`
19 |
20 | ```
21 | Available commands (assumes httpie - https://github.com/jkbrzt/httpie):
22 |
23 | http http://localhost:8080/
24 | This usage message
25 |
26 | http http://localhost:8080/static-builder
27 | build JWT from hardcoded claims
28 |
29 | http POST http://localhost:8080/dynamic-builder-general claim-1=value-1 ... [claim-n=value-n]
30 | build JWT from passed in claims (using general claims map)
31 |
32 | http POST http://localhost:8080/dynamic-builder-specific claim-1=value-1 ... [claim-n=value-n]
33 | build JWT from passed in claims (using specific claims methods)
34 |
35 | http POST http://localhost:8080/dynamic-builder-compress claim-1=value-1 ... [claim-n=value-n]
36 | build DEFLATE compressed JWT from passed in claims
37 |
38 | http http://localhost:8080/parser?jwt=
39 | Parse passed in JWT
40 |
41 | http http://localhost:8080/parser-enforce?jwt=
42 | Parse passed in JWT enforcing the 'iss' registered claim and the 'hasMotorcycle' custom claim
43 | ```
44 |
45 | The Baeldung post that compliments this repo can be found [here](http://www.baeldung.com/)
--------------------------------------------------------------------------------
/roadstorm-jwt-csrf-tutorial/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 4.0.0
5 |
6 | com.stormpath.tutorial
7 | roadstorm-jwt-csrf-tutorial
8 | 0.0.1-SNAPSHOT
9 | jar
10 |
11 | roadstorm-jwt-csrf-tutorial
12 | Exercising the JJWT
13 |
14 |
15 | com.stormpath.tutorial
16 | stormpath-java-road-storm-tour
17 | 0.1.0-SNAPSHOT
18 | ../pom.xml
19 |
20 |
21 |
22 | UTF-8
23 | 1.8
24 |
25 |
26 |
27 |
28 | org.springframework.boot
29 | spring-boot-starter-thymeleaf
30 |
31 |
32 | org.springframework.boot
33 | spring-boot-starter-security
34 |
35 |
36 |
37 |
38 |
39 |
40 | org.springframework.boot
41 | spring-boot-maven-plugin
42 |
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/roadstorm-jwt-csrf-tutorial/src/main/java/io/jsonwebtoken/jjwtfun/model/JwtResponse.java:
--------------------------------------------------------------------------------
1 | package io.jsonwebtoken.jjwtfun.model;
2 |
3 | import com.fasterxml.jackson.annotation.JsonInclude;
4 | import io.jsonwebtoken.Claims;
5 | import io.jsonwebtoken.Jws;
6 |
7 | @JsonInclude(JsonInclude.Include.NON_NULL)
8 | public class JwtResponse {
9 | private String message;
10 | private Status status;
11 | private String exceptionType;
12 | private String jwt;
13 | private Jws jws;
14 |
15 | public enum Status {
16 | SUCCESS, ERROR
17 | }
18 |
19 | public JwtResponse() {}
20 |
21 | public JwtResponse(String jwt) {
22 | this.jwt = jwt;
23 | this.status = Status.SUCCESS;
24 | }
25 |
26 | public JwtResponse(Jws jws) {
27 | this.jws = jws;
28 | this.status = Status.SUCCESS;
29 | }
30 |
31 | public String getMessage() {
32 | return message;
33 | }
34 |
35 | public void setMessage(String message) {
36 | this.message = message;
37 | }
38 |
39 | public Status getStatus() {
40 | return status;
41 | }
42 |
43 | public void setStatus(Status status) {
44 | this.status = status;
45 | }
46 |
47 | public String getExceptionType() {
48 | return exceptionType;
49 | }
50 |
51 | public void setExceptionType(String exceptionType) {
52 | this.exceptionType = exceptionType;
53 | }
54 |
55 | public String getJwt() {
56 | return jwt;
57 | }
58 |
59 | public void setJwt(String jwt) {
60 | this.jwt = jwt;
61 | }
62 |
63 | public Jws getJws() {
64 | return jws;
65 | }
66 |
67 | public void setJws(Jws jws) {
68 | this.jws = jws;
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/roadstorm-jwt-microservices-tutorial/src/main/java/com/stormpath/tutorial/service/SpringBootKafkaProducer.java:
--------------------------------------------------------------------------------
1 | package com.stormpath.tutorial.service;
2 |
3 | import org.apache.kafka.clients.producer.KafkaProducer;
4 | import org.apache.kafka.clients.producer.Producer;
5 | import org.apache.kafka.clients.producer.ProducerRecord;
6 | import org.apache.kafka.clients.producer.RecordMetadata;
7 | import org.springframework.beans.factory.annotation.Value;
8 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
9 | import org.springframework.stereotype.Service;
10 |
11 | import javax.annotation.PostConstruct;
12 | import java.util.Properties;
13 | import java.util.concurrent.ExecutionException;
14 |
15 | @Service
16 | @ConditionalOnProperty(name = "kafka.enabled", matchIfMissing = true)
17 | public class SpringBootKafkaProducer {
18 |
19 | @Value("${kafka.broker.address}")
20 | private String brokerAddress;
21 |
22 | @Value("${topic}")
23 | private String topic;
24 |
25 | private Producer producer;
26 |
27 | @PostConstruct
28 | public void init() {
29 | Properties kafkaProps = new Properties();
30 |
31 | kafkaProps.put("bootstrap.servers", brokerAddress);
32 |
33 | kafkaProps.put("key.serializer",
34 | "org.apache.kafka.common.serialization.StringSerializer");
35 | kafkaProps.put("value.serializer",
36 | "org.apache.kafka.common.serialization.StringSerializer");
37 | kafkaProps.put("acks", "1");
38 |
39 | kafkaProps.put("retries", "1");
40 | kafkaProps.put("linger.ms", 5);
41 |
42 | producer = new KafkaProducer<>(kafkaProps);
43 | }
44 |
45 | public void send(String value) throws ExecutionException, InterruptedException {
46 | ProducerRecord record = new ProducerRecord<>(topic, value);
47 |
48 | producer.send(record, (RecordMetadata recordMetadata, Exception e) -> {
49 | if (e != null) {
50 | e.printStackTrace();
51 | }
52 | });
53 | }
54 | }
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 4.0.0
5 |
6 | com.stormpath.tutorial
7 | stormpath-java-road-storm-tour
8 | 0.1.0-SNAPSHOT
9 | pom
10 |
11 | Java RoadStorm Tour Code
12 |
13 | The code examples used in the Java RoadStorm Tour
14 |
15 | https://github.com/stormpath/JavaRoadStorm2016
16 | 2016
17 |
18 |
19 | org.springframework.boot
20 | spring-boot-starter-parent
21 | 1.5.1.RELEASE
22 |
23 |
24 |
25 |
26 |
27 | Apache License, Version 2.0
28 | http://www.apache.org/licenses/LICENSE-2.0
29 | repo
30 |
31 |
32 |
33 |
34 | roadstorm-jwt-csrf-tutorial
35 | roadstorm-jwt-microservices-tutorial
36 |
37 |
38 |
39 | UTF-8
40 | 1.8
41 | 0.6.0
42 |
43 |
44 |
45 |
46 | org.springframework.boot
47 | spring-boot-devtools
48 |
49 |
50 |
51 | io.jsonwebtoken
52 | jjwt
53 | ${jjwt.version}
54 |
55 |
56 |
57 |
58 |
59 |
60 | org.springframework.boot
61 | spring-boot-maven-plugin
62 |
63 | true
64 |
65 |
66 |
67 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/roadstorm-jwt-csrf-tutorial/src/main/java/io/jsonwebtoken/jjwtfun/controller/HomeController.java:
--------------------------------------------------------------------------------
1 | package io.jsonwebtoken.jjwtfun.controller;
2 |
3 | import org.springframework.http.MediaType;
4 | import org.springframework.stereotype.Controller;
5 | import org.springframework.web.bind.annotation.RequestMapping;
6 | import org.springframework.web.bind.annotation.ResponseBody;
7 |
8 | import javax.servlet.http.HttpServletRequest;
9 |
10 | @Controller
11 | public class HomeController {
12 |
13 | @RequestMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.ALL_VALUE)
14 | public @ResponseBody String restHome(HttpServletRequest req) {
15 | String requestUrl = getUrl(req);
16 | return "Available commands (assumes httpie - https://github.com/jkbrzt/httpie):\n\n" +
17 | " http " + requestUrl + "/\n\tThis usage message\n\n" +
18 | " http " + requestUrl + "/static-builder\n\tbuild JWT from hardcoded claims\n\n" +
19 | " http POST " + requestUrl + "/dynamic-builder-general claim-1=value-1 ... [claim-n=value-n]\n\tbuild JWT from passed in claims (using general claims map)\n\n" +
20 | " http POST " + requestUrl + "/dynamic-builder-specific claim-1=value-1 ... [claim-n=value-n]\n\tbuild JWT from passed in claims (using specific claims methods)\n\n" +
21 | " http POST " + requestUrl + "/dynamic-builder-compress claim-1=value-1 ... [claim-n=value-n]\n\tbuild DEFLATE compressed JWT from passed in claims\n\n" +
22 | " http " + requestUrl + "/parser?jwt=\n\tParse passed in JWT\n\n" +
23 | " http " + requestUrl + "/parser-enforce?jwt=\n\tParse passed in JWT enforcing the 'iss' registered claim and the 'hasMotorcycle' custom claim\n\n" +
24 | " http " + requestUrl + "/get-secrets\n\tShow the signing keys currently in use.\n\n" +
25 | " http " + requestUrl + "/refresh-secrets\n\tGenerate new signing keys and show them.\n\n" +
26 | " http POST " + requestUrl + "/set-secrets HS256=base64-encoded-value HS384=base64-encoded-value HS512=base64-encoded-value\n\tExplicitly set secrets to use in the application.";
27 | }
28 |
29 | @RequestMapping(value = "/", produces = MediaType.TEXT_HTML_VALUE, consumes = MediaType.ALL_VALUE)
30 | public String home() {
31 | return "redirect:/jwt-csrf-form";
32 | }
33 |
34 | private String getUrl(HttpServletRequest req) {
35 | return req.getScheme() + "://" +
36 | req.getServerName() +
37 | ((req.getServerPort() == 80 || req.getServerPort() == 443) ? "" : ":" + req.getServerPort());
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/roadstorm-jwt-csrf-tutorial/src/main/java/io/jsonwebtoken/jjwtfun/config/JWTCSRFTokenRepository.java:
--------------------------------------------------------------------------------
1 | package io.jsonwebtoken.jjwtfun.config;
2 |
3 | import io.jsonwebtoken.Jwts;
4 | import io.jsonwebtoken.SignatureAlgorithm;
5 | import org.slf4j.Logger;
6 | import org.slf4j.LoggerFactory;
7 | import org.springframework.security.web.csrf.CsrfToken;
8 | import org.springframework.security.web.csrf.CsrfTokenRepository;
9 | import org.springframework.security.web.csrf.DefaultCsrfToken;
10 |
11 | import javax.servlet.http.HttpServletRequest;
12 | import javax.servlet.http.HttpServletResponse;
13 | import javax.servlet.http.HttpSession;
14 | import java.util.Date;
15 | import java.util.UUID;
16 |
17 | public class JWTCSRFTokenRepository implements CsrfTokenRepository {
18 |
19 | private static final String DEFAULT_CSRF_TOKEN_ATTR_NAME = CSRFConfig.class.getName().concat(".CSRF_TOKEN");
20 |
21 | private static final Logger log = LoggerFactory.getLogger(JWTCSRFTokenRepository.class);
22 | private byte[] secret;
23 |
24 | public JWTCSRFTokenRepository(byte[] secret) {
25 | this.secret = secret;
26 | }
27 |
28 | @Override
29 | public CsrfToken generateToken(HttpServletRequest request) {
30 | String id = UUID.randomUUID().toString().replace("-", "");
31 |
32 | Date now = new Date();
33 | Date exp = new Date(now.getTime() + (1000*30)); // 30 seconds
34 |
35 | String token = Jwts.builder()
36 | .setId(id)
37 | .setIssuedAt(now)
38 | .setNotBefore(now)
39 | .setExpiration(exp)
40 | .signWith(SignatureAlgorithm.HS256, secret)
41 | .compact();
42 |
43 | return new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", token);
44 | }
45 |
46 | @Override
47 | public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) {
48 | if (token == null) {
49 | HttpSession session = request.getSession(false);
50 | if (session != null) {
51 | session.removeAttribute(DEFAULT_CSRF_TOKEN_ATTR_NAME);
52 | }
53 | }
54 | else {
55 | HttpSession session = request.getSession();
56 | session.setAttribute(DEFAULT_CSRF_TOKEN_ATTR_NAME, token);
57 | }
58 | }
59 |
60 | @Override
61 | public CsrfToken loadToken(HttpServletRequest request) {
62 | HttpSession session = request.getSession(false);
63 | if (session == null || "GET".equals(request.getMethod())) {
64 | return null;
65 | }
66 | return (CsrfToken) session.getAttribute(DEFAULT_CSRF_TOKEN_ATTR_NAME);
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/roadstorm-jwt-csrf-tutorial/src/main/java/io/jsonwebtoken/jjwtfun/controller/StaticJWTController.java:
--------------------------------------------------------------------------------
1 | package io.jsonwebtoken.jjwtfun.controller;
2 |
3 | import io.jsonwebtoken.Claims;
4 | import io.jsonwebtoken.Jws;
5 | import io.jsonwebtoken.Jwts;
6 | import io.jsonwebtoken.SignatureAlgorithm;
7 | import io.jsonwebtoken.jjwtfun.model.JwtResponse;
8 | import io.jsonwebtoken.jjwtfun.service.SecretService;
9 | import org.springframework.beans.factory.annotation.Autowired;
10 | import org.springframework.web.bind.annotation.RequestMapping;
11 | import org.springframework.web.bind.annotation.RequestParam;
12 | import org.springframework.web.bind.annotation.RestController;
13 |
14 | import java.io.UnsupportedEncodingException;
15 | import java.time.Instant;
16 | import java.util.Date;
17 |
18 | import static org.springframework.web.bind.annotation.RequestMethod.GET;
19 |
20 | @RestController
21 | public class StaticJWTController extends BaseController {
22 |
23 | @Autowired
24 | SecretService secretService;
25 |
26 | @RequestMapping(value = "/static-builder", method = GET)
27 | public JwtResponse fixedBuilder() throws UnsupportedEncodingException {
28 | String jws = Jwts.builder()
29 | .setIssuer("Stormpath")
30 | .setSubject("msilverman")
31 | .claim("name", "Micah Silverman")
32 | .claim("scope", "admins")
33 | .setIssuedAt(Date.from(Instant.ofEpochSecond(1466796822L))) // Fri Jun 24 2016 15:33:42 GMT-0400 (EDT)
34 | .setExpiration(Date.from(Instant.ofEpochSecond(4622470422L))) // Sat Jun 24 2116 15:33:42 GMT-0400 (EDT)
35 | .signWith(
36 | SignatureAlgorithm.HS256,
37 | secretService.getHS256SecretBytes()
38 | )
39 | .compact();
40 |
41 | return new JwtResponse(jws);
42 | }
43 |
44 | @RequestMapping(value = "/parser", method = GET)
45 | public JwtResponse parser(@RequestParam String jwt) throws UnsupportedEncodingException {
46 |
47 | Jws jws = Jwts.parser()
48 | .setSigningKeyResolver(secretService.getSigningKeyResolver())
49 | .parseClaimsJws(jwt);
50 |
51 | return new JwtResponse(jws);
52 | }
53 |
54 | @RequestMapping(value = "/parser-enforce", method = GET)
55 | public JwtResponse parserEnforce(@RequestParam String jwt) throws UnsupportedEncodingException {
56 | Jws jws = Jwts.parser()
57 | .requireIssuer("Stormpath")
58 | .require("hasMotorcycle", true)
59 | .setSigningKeyResolver(secretService.getSigningKeyResolver())
60 | .parseClaimsJws(jwt);
61 |
62 | return new JwtResponse(jws);
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/roadstorm-jwt-microservices-tutorial/src/main/java/com/stormpath/tutorial/controller/BaseController.java:
--------------------------------------------------------------------------------
1 | package com.stormpath.tutorial.controller;
2 |
3 | import com.stormpath.tutorial.exception.UnauthorizedException;
4 | import com.stormpath.tutorial.model.JWTResponse;
5 | import com.stormpath.tutorial.service.AccountService;
6 | import com.stormpath.tutorial.service.SecretService;
7 | import io.jsonwebtoken.JwtException;
8 | import io.jsonwebtoken.Jwts;
9 | import io.jsonwebtoken.MalformedJwtException;
10 | import io.jsonwebtoken.SignatureAlgorithm;
11 | import io.jsonwebtoken.SignatureException;
12 | import io.jsonwebtoken.lang.Assert;
13 | import org.springframework.beans.factory.annotation.Autowired;
14 | import org.springframework.http.HttpStatus;
15 | import org.springframework.web.bind.annotation.ExceptionHandler;
16 | import org.springframework.web.bind.annotation.ResponseStatus;
17 |
18 | import java.util.Date;
19 | import java.util.Map;
20 |
21 | public class BaseController {
22 |
23 | @Autowired
24 | SecretService secretService;
25 |
26 | protected String createJwt(Map claims) {
27 | Assert.notNull(
28 | claims.get(AccountService.USERNAME_CLAIM),
29 | AccountService.USERNAME_CLAIM + " claim is required."
30 | );
31 |
32 | Date now = new Date();
33 | Date exp = new Date(now.getTime() + (1000*60)); // 60 seconds
34 |
35 | String jwt = Jwts.builder()
36 | .setHeaderParam("kid", secretService.getMyPublicCreds().getKid())
37 | .setClaims(claims)
38 | .setIssuedAt(now)
39 | .setNotBefore(now)
40 | .setExpiration(exp)
41 | .signWith(
42 | SignatureAlgorithm.RS256,
43 | secretService.getMyPrivateKey()
44 | )
45 | .compact();
46 |
47 | return jwt;
48 | }
49 |
50 | @ResponseStatus(HttpStatus.BAD_REQUEST)
51 | @ExceptionHandler({
52 | SignatureException.class, MalformedJwtException.class, JwtException.class, IllegalArgumentException.class
53 | })
54 | public JWTResponse badRequest(Exception e) {
55 | return processException(e);
56 | }
57 |
58 | @ResponseStatus(HttpStatus.UNAUTHORIZED)
59 | @ExceptionHandler(UnauthorizedException.class)
60 | public JWTResponse unauthorized(Exception e) {
61 | return processException(e);
62 | }
63 |
64 | private JWTResponse processException(Exception e) {
65 | JWTResponse response = new JWTResponse();
66 | response.setStatus(JWTResponse.Status.ERROR);
67 | response.setMessage(e.getMessage());
68 | response.setExceptionType(e.getClass().getName());
69 |
70 | return response;
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/roadstorm-jwt-microservices-tutorial/src/main/java/com/stormpath/tutorial/controller/SecretServiceController.java:
--------------------------------------------------------------------------------
1 | package com.stormpath.tutorial.controller;
2 |
3 | import com.stormpath.tutorial.model.JWTResponse;
4 | import com.stormpath.tutorial.model.PublicCreds;
5 | import com.stormpath.tutorial.service.SecretService;
6 | import io.jsonwebtoken.Claims;
7 | import io.jsonwebtoken.Jws;
8 | import io.jsonwebtoken.Jwts;
9 | import io.jsonwebtoken.SignatureAlgorithm;
10 | import org.springframework.beans.factory.annotation.Autowired;
11 | import org.springframework.web.bind.annotation.RequestBody;
12 | import org.springframework.web.bind.annotation.RequestMapping;
13 | import org.springframework.web.bind.annotation.RequestParam;
14 | import org.springframework.web.bind.annotation.RestController;
15 |
16 | import java.time.Instant;
17 | import java.util.Date;
18 |
19 | @RestController
20 | public class SecretServiceController extends BaseController {
21 |
22 | @Autowired
23 | SecretService secretService;
24 |
25 | @RequestMapping("/refresh-my-creds")
26 | public PublicCreds refreshMyCreds() {
27 | return secretService.refreshMyCreds();
28 | }
29 |
30 | @RequestMapping("/get-my-public-creds")
31 | public PublicCreds getMyPublicCreds() {
32 | return secretService.getMyPublicCreds();
33 | }
34 |
35 | @RequestMapping("/add-public-creds")
36 | public PublicCreds addPublicCreds(@RequestBody PublicCreds publicCreds) {
37 | secretService.addPublicCreds(publicCreds);
38 | // just to prove that the key was successfully added
39 | return secretService.getPublicCreds(publicCreds.getKid());
40 | }
41 |
42 | @RequestMapping("/test-build")
43 | public JWTResponse testBuild() {
44 | String jws = Jwts.builder()
45 | .setHeaderParam("kid", secretService.getMyPublicCreds().getKid())
46 | .setIssuer("Stormpath")
47 | .setSubject("msilverman")
48 | .claim("name", "Micah Silverman")
49 | .claim("hasMotorcycle", true)
50 | .setIssuedAt(Date.from(Instant.ofEpochSecond(1466796822L))) // Fri Jun 24 2016 15:33:42 GMT-0400 (EDT)
51 | .setExpiration(Date.from(Instant.ofEpochSecond(4622470422L))) // Sat Jun 24 2116 15:33:42 GMT-0400 (EDT)
52 | .signWith(
53 | SignatureAlgorithm.RS256,
54 | secretService.getMyPrivateKey()
55 | )
56 | .compact();
57 | return new JWTResponse(jws);
58 | }
59 |
60 | @RequestMapping("/test-parse")
61 | public JWTResponse testParse(@RequestParam String jwt) {
62 | Jws jwsClaims = Jwts.parser()
63 | .setSigningKeyResolver(secretService.getSigningKeyResolver())
64 | .parseClaimsJws(jwt);
65 |
66 | return new JWTResponse(jwsClaims);
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/roadstorm-jwt-csrf-tutorial/src/main/java/io/jsonwebtoken/jjwtfun/service/SecretService.java:
--------------------------------------------------------------------------------
1 | package io.jsonwebtoken.jjwtfun.service;
2 |
3 | import io.jsonwebtoken.Claims;
4 | import io.jsonwebtoken.JwsHeader;
5 | import io.jsonwebtoken.SignatureAlgorithm;
6 | import io.jsonwebtoken.SigningKeyResolver;
7 | import io.jsonwebtoken.SigningKeyResolverAdapter;
8 | import io.jsonwebtoken.impl.TextCodec;
9 | import io.jsonwebtoken.impl.crypto.MacProvider;
10 | import io.jsonwebtoken.lang.Assert;
11 | import org.springframework.stereotype.Service;
12 |
13 | import javax.annotation.PostConstruct;
14 | import javax.crypto.SecretKey;
15 | import java.util.HashMap;
16 | import java.util.Map;
17 |
18 | @Service
19 | public class SecretService {
20 |
21 | private Map secrets = new HashMap<>();
22 |
23 | private SigningKeyResolver signingKeyResolver = new SigningKeyResolverAdapter() {
24 | @Override
25 | public byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) {
26 | return TextCodec.BASE64.decode(secrets.get(header.getAlgorithm()));
27 | }
28 | };
29 |
30 | @PostConstruct
31 | public void setup() {
32 | refreshSecrets();
33 | }
34 |
35 | public SigningKeyResolver getSigningKeyResolver() {
36 | return signingKeyResolver;
37 | }
38 |
39 | public Map getSecrets() {
40 | return secrets;
41 | }
42 |
43 | public void setSecrets(Map secrets) {
44 | Assert.notNull(secrets);
45 | Assert.hasText(secrets.get(SignatureAlgorithm.HS256.getValue()));
46 | Assert.hasText(secrets.get(SignatureAlgorithm.HS384.getValue()));
47 | Assert.hasText(secrets.get(SignatureAlgorithm.HS512.getValue()));
48 |
49 | this.secrets = secrets;
50 | }
51 |
52 | public byte[] getHS256SecretBytes() {
53 | return TextCodec.BASE64.decode(secrets.get(SignatureAlgorithm.HS256.getValue()));
54 | }
55 |
56 | public byte[] getHS384SecretBytes() {
57 | return TextCodec.BASE64.decode(secrets.get(SignatureAlgorithm.HS384.getValue()));
58 | }
59 |
60 | public byte[] getHS512SecretBytes() {
61 | return TextCodec.BASE64.decode(secrets.get(SignatureAlgorithm.HS384.getValue()));
62 | }
63 |
64 |
65 | public Map refreshSecrets() {
66 | SecretKey key = MacProvider.generateKey(SignatureAlgorithm.HS256);
67 | secrets.put(SignatureAlgorithm.HS256.getValue(), TextCodec.BASE64.encode(key.getEncoded()));
68 | key = MacProvider.generateKey(SignatureAlgorithm.HS384);
69 | secrets.put(SignatureAlgorithm.HS384.getValue(), TextCodec.BASE64.encode(key.getEncoded()));
70 | key = MacProvider.generateKey(SignatureAlgorithm.HS512);
71 | secrets.put(SignatureAlgorithm.HS512.getValue(), TextCodec.BASE64.encode(key.getEncoded()));
72 | return secrets;
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/roadstorm-jwt-microservices-tutorial/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 4.0.0
5 |
6 | com.stormpath.tutorial
7 | stormpath-jwt-microservices-tutorial
8 | 0.1.0-SNAPSHOT
9 | jar
10 |
11 | stormpath-jwt-microservices-tutorial
12 | Demo project for Spring Boot
13 |
14 |
15 | com.stormpath.tutorial
16 | stormpath-java-road-storm-tour
17 | 0.1.0-SNAPSHOT
18 | ../pom.xml
19 |
20 |
21 |
22 | UTF-8
23 | 1.8
24 |
25 |
26 |
27 |
28 | org.springframework.boot
29 | spring-boot-starter-web
30 |
31 |
32 |
33 | org.apache.zookeeper
34 | zookeeper
35 | 3.4.7
36 |
37 |
38 | org.slf4j
39 | slf4j-log4j12
40 |
41 |
42 | log4j
43 | log4j
44 |
45 |
46 |
47 |
48 | org.apache.kafka
49 | kafka_2.11
50 | 0.9.0.0
51 |
52 |
53 | org.slf4j
54 | slf4j-log4j12
55 |
56 |
57 | log4j
58 | log4j
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | org.springframework.boot
68 | spring-boot-maven-plugin
69 |
70 | true
71 |
72 |
73 |
74 |
75 |
76 |
77 |
--------------------------------------------------------------------------------
/roadstorm-jwt-microservices-tutorial/src/main/java/com/stormpath/tutorial/service/SpringBootKafkaConsumer.java:
--------------------------------------------------------------------------------
1 | package com.stormpath.tutorial.service;
2 |
3 | import com.stormpath.tutorial.model.AccountResponse;
4 | import io.jsonwebtoken.JwtException;
5 | import org.apache.kafka.clients.consumer.Consumer;
6 | import org.apache.kafka.clients.consumer.ConsumerRecord;
7 | import org.apache.kafka.clients.consumer.ConsumerRecords;
8 | import org.apache.kafka.clients.consumer.KafkaConsumer;
9 | import org.slf4j.Logger;
10 | import org.slf4j.LoggerFactory;
11 | import org.springframework.beans.factory.annotation.Autowired;
12 | import org.springframework.beans.factory.annotation.Value;
13 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
14 | import org.springframework.stereotype.Service;
15 |
16 | import javax.annotation.PostConstruct;
17 | import java.util.Collections;
18 | import java.util.Properties;
19 |
20 | @Service
21 | @ConditionalOnProperty(name = "kafka.enabled", matchIfMissing = true)
22 | public class SpringBootKafkaConsumer {
23 |
24 | @Value("${kafka.broker.address}")
25 | private String brokerAddress;
26 |
27 | @Value("${topic}")
28 | private String topic;
29 |
30 | @Autowired
31 | AccountService accountService;
32 |
33 | private Properties kafkaProps;
34 | private Consumer consumer;
35 |
36 | private static final Logger log = LoggerFactory.getLogger(SpringBootKafkaConsumer.class);
37 |
38 |
39 | @PostConstruct
40 | public void init() {
41 | kafkaProps = new Properties();
42 |
43 | kafkaProps.put("bootstrap.servers", brokerAddress);
44 |
45 | kafkaProps.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
46 | kafkaProps.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
47 | kafkaProps.put("group.id", "consumer-tutorial");
48 | kafkaProps.put("acks", "1");
49 |
50 | kafkaProps.put("retries", "1");
51 | kafkaProps.put("linger.ms", 5);
52 | }
53 |
54 | public void consume() {
55 | log.info("Starting consumer...");
56 |
57 | consumer = new KafkaConsumer<>(kafkaProps);
58 | consumer.subscribe(Collections.singletonList(topic));
59 |
60 | while (true) {
61 | ConsumerRecords records = consumer.poll(1000);
62 | for (ConsumerRecord record : records) {
63 | log.info("record offset: {}, record value: {}", record.offset(), record.value());
64 | AccountResponse accountResponse = null;
65 | try {
66 | accountResponse = accountService.getAccount(record.value());
67 | } catch (JwtException e) {
68 | log.error("Unable to get account: {}", e.getMessage());
69 | }
70 | if (accountResponse != null && accountResponse.getAccount() != null) {
71 | log.info("Account name extracted from JWT: {}", accountResponse.getAccount().getFirstName() + " " + accountResponse.getAccount().getLastName());
72 | }
73 | }
74 | }
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/roadstorm-jwt-microservices-tutorial/src/main/java/com/stormpath/tutorial/controller/HomeController.java:
--------------------------------------------------------------------------------
1 | package com.stormpath.tutorial.controller;
2 |
3 | import org.springframework.web.bind.annotation.RequestMapping;
4 | import org.springframework.web.bind.annotation.RequestParam;
5 | import org.springframework.web.bind.annotation.RestController;
6 |
7 | import javax.servlet.http.HttpServletRequest;
8 |
9 | @RestController
10 | public class HomeController {
11 |
12 | @RequestMapping("/")
13 | public String home(
14 | HttpServletRequest req,
15 | @RequestParam(required = false) String localUrl, @RequestParam(required = false) String remoteUrl
16 | ) {
17 | if (localUrl == null || remoteUrl == null) {
18 | localUrl = getUrl(req);
19 | remoteUrl = "";
20 | }
21 |
22 | return "Available commands (assumes httpie - https://github.com/jkbrzt/httpie):\n\n" +
23 | "Key Management Endpoints:\n" +
24 | " Use these endpoints to manage local key pairs and to establish trust with other microservices.\n\n" +
25 | " http " + localUrl + "/\n\tThis usage message\n\n" +
26 | " http " + localUrl + "/ localUrl== remoteUrl==\n\tThis usage message, with a local and remote url plugged in to show usages between microservices\n\n" +
27 | " http " + localUrl + "/refresh-my-creds\n\tCreate a new private/public key pair for this microservice and return the public credentials\n\n" +
28 | " http " + localUrl + "/get-my-public-creds\n\tReturn the base64 url encoded public key and key id for this microservice\n\n" +
29 | " http POST " + remoteUrl + "/add-public-creds b64UrlPublicKey= kid=\n\tAdd the public credentials from one microservice to another microservice\n\n" +
30 | "Trust testing endpoints:\n" +
31 | " Use these endpoints to test the trust between microservices\n\n" +
32 | " http " + localUrl + "/test-build\n\tReturn a JWT from this microservice\n\n" +
33 | " http " + remoteUrl + "/test-parse?jwt=\n\tSend the JWT from /test-build from one microservice to be parsed on another microservice\n\n" +
34 | "Microservice Authorization endpoints:\n" +
35 | " Use these endpoints to exercise trusted communication between microservices\n\n" +
36 | " http " + localUrl + "/account-request userName=\n\tReturn a JWT to be sent to another microservice\n\n" +
37 | " http " + localUrl + "/msg-account-request userName=\n\tReturn a JWT to be sent to another microservice\n\tAlso, publish JWT as a message. The message will be picked up by the consumer running on the other microservice.\n\tNOTE: You must be running Kafka for this functionality.\n\n" +
38 | " http " + remoteUrl + "/restricted Authorization:\"Bearer \"\n\tReturn the search results for the userName\n\n";
39 | }
40 |
41 | private String getUrl(HttpServletRequest req) {
42 | return req.getScheme() + "://" +
43 | req.getServerName() +
44 | ((req.getServerPort() == 80 || req.getServerPort() == 443) ? "" : ":" + req.getServerPort());
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/roadstorm-jwt-microservices-tutorial/src/main/java/com/stormpath/tutorial/service/AccountService.java:
--------------------------------------------------------------------------------
1 | package com.stormpath.tutorial.service;
2 |
3 | import com.stormpath.tutorial.exception.UnauthorizedException;
4 | import com.stormpath.tutorial.model.Account;
5 | import com.stormpath.tutorial.model.AccountResponse;
6 | import com.stormpath.tutorial.model.BaseResponse;
7 | import io.jsonwebtoken.Claims;
8 | import io.jsonwebtoken.Jws;
9 | import io.jsonwebtoken.Jwts;
10 | import io.jsonwebtoken.MissingClaimException;
11 | import io.jsonwebtoken.lang.Assert;
12 | import org.slf4j.Logger;
13 | import org.slf4j.LoggerFactory;
14 | import org.springframework.beans.factory.annotation.Autowired;
15 | import org.springframework.stereotype.Service;
16 |
17 | import javax.annotation.PostConstruct;
18 | import javax.servlet.http.HttpServletRequest;
19 | import java.util.HashMap;
20 | import java.util.Map;
21 |
22 | @Service
23 | public class AccountService {
24 |
25 | @Autowired
26 | SecretService secretService;
27 |
28 | public static final String USERNAME_CLAIM = "userName";
29 |
30 | private static final String BEARER_IDENTIFIER = "Bearer "; // space is important
31 | private static final Logger log = LoggerFactory.getLogger(AccountService.class);
32 |
33 | private Map accounts;
34 |
35 | @PostConstruct
36 | void setup() {
37 | accounts = new HashMap<>();
38 | accounts.put("anna", new Account("Anna", "Apple", "anna"));
39 | accounts.put("betty", new Account("Betty", "Baker", "betty"));
40 | accounts.put("colin", new Account("Colin", "Cooper", "colin"));
41 | }
42 |
43 | public AccountResponse getAccount(HttpServletRequest req) {
44 | Assert.notNull(req);
45 |
46 | // get JWT as Authorization header
47 | String authorization = req.getHeader("Authorization");
48 | if (authorization == null || !authorization.startsWith(BEARER_IDENTIFIER)) {
49 | throw new UnauthorizedException("Missing or invalid Authorization header with Bearer type.");
50 | }
51 |
52 | String jwt = authorization.substring(BEARER_IDENTIFIER.length());
53 |
54 | return getAccount(jwt);
55 | }
56 |
57 | public AccountResponse getAccount(String jwt) {
58 | AccountResponse accountResponse = new AccountResponse();
59 | accountResponse.setStatus(BaseResponse.Status.ERROR);
60 |
61 | // verify JWT - will throw JWT Exception if not valid
62 | Jws jws = Jwts.parser()
63 | .setSigningKeyResolver(secretService.getSigningKeyResolver())
64 | .parseClaimsJws(jwt);
65 |
66 | // get userName - throw if missing
67 | String userName;
68 | if ((userName = (String)jws.getBody().get(USERNAME_CLAIM)) == null) {
69 | throw new MissingClaimException(
70 | jws.getHeader(),
71 | jws.getBody(),
72 | "Required claim: '" + USERNAME_CLAIM + "' missing on the JWT"
73 | );
74 | }
75 |
76 | // see if it exists
77 | if (accounts.get(userName) == null) {
78 | String msg = "Account with " + USERNAME_CLAIM + ": " + userName + ", not found";
79 | log.warn(msg);
80 | accountResponse.setMessage(msg);
81 | return accountResponse;
82 | }
83 |
84 | accountResponse.setMessage("Found Account");
85 | accountResponse.setStatus(BaseResponse.Status.SUCCESS);
86 | accountResponse.setAccount(accounts.get(userName));
87 |
88 | return accountResponse;
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/roadstorm-jwt-microservices-tutorial/src/main/java/com/stormpath/tutorial/JJWTMicroservicesTutorial.java:
--------------------------------------------------------------------------------
1 | package com.stormpath.tutorial;
2 |
3 | import com.stormpath.tutorial.service.SpringBootKafkaConsumer;
4 | import kafka.admin.AdminUtils;
5 | import kafka.common.TopicExistsException;
6 | import kafka.utils.ZKStringSerializer$;
7 | import kafka.utils.ZkUtils;
8 | import org.I0Itec.zkclient.ZkClient;
9 | import org.slf4j.Logger;
10 | import org.slf4j.LoggerFactory;
11 | import org.springframework.beans.factory.annotation.Value;
12 | import org.springframework.boot.SpringApplication;
13 | import org.springframework.boot.autoconfigure.SpringBootApplication;
14 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
15 | import org.springframework.context.ConfigurableApplicationContext;
16 | import org.springframework.context.SmartLifecycle;
17 | import org.springframework.context.annotation.Bean;
18 |
19 | import java.util.Properties;
20 |
21 | @SpringBootApplication
22 | public class JJWTMicroservicesTutorial {
23 |
24 | @Value("${topic}")
25 | private String topic;
26 |
27 | @Value("${zookeeper.address}")
28 | private String zookeeperAddress;
29 |
30 | private static final Logger log = LoggerFactory.getLogger(JJWTMicroservicesTutorial.class);
31 |
32 | public static void main(String[] args) {
33 | ConfigurableApplicationContext context = SpringApplication.run(JJWTMicroservicesTutorial.class, args);
34 |
35 | boolean shouldConsume = context
36 | .getEnvironment()
37 | .getProperty("kafka.consumer.enabled", Boolean.class, Boolean.FALSE);
38 |
39 |
40 | if (shouldConsume && context.containsBean("springBootKafkaConsumer")) {
41 | SpringBootKafkaConsumer springBootKafkaConsumer =
42 | context.getBean("springBootKafkaConsumer", SpringBootKafkaConsumer.class);
43 |
44 | springBootKafkaConsumer.consume();
45 | }
46 | }
47 |
48 | @Bean
49 | @ConditionalOnProperty(name = "kafka.enabled", matchIfMissing = true)
50 | public TopicCreator topicCreator() {
51 | return new TopicCreator(this.topic, this.zookeeperAddress);
52 | }
53 |
54 | private static class TopicCreator implements SmartLifecycle {
55 |
56 | private final String topic;
57 |
58 | private final String zkAddress;
59 |
60 | private volatile boolean running;
61 |
62 | public TopicCreator(String topic, String zkAddress) {
63 | this.topic = topic;
64 | this.zkAddress = zkAddress;
65 | }
66 |
67 | @Override
68 | public void start() {
69 | ZkUtils zkUtils = new ZkUtils(
70 | new ZkClient(this.zkAddress, 6000, 6000, ZKStringSerializer$.MODULE$), null, false
71 | );
72 | try {
73 | AdminUtils.createTopic(zkUtils, topic, 1, 1, new Properties());
74 | } catch (TopicExistsException e) {
75 | log.info("Topic: {} already exists.", topic);
76 | }
77 | this.running = true;
78 | }
79 |
80 | @Override
81 | public void stop() {}
82 |
83 | @Override
84 | public boolean isRunning() {
85 | return this.running;
86 | }
87 |
88 | @Override
89 | public int getPhase() {
90 | return Integer.MIN_VALUE;
91 | }
92 |
93 | @Override
94 | public boolean isAutoStartup() {
95 | return true;
96 | }
97 |
98 | @Override
99 | public void stop(Runnable callback) {
100 | callback.run();
101 | }
102 |
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/roadstorm-jwt-microservices-tutorial/src/main/java/com/stormpath/tutorial/service/SecretService.java:
--------------------------------------------------------------------------------
1 | package com.stormpath.tutorial.service;
2 |
3 | import com.stormpath.tutorial.model.PublicCreds;
4 | import io.jsonwebtoken.Claims;
5 | import io.jsonwebtoken.JwsHeader;
6 | import io.jsonwebtoken.JwtException;
7 | import io.jsonwebtoken.SigningKeyResolver;
8 | import io.jsonwebtoken.SigningKeyResolverAdapter;
9 | import io.jsonwebtoken.impl.TextCodec;
10 | import io.jsonwebtoken.impl.crypto.RsaProvider;
11 | import io.jsonwebtoken.lang.Strings;
12 | import org.slf4j.Logger;
13 | import org.slf4j.LoggerFactory;
14 | import org.springframework.stereotype.Service;
15 |
16 | import javax.annotation.PostConstruct;
17 | import java.security.Key;
18 | import java.security.KeyFactory;
19 | import java.security.KeyPair;
20 | import java.security.NoSuchAlgorithmException;
21 | import java.security.PrivateKey;
22 | import java.security.PublicKey;
23 | import java.security.spec.InvalidKeySpecException;
24 | import java.security.spec.X509EncodedKeySpec;
25 | import java.util.HashMap;
26 | import java.util.Map;
27 | import java.util.UUID;
28 |
29 | @Service
30 | public class SecretService {
31 |
32 | private static final Logger log = LoggerFactory.getLogger(SecretService.class);
33 |
34 | private KeyPair myKeyPair;
35 | private String kid;
36 |
37 | private Map publicKeys = new HashMap<>();
38 |
39 | @PostConstruct
40 | public void setup() {
41 | refreshMyCreds();
42 | }
43 |
44 | private SigningKeyResolver signingKeyResolver = new SigningKeyResolverAdapter() {
45 | @Override
46 | public Key resolveSigningKey(JwsHeader header, Claims claims) {
47 | String kid = header.getKeyId();
48 | if (!Strings.hasText(kid)) {
49 | throw new JwtException("Missing required 'kid' header param in JWT with claims: " + claims);
50 | }
51 | Key key = publicKeys.get(kid);
52 | if (key == null) {
53 | throw new JwtException("No public key registered for kid: " + kid + ". JWT claims: " + claims);
54 | }
55 | return key;
56 | }
57 | };
58 |
59 | public SigningKeyResolver getSigningKeyResolver() {
60 | return signingKeyResolver;
61 | }
62 |
63 | public PublicCreds getPublicCreds(String kid) {
64 | return createPublicCreds(kid, publicKeys.get(kid));
65 | }
66 |
67 | public PublicCreds getMyPublicCreds() {
68 | return createPublicCreds(this.kid, myKeyPair.getPublic());
69 | }
70 |
71 | private PublicCreds createPublicCreds(String kid, PublicKey key) {
72 | return new PublicCreds(kid, TextCodec.BASE64URL.encode(key.getEncoded()));
73 | }
74 |
75 | // do not expose in controllers
76 | public PrivateKey getMyPrivateKey() {
77 | return myKeyPair.getPrivate();
78 | }
79 |
80 | public PublicCreds refreshMyCreds() {
81 | myKeyPair = RsaProvider.generateKeyPair(1024);
82 | kid = UUID.randomUUID().toString();
83 |
84 | PublicCreds publicCreds = getMyPublicCreds();
85 |
86 | // this microservice will trust itself
87 | addPublicCreds(publicCreds);
88 |
89 | return publicCreds;
90 | }
91 |
92 | public void addPublicCreds(PublicCreds publicCreds) {
93 | byte[] encoded = TextCodec.BASE64URL.decode(publicCreds.getB64UrlPublicKey());
94 |
95 | PublicKey publicKey = null;
96 | try {
97 | publicKey = KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(encoded));
98 | } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
99 | log.error("Unable to create public key: {}", e.getMessage(), e);
100 | }
101 |
102 | publicKeys.put(publicCreds.getKid(), publicKey);
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/roadstorm-jwt-csrf-tutorial/src/main/java/io/jsonwebtoken/jjwtfun/config/JWTCSRFWebSecurityConfig.java:
--------------------------------------------------------------------------------
1 | package io.jsonwebtoken.jjwtfun.config;
2 |
3 | import io.jsonwebtoken.JwtException;
4 | import io.jsonwebtoken.Jwts;
5 | import io.jsonwebtoken.jjwtfun.service.SecretService;
6 | import org.springframework.beans.factory.annotation.Autowired;
7 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
8 | import org.springframework.context.annotation.Configuration;
9 | import org.springframework.security.config.annotation.web.builders.HttpSecurity;
10 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
11 | import org.springframework.security.web.csrf.CsrfFilter;
12 | import org.springframework.security.web.csrf.CsrfToken;
13 | import org.springframework.security.web.csrf.CsrfTokenRepository;
14 | import org.springframework.web.filter.OncePerRequestFilter;
15 |
16 | import javax.servlet.FilterChain;
17 | import javax.servlet.RequestDispatcher;
18 | import javax.servlet.ServletException;
19 | import javax.servlet.http.HttpServletRequest;
20 | import javax.servlet.http.HttpServletResponse;
21 | import java.io.IOException;
22 | import java.util.Arrays;
23 |
24 | @Configuration
25 | @ConditionalOnProperty(name = {"jwt.csrf.token.repository.disabled"}, havingValue = "false", matchIfMissing = true)
26 | public class JWTCSRFWebSecurityConfig extends WebSecurityConfigurerAdapter {
27 |
28 | @Autowired
29 | CsrfTokenRepository jwtCsrfTokenRepository;
30 |
31 | @Autowired
32 | SecretService secretService;
33 |
34 | // ordered so we can use binary search below
35 | private String[] ignoreCsrfAntMatchers = {
36 | "/dynamic-builder-compress",
37 | "/dynamic-builder-general",
38 | "/dynamic-builder-specific",
39 | "/set-secrets"
40 | };
41 |
42 | @Override
43 | protected void configure(HttpSecurity http) throws Exception {
44 | http
45 | .addFilterAfter(new JwtCsrfValidatorFilter(), CsrfFilter.class)
46 | .csrf()
47 | .csrfTokenRepository(jwtCsrfTokenRepository)
48 | .ignoringAntMatchers(ignoreCsrfAntMatchers)
49 | .and()
50 | .authorizeRequests()
51 | .antMatchers("/**")
52 | .permitAll();
53 | }
54 |
55 | private class JwtCsrfValidatorFilter extends OncePerRequestFilter {
56 |
57 | @Override
58 | protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
59 | // NOTE: A real implementation should have a nonce cache so the token cannot be reused
60 |
61 | CsrfToken token = (CsrfToken) request.getAttribute("_csrf");
62 |
63 | if (
64 | // only care if it's a POST
65 | "POST".equals(request.getMethod()) &&
66 | // ignore if the request path is in our list
67 | Arrays.binarySearch(ignoreCsrfAntMatchers, request.getServletPath()) < 0 &&
68 | // make sure we have a token
69 | token != null
70 | ) {
71 | // CsrfFilter already made sure the token matched. Here, we'll make sure it's not expired
72 | try {
73 | Jwts.parser()
74 | .setSigningKeyResolver(secretService.getSigningKeyResolver())
75 | .parseClaimsJws(token.getToken());
76 | } catch (JwtException e) {
77 | // most likely an ExpiredJwtException, but this will handle any
78 | request.setAttribute("exception", e);
79 | response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
80 | RequestDispatcher dispatcher = request.getRequestDispatcher("expired-jwt");
81 | dispatcher.forward(request, response);
82 | }
83 | }
84 |
85 | filterChain.doFilter(request, response);
86 | }
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/roadstorm-jwt-csrf-tutorial/src/main/java/io/jsonwebtoken/jjwtfun/controller/DynamicJWTController.java:
--------------------------------------------------------------------------------
1 | package io.jsonwebtoken.jjwtfun.controller;
2 |
3 | import io.jsonwebtoken.JwtBuilder;
4 | import io.jsonwebtoken.JwtException;
5 | import io.jsonwebtoken.Jwts;
6 | import io.jsonwebtoken.SignatureAlgorithm;
7 | import io.jsonwebtoken.impl.compression.CompressionCodecs;
8 | import io.jsonwebtoken.jjwtfun.model.JwtResponse;
9 | import io.jsonwebtoken.jjwtfun.service.SecretService;
10 | import org.springframework.beans.factory.annotation.Autowired;
11 | import org.springframework.web.bind.annotation.RequestBody;
12 | import org.springframework.web.bind.annotation.RequestMapping;
13 | import org.springframework.web.bind.annotation.RestController;
14 |
15 | import java.io.UnsupportedEncodingException;
16 | import java.time.Instant;
17 | import java.util.Date;
18 | import java.util.Map;
19 |
20 | import static org.springframework.web.bind.annotation.RequestMethod.POST;
21 |
22 | @RestController
23 | public class DynamicJWTController extends BaseController {
24 |
25 | @Autowired
26 | SecretService secretService;
27 |
28 | @RequestMapping(value = "/dynamic-builder-general", method = POST)
29 | public JwtResponse dynamicBuilderGeneric(@RequestBody Map claims) throws UnsupportedEncodingException {
30 | String jws = Jwts.builder()
31 | .setClaims(claims)
32 | .signWith(
33 | SignatureAlgorithm.HS256,
34 | secretService.getHS256SecretBytes()
35 | )
36 | .compact();
37 | return new JwtResponse(jws);
38 | }
39 |
40 | @RequestMapping(value = "/dynamic-builder-compress", method = POST)
41 | public JwtResponse dynamicBuildercompress(@RequestBody Map claims) throws UnsupportedEncodingException {
42 | String jws = Jwts.builder()
43 | .setClaims(claims)
44 | .compressWith(CompressionCodecs.DEFLATE)
45 | .signWith(
46 | SignatureAlgorithm.HS256,
47 | secretService.getHS256SecretBytes()
48 | )
49 | .compact();
50 | return new JwtResponse(jws);
51 | }
52 |
53 | @RequestMapping(value = "/dynamic-builder-specific", method = POST)
54 | public JwtResponse dynamicBuilderSpecific(@RequestBody Map claims) throws UnsupportedEncodingException {
55 | JwtBuilder builder = Jwts.builder();
56 |
57 | claims.forEach((key, value) -> {
58 | switch (key) {
59 | case "iss":
60 | ensureType(key, value, String.class);
61 | builder.setIssuer((String) value);
62 | break;
63 | case "sub":
64 | ensureType(key, value, String.class);
65 | builder.setSubject((String) value);
66 | break;
67 | case "aud":
68 | ensureType(key, value, String.class);
69 | builder.setAudience((String) value);
70 | break;
71 | case "exp":
72 | ensureType(key, value, Long.class);
73 | builder.setExpiration(Date.from(Instant.ofEpochSecond(Long.parseLong(value.toString()))));
74 | break;
75 | case "nbf":
76 | ensureType(key, value, Long.class);
77 | builder.setNotBefore(Date.from(Instant.ofEpochSecond(Long.parseLong(value.toString()))));
78 | break;
79 | case "iat":
80 | ensureType(key, value, Long.class);
81 | builder.setIssuedAt(Date.from(Instant.ofEpochSecond(Long.parseLong(value.toString()))));
82 | break;
83 | case "jti":
84 | ensureType(key, value, String.class);
85 | builder.setId((String) value);
86 | break;
87 | default:
88 | builder.claim(key, value);
89 | }
90 | });
91 |
92 | builder.signWith(SignatureAlgorithm.HS256, secretService.getHS256SecretBytes());
93 |
94 | return new JwtResponse(builder.compact());
95 | }
96 |
97 | private void ensureType(String registeredClaim, Object value, Class expectedType) {
98 | boolean isCorrectType =
99 | expectedType.isInstance(value) ||
100 | expectedType == Long.class && value instanceof Integer;
101 |
102 | if (!isCorrectType) {
103 | String msg = "Expected type: " + expectedType.getCanonicalName() + " for registered claim: '" +
104 | registeredClaim + "', but got value: " + value + " of type: " + value.getClass().getCanonicalName();
105 | throw new JwtException(msg);
106 | }
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/roadstorm-jwt-microservices-tutorial/README.md:
--------------------------------------------------------------------------------
1 | # Securing Microservices with JJWT Tutorial
2 |
3 | The purpose of this tutorial is to demonstrate how the [JJWT](https://github.com/jwtk/jjwt) library can be used to secure microservices.
4 |
5 | The only dependencies for interacting purely with HTTP are the Spring Boot Web Starter and the JJWT library.
6 |
7 | There's also a messaging mode (disabled by default) that requires [Kafka](http://kafka.apache.org/documentation.html#quickstart).
8 | All you need to do is follow steps one and two in the Kafka quickstart to get it setup for use with this tutorial.
9 |
10 | Wondering what JWTs and/or the JJWT library is all about? Click [here](https://java.jsonwebtoken.io).
11 |
12 | ## What Does the App Do?
13 |
14 | This application demonstrates many of the critical functions of microservices that need to communicate with each other.
15 |
16 | This includes:
17 |
18 | * Creation of private/public key pair
19 | * Registration of public key from one service to another service
20 | * Creation of JWTs signed with private key
21 | * Verification of JWTs using public key
22 | * Example of Account Resolution Service using signed JWTs
23 | * Example of JWT communication between microservices using Kafka messaging
24 |
25 | ## Building the App
26 |
27 | Easy peasy:
28 |
29 | ```
30 | mvn clean install
31 | ```
32 |
33 | ## Running the App
34 |
35 | To exercise the communication between microservices, you'll want to run at least two instances of the application.
36 |
37 | Building the app creates a fully standalone executable jar. You can run multiple instances like so:
38 |
39 | ```
40 | target/*.jar --server.port=8080 &
41 | target/*.jar --server.port=8081 &
42 | ```
43 |
44 | This will run one instance on port `8080` and one on `8081` and they will both be put in the background.
45 |
46 | You can also use the purple Heroku button below to deploy to your own Heroku account. Setup two different instances
47 | so you can communicate between them.
48 |
49 | [](https://heroku.com/deploy)
50 |
51 | ## Service Registry
52 |
53 | Note: all service to service communication examples below use [httpie](https://github.com/jkbrzt/httpie)
54 |
55 | When the application is launched, a private/public keypair is automatically created. All operations involving keys
56 | are handled via the `SecretService` service and exposed via endpoints in the `SecretServiceController`.
57 |
58 | Below are the available endpoints from `SecretServiceController`:
59 |
60 | 1. `/refresh-my-creds` - Create a new private/public key pair for this microservice instance.
61 | 2. `/get-my-public-creds` - Return the Base64 URL Encoded version of this microservice instance's Public Key and its `kid`.
62 | 3. `/add-public-creds` - Register the Public Key of one microservice instance on another microservice instance.
63 | 4. `/test-build` - Returns a JWS signed with the instance's private key. The JWS includes the instance's `kid` as a header param.
64 | 5. `/test-parse` - Takes a JWS as a parameter and attempts to parse it by looking up the public key identified by the `kid`.
65 |
66 | Let's look at this in action:
67 |
68 | Let's first try to have one microservice communicate with the other *without* establishing trust:
69 |
70 | `http localhost:8080/test-build`
71 |
72 | HTTP/1.1 200 OK
73 | Content-Type: application/json;charset=UTF-8
74 | Date: Mon, 18 Jul 2016 04:42:09 GMT
75 | Server: Apache-Coyote/1.1
76 | Transfer-Encoding: chunked
77 |
78 | {
79 | "jwt": "eyJraWQiOiI5NzYzMWI5YS0yZjM0LTRhYzQtOGMxYy1kN2U3MmZkYTExMGYiLCJhbGciOiJSUzI1NiJ9...",
80 | "status": "SUCCESS"
81 | }
82 |
83 | `http localhost:8081/test-parse?jwt=eyJraWQiOiI5NzYzMWI5YS0yZjM0LTRhYzQtOGMxYy1kN2U3MmZkYTExMGYiLCJhbGciOiJSUzI1NiJ9...`
84 |
85 | HTTP/1.1 400 Bad Request
86 | Connection: close
87 | Content-Type: application/json;charset=UTF-8
88 | Date: Mon, 18 Jul 2016 04:42:32 GMT
89 | Server: Apache-Coyote/1.1
90 | Transfer-Encoding: chunked
91 |
92 | {
93 | "exceptionType": "io.jsonwebtoken.JwtException",
94 | "message": "No public key registered for kid: 97631b9a-2f34-4ac4-8c1c-d7e72fda110f. JWT claims: {iss=Stormpath, sub=msilverman, name=Micah Silverman, hasMotorcycle=true, iat=1466796822, exp=4622470422}",
95 | "status": "ERROR"
96 | }
97 |
98 | Notice that our second microservice cannot parse the JWT since it doesn't have the public key in its registry.
99 |
100 | Now, let's register the first microservice's public key with the second microservice and then try the above operation again:
101 |
102 | `http localhost:8080/get-my-public-creds`
103 |
104 | HTTP/1.1 200 OK
105 | Content-Type: application/json;charset=UTF-8
106 | Date: Mon, 18 Jul 2016 04:47:26 GMT
107 | Server: Apache-Coyote/1.1
108 | Transfer-Encoding: chunked
109 |
110 | {
111 | "b64UrlPublicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCo6Lfrn...",
112 | "kid": "97631b9a-2f34-4ac4-8c1c-d7e72fda110f"
113 | }
114 |
115 | ```
116 | http POST localhost:8081/add-public-creds \
117 | b64UrlPublicKey="MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCo6Lfrn..." \
118 | kid="97631b9a-2f34-4ac4-8c1c-d7e72fda110f"
119 | ```
120 |
121 | HTTP/1.1 200 OK
122 | Content-Type: application/json;charset=UTF-8
123 | Date: Mon, 18 Jul 2016 04:51:25 GMT
124 | Server: Apache-Coyote/1.1
125 | Transfer-Encoding: chunked
126 |
127 | {
128 | "b64UrlPublicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCo6Lfrn...",
129 | "kid": "97631b9a-2f34-4ac4-8c1c-d7e72fda110f"
130 | }
131 |
132 | Now, we can re-run our `/test-parse` endpoint using the same JWT from before:
133 |
134 | `http localhost:8081/test-parse?jwt=eyJraWQiOiI5NzYzMWI5YS0yZjM0LTRhYzQtOGMxYy1kN2U3MmZkYTExMGYiLCJhbGciOiJSUzI1NiJ9...`
135 |
136 | HTTP/1.1 200 OK
137 | Content-Type: application/json;charset=UTF-8
138 | Date: Mon, 18 Jul 2016 04:52:47 GMT
139 | Server: Apache-Coyote/1.1
140 | Transfer-Encoding: chunked
141 |
142 | {
143 | "jws": {
144 | "body": {
145 | "exp": 4622470422,
146 | "hasMotorcycle": true,
147 | "iat": 1466796822,
148 | "iss": "Stormpath",
149 | "name": "Micah Silverman",
150 | "sub": "msilverman"
151 | },
152 | "header": {
153 | "alg": "RS256",
154 | "kid": "97631b9a-2f34-4ac4-8c1c-d7e72fda110f"
155 | },
156 | "signature": "phsExAX5CflcLJJQ-q4xYEOq9gbtu7DxzokMq_yPKz2Bx-TQz72EdG25HssNGnkiOCCDVH7iSnaARoiIBPgRKj4W8FstVBR1I3hreIS4MrqMZBaDrS62xwyVnCU1HIMvsqOj6hHBwIowQwlTld887C1hznpTjk74Q1__Vk_wZJU"
157 | },
158 | "status": "SUCCESS"
159 | }
160 |
161 | This time, our second microservice is able to parse the JWT from the first microservice since we registered the public key with it.
162 |
163 | ## Account Resolution
164 |
165 | In this part of the tutorial, we introduce an `AccountResolver`. This interface exposes an `INSTANCE` that can then be used to lookup an `Account`.
166 | For the purposes of the tutorial, three accounts are setup that represent the "database" of accounts.
167 |
168 | The `AccountResolver` implementation expects a JWT that has a `userName` claim that will be used to lookup the account.
169 |
170 | The microservice that is doing the account resolution will need to retrieve the bearer token from the request (the JWT) and it will need to be able to parse the JWT to pull out the `userName` claim.
171 | Like before, the public key of the microservice that created the JWT will need to be registered with the microservice that will be parsing the JWT.
172 |
173 | The `MicroServiceController` exposes two endpoints to manage these interactions:
174 |
175 | 1. `/account-request` - Generate a JWT with a 60-second expiration. It can take in any number of claims. `userName` claim is required.
176 | 2. `/restricted` - Return an `Account` based on processing a bearer token
177 |
178 | Let's see this in action. Note: this assumes that you've registered the public key from the first microservice with the second microservice.
179 |
180 | `http POST localhost:8080/account-request username=anna`
181 |
182 | HTTP/1.1 200 OK
183 | Content-Type: application/json;charset=UTF-8
184 | Date: Mon, 18 Jul 2016 05:13:56 GMT
185 | Server: Apache-Coyote/1.1
186 | Transfer-Encoding: chunked
187 |
188 | {
189 | "jwt": "eyJraWQiOiI5NzYzMWI5YS0yZjM0LTRhYzQtOGMxYy1kN2U3MmZkYTExMGYiLCJhbGciOiJSUzI1NiJ9...",
190 | "status": "SUCCESS"
191 | }
192 |
193 | `http localhost:8081/restricted Authorization:"Bearer eyJraWQiOiI5NzYzMWI5YS0yZjM0LTRhYzQtOGMxYy1kN2U3MmZkYTExMGYiLCJhbGciOiJSUzI1NiJ9..."`
194 |
195 | HTTP/1.1 200 OK
196 | Content-Type: application/json;charset=UTF-8
197 | Date: Mon, 18 Jul 2016 05:16:26 GMT
198 | Server: Apache-Coyote/1.1
199 | Transfer-Encoding: chunked
200 |
201 | {
202 | "account": {
203 | "firstName": "Anna",
204 | "lastName": "Apple",
205 | "userName": "anna"
206 | },
207 | "message": "Found Account",
208 | "status": "SUCCESS"
209 | }
210 |
211 | The above request uses the standard `Authorization` header as part of the request to the second microservice using the JWT from the first microservice.
212 |
213 | ## Microservice Communication with messages
214 |
215 | While the HTTP examples above are simple, HTTP just isn't a good protocol for microservice communication.
216 |
217 | It's a synchronous protocol that is easily overwhelmed (Think [DDOS](https://en.wikipedia.org/wiki/Denial-of-service_attack)).
218 |
219 | [Apache Kafka](http://kafka.apache.org/) is a popular, highly scalable pub/sub messaging platform with robust libraries in Java.
220 |
221 | Follow these steps to use the messaging mode of the sample app:
222 |
223 | 1. Setup Kafka
224 |
225 | An exhaustive discussion of Kafka is outside the scope of this tutorial.
226 | However, if you follow the first two steps of the [quickstart](http://kafka.apache.org/documentation.html#quickstart), you'll have a local environment that's ready for this tutorial to work with.
227 |
228 | 2. Configure the tutorial
229 |
230 | This is the `application.properties` file of this tutorial:
231 |
232 | ```
233 | kafka.enabled=false
234 | kafka.broker.address=localhost:9092
235 | zookeeper.address=localhost:2181
236 | topic=micro-services
237 | ```
238 |
239 | Simply change the first line to: `kafka.enabled=true`
240 |
241 | **Note**: You will get lots of error output if Kafka is enabled in the tutorial application, but it is not running on your machine.
242 |
243 | 3. Build the tutorial
244 |
245 | Just like before:
246 |
247 | `mvn clean install`
248 |
249 | 4. Run the tutorial app
250 |
251 | Open up two terminal windows. In one, run:
252 |
253 | `target/*.jar --server.port=8080`
254 |
255 | You'll notice some new output from Kafka. This microservice will be producing messages.
256 |
257 | In the second terminal window, run:
258 |
259 | `target/*.jar --server.port=8081 --kafka.consumer.enabled=true`
260 |
261 | This microservice will be consuming messages.
262 |
263 |
264 | 5. Exercise the application
265 |
266 | 1. `http localhost:8080/msg-account-request userName=anna`
267 |
268 | In the `8080` terminal window, you will see a response like this:
269 |
270 | ```
271 | HTTP/1.1 200
272 | Content-Type: application/json;charset=UTF-8
273 | Date: Tue, 23 Aug 2016 16:59:30 GMT
274 | Transfer-Encoding: chunked
275 |
276 | {
277 | "jwt": "eyJraWQiOiI2YjllZTE5YS1mMTc0LTRjNzctYWE5Ni05MjJhYmE4YTc4NzkiLCJhbGciOiJSUzI1NiJ9.eyJ1c2VyTmFtZSI6ImFubmEiLCJpYXQiOjE0NzE5NzE1NjksIm5iZiI6MTQ3MTk3MTU2OSwiZXhwIjoxNDcxOTcxNjI5fQ.Tmf934D_H_Kuz5NxqYBbZfkR0PhYBB0pNdSx8cycP712xdtXz0vUqEJHNN-RQeN1Gwu6CiKc4FUEQIRap0AhfIbFNfs5bjdJODRKGasPGFhT2hbTU8zpF43Z3DujX4mXrS4eEUVpdTMWxc2ISvR_UvfwvwwcVO-pgTqjz8WCdqk",
278 | "status": "SUCCESS"
279 | }
280 | ```
281 |
282 | That's the JWT request that was created. The JWT is sent as a message which is picked up by the `8081` consumer.
283 |
284 | In the `8081` terminal window, you will see a response like this:
285 |
286 | ```
287 | INFO record offset: 12, record value: eyJraWQiOiI2YjllZTE5YS1mMTc0LTRjNzctYWE5Ni05MjJhYmE4YTc4NzkiLCJhbGciOiJSUzI1NiJ9.eyJ1c2VyTmFtZSI6ImFubmEiLCJpYXQiOjE0NzE5NzE1NjksIm5iZiI6MTQ3MTk3MTU2OSwiZXhwIjoxNDcxOTcxNjI5fQ.Tmf934D_H_Kuz5NxqYBbZfkR0PhYBB0pNdSx8cycP712xdtXz0vUqEJHNN-RQeN1Gwu6CiKc4FUEQIRap0AhfIbFNfs5bjdJODRKGasPGFhT2hbTU8zpF43Z3DujX4mXrS4eEUVpdTMWxc2ISvR_UvfwvwwcVO-pgTqjz8WCdqk
288 | ERROR Unable to get account: No public key registered for kid: 6b9ee19a-f174-4c77-aa96-922aba8a7879. JWT claims: {userName=anna, iat=1471971569, nbf=1471971569, exp=1471971629}
289 | ```
290 |
291 | The good news is that the consumer got the message. The bad news is that the `8081` microservice doesn't trust the `8080` microservice. That is, the `8080` microservice has not registered its public key with the `8081` microservice, so there's no way for it to verify the signature of the JWT that was created with teh `8080` microservice's private key.
292 |
293 | 2. Establish trust
294 |
295 | Just like before, do:
296 |
297 | `http localhost:8080/get-my-public-creds`
298 |
299 | Take the data from that response and add the public key to the other microservice:
300 |
301 | ```
302 | http POST localhost:8081/add-public-creds \
303 | b64UrlPublicKey= \
304 | kid=
305 | ```
306 |
307 | 3. Again: `http localhost:8080/msg-account-request userName=anna`
308 |
309 | This time, you will see a log messages like this on the `8081` microservice:
310 |
311 | ```
312 | INFO record offset: 13, record value: eyJraWQiOiI2YjllZTE5YS1mMTc0LTRjNzctYWE5Ni05MjJhYmE4YTc4NzkiLCJhbGciOiJSUzI1NiJ9.eyJ1c2VyTmFtZSI6ImFubmEiLCJpYXQiOjE0NzE5NzIyMDIsIm5iZiI6MTQ3MTk3MjIwMiwiZXhwIjoxNDcxOTcyMjYyfQ.3C2tz_PgIzkMXZMoDTLyPgxfZUQbsK6crnwc1Fu3-5btJKDV4nnq6S07wFwGNhksD365jOAF7NSHSWo8PNfHR9XPQQXhKVmkdnTCr9XO1cZTHdsUo2yH3TWvLxT2i7a4QxTvGHFcxsookX5cOUCGaT4gq5PeeN-1TRE22Xd2Di8
313 | INFO Account name extracted from JWT: Anna Apple
314 | ```
315 |
316 | Now, the consumer is able to verify the signature on the incoming JWT and it does an account lookup based on the `userName` claim
--------------------------------------------------------------------------------