├── .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 |
8 |
9 |
10 |

11 |

12 | 13 |
14 |
15 |
16 |
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 | [![Deploy](https://www.herokucdn.com/deploy/button.svg)](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 --------------------------------------------------------------------------------