diff --git a/jjwt/.gitignore b/jjwt/.gitignore new file mode 100644 index 0000000000..f83e8cf07c --- /dev/null +++ b/jjwt/.gitignore @@ -0,0 +1,3 @@ +.idea +target +*.iml diff --git a/jjwt/README.md b/jjwt/README.md new file mode 100644 index 0000000000..47b51038a8 --- /dev/null +++ b/jjwt/README.md @@ -0,0 +1,45 @@ +## JWT Fun + +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. + +### Build and Run + +It's super easy to build and exercise this tutorial. + +``` +mvn clean install +java -jar target/*.jar +``` + +That's it! + +You can hit the home endpoint with your favorite command-line http client. My favorite is: [httpie](https://github.com/jkbrzt/httpie) + +`http localhost:8080` + +``` +Available commands (assumes httpie - https://github.com/jkbrzt/httpie): + + http http://localhost:8080/ + This usage message + + http http://localhost:8080/static-builder + build JWT from hardcoded claims + + http POST http://localhost:8080/dynamic-builder-general claim-1=value-1 ... [claim-n=value-n] + build JWT from passed in claims (using general claims map) + + http POST http://localhost:8080/dynamic-builder-specific claim-1=value-1 ... [claim-n=value-n] + build JWT from passed in claims (using specific claims methods) + + http POST http://localhost:8080/dynamic-builder-compress claim-1=value-1 ... [claim-n=value-n] + build DEFLATE compressed JWT from passed in claims + + http http://localhost:8080/parser?jwt= + Parse passed in JWT + + http http://localhost:8080/parser-enforce?jwt= + Parse passed in JWT enforcing the 'iss' registered claim and the 'hasMotorcycle' custom claim +``` + +The Baeldung post that compliments this repo can be found [here](http://www.baeldung.com/) \ No newline at end of file diff --git a/jjwt/pom.xml b/jjwt/pom.xml new file mode 100644 index 0000000000..24f1c5c5ab --- /dev/null +++ b/jjwt/pom.xml @@ -0,0 +1,65 @@ + + + 4.0.0 + + io.jsonwebtoken + jjwtfun + 0.0.1-SNAPSHOT + jar + + jjwtfun + Exercising the JJWT + + + org.springframework.boot + spring-boot-starter-parent + 1.3.5.RELEASE + + + + + UTF-8 + 1.8 + + + + + org.springframework.boot + spring-boot-devtools + + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.boot + spring-boot-starter-test + test + + + + io.jsonwebtoken + jjwt + 0.6.0 + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + diff --git a/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/JJWTFunApplication.java b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/JJWTFunApplication.java new file mode 100644 index 0000000000..5c106aac70 --- /dev/null +++ b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/JJWTFunApplication.java @@ -0,0 +1,12 @@ +package io.jsonwebtoken.jjwtfun; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class JJWTFunApplication { + + public static void main(String[] args) { + SpringApplication.run(JJWTFunApplication.class, args); + } +} diff --git a/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/config/CSRFConfig.java b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/config/CSRFConfig.java new file mode 100644 index 0000000000..7d88835243 --- /dev/null +++ b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/config/CSRFConfig.java @@ -0,0 +1,21 @@ +package io.jsonwebtoken.jjwtfun.config; + +import io.jsonwebtoken.jjwtfun.service.SecretService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.web.csrf.CsrfTokenRepository; + +@Configuration +public class CSRFConfig { + + @Autowired + SecretService secretService; + + @Bean + @ConditionalOnMissingBean + public CsrfTokenRepository jwtCsrfTokenRepository() { + return new JWTCsrfTokenRepository(secretService.getHS256SecretBytes()); + } +} diff --git a/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/config/JWTCsrfTokenRepository.java b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/config/JWTCsrfTokenRepository.java new file mode 100644 index 0000000000..bf88b8aff1 --- /dev/null +++ b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/config/JWTCsrfTokenRepository.java @@ -0,0 +1,68 @@ +package io.jsonwebtoken.jjwtfun.config; + +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.web.csrf.CsrfToken; +import org.springframework.security.web.csrf.CsrfTokenRepository; +import org.springframework.security.web.csrf.DefaultCsrfToken; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.util.Date; +import java.util.UUID; + +public class JWTCsrfTokenRepository implements CsrfTokenRepository { + + private static final String DEFAULT_CSRF_TOKEN_ATTR_NAME = CSRFConfig.class.getName().concat(".CSRF_TOKEN"); + + private static final Logger log = LoggerFactory.getLogger(JWTCsrfTokenRepository.class); + private byte[] secret; + + public JWTCsrfTokenRepository(byte[] secret) { + this.secret = secret; + } + + @Override + public CsrfToken generateToken(HttpServletRequest request) { + String id = UUID.randomUUID().toString().replace("-", ""); + + Date now = new Date(); + Date exp = new Date(System.currentTimeMillis() + (1000*30)); // 30 seconds + + String token = Jwts.builder() + .setId(id) + .setIssuedAt(now) + .setNotBefore(now) + .setExpiration(exp) + .signWith(SignatureAlgorithm.HS256, secret) + .compact(); + + return new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", token); + } + + @Override + public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) { + if (token == null) { + HttpSession session = request.getSession(false); + if (session != null) { + session.removeAttribute(DEFAULT_CSRF_TOKEN_ATTR_NAME); + } + } + else { + HttpSession session = request.getSession(); + session.setAttribute(DEFAULT_CSRF_TOKEN_ATTR_NAME, token); + } + } + + @Override + public CsrfToken loadToken(HttpServletRequest request) { + HttpSession session = request.getSession(false); + if (session == null || "GET".equals(request.getMethod())) { + return null; + } + return (CsrfToken) session.getAttribute(DEFAULT_CSRF_TOKEN_ATTR_NAME); + } +} diff --git a/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/config/WebSecurityConfig.java b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/config/WebSecurityConfig.java new file mode 100644 index 0000000000..94e2c6ddc5 --- /dev/null +++ b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/config/WebSecurityConfig.java @@ -0,0 +1,85 @@ +package io.jsonwebtoken.jjwtfun.config; + +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.jjwtfun.service.SecretService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.web.csrf.CsrfFilter; +import org.springframework.security.web.csrf.CsrfToken; +import org.springframework.security.web.csrf.CsrfTokenRepository; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.RequestDispatcher; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Arrays; + +@Configuration +public class WebSecurityConfig extends WebSecurityConfigurerAdapter { + + @Autowired + CsrfTokenRepository jwtCsrfTokenRepository; + + @Autowired + SecretService secretService; + + // ordered so we can use binary search below + private String[] ignoreCsrfAntMatchers = { + "/dynamic-builder-compress", + "/dynamic-builder-general", + "/dynamic-builder-specific", + "/set-secrets" + }; + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .addFilterAfter(new JwtCsrfValidatorFilter(), CsrfFilter.class) + .csrf() + .csrfTokenRepository(jwtCsrfTokenRepository) + .ignoringAntMatchers(ignoreCsrfAntMatchers) + .and().authorizeRequests() + .antMatchers("/**") + .permitAll(); + } + + private class JwtCsrfValidatorFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + // NOTE: A real implementation should have a nonce cache so the token cannot be reused + + CsrfToken token = (CsrfToken) request.getAttribute("_csrf"); + + if ( + // only care if it's a POST + "POST".equals(request.getMethod()) && + // ignore if the request path is in our list + Arrays.binarySearch(ignoreCsrfAntMatchers, request.getServletPath()) < 0 && + // make sure we have a token + token != null + ) { + // CsrfFilter already made sure the token matched. Here, we'll make sure it's not expired + try { + Jwts.parser() + .setSigningKeyResolver(secretService.getSigningKeyResolver()) + .parseClaimsJws(token.getToken()); + } catch (JwtException e) { + // most likely an ExpiredJwtException, but this will handle any + request.setAttribute("exception", e); + response.setStatus(HttpServletResponse.SC_BAD_REQUEST); + RequestDispatcher dispatcher = request.getRequestDispatcher("expired-jwt"); + dispatcher.forward(request, response); + } + } + + filterChain.doFilter(request, response); + } + } +} diff --git a/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/BaseController.java b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/BaseController.java new file mode 100644 index 0000000000..e1e195c6ab --- /dev/null +++ b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/BaseController.java @@ -0,0 +1,23 @@ +package io.jsonwebtoken.jjwtfun.controller; + +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.SignatureException; +import io.jsonwebtoken.jjwtfun.model.JwtResponse; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; + +public class BaseController { + + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler({SignatureException.class, MalformedJwtException.class, JwtException.class}) + public JwtResponse exception(Exception e) { + JwtResponse response = new JwtResponse(); + response.setStatus(JwtResponse.Status.ERROR); + response.setMessage(e.getMessage()); + response.setExceptionType(e.getClass().getName()); + + return response; + } +} diff --git a/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/DynamicJWTController.java b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/DynamicJWTController.java new file mode 100644 index 0000000000..c03c63dd80 --- /dev/null +++ b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/DynamicJWTController.java @@ -0,0 +1,108 @@ +package io.jsonwebtoken.jjwtfun.controller; + +import io.jsonwebtoken.JwtBuilder; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.impl.compression.CompressionCodecs; +import io.jsonwebtoken.jjwtfun.model.JwtResponse; +import io.jsonwebtoken.jjwtfun.service.SecretService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.io.UnsupportedEncodingException; +import java.time.Instant; +import java.util.Date; +import java.util.Map; + +import static org.springframework.web.bind.annotation.RequestMethod.POST; + +@RestController +public class DynamicJWTController extends BaseController { + + @Autowired + SecretService secretService; + + @RequestMapping(value = "/dynamic-builder-general", method = POST) + public JwtResponse dynamicBuilderGeneric(@RequestBody Map claims) throws UnsupportedEncodingException { + String jws = Jwts.builder() + .setClaims(claims) + .signWith( + SignatureAlgorithm.HS256, + secretService.getHS256SecretBytes() + ) + .compact(); + return new JwtResponse(jws); + } + + @RequestMapping(value = "/dynamic-builder-compress", method = POST) + public JwtResponse dynamicBuildercompress(@RequestBody Map claims) throws UnsupportedEncodingException { + String jws = Jwts.builder() + .setClaims(claims) + .compressWith(CompressionCodecs.DEFLATE) + .signWith( + SignatureAlgorithm.HS256, + secretService.getHS256SecretBytes() + ) + .compact(); + return new JwtResponse(jws); + } + + @RequestMapping(value = "/dynamic-builder-specific", method = POST) + public JwtResponse dynamicBuilderSpecific(@RequestBody Map claims) throws UnsupportedEncodingException { + JwtBuilder builder = Jwts.builder(); + + claims.forEach((key, value) -> { + switch (key) { + case "iss": + ensureType(key, value, String.class); + builder.setIssuer((String) value); + break; + case "sub": + ensureType(key, value, String.class); + builder.setSubject((String) value); + break; + case "aud": + ensureType(key, value, String.class); + builder.setAudience((String) value); + break; + case "exp": + ensureType(key, value, Long.class); + builder.setExpiration(Date.from(Instant.ofEpochSecond(Long.parseLong(value.toString())))); + break; + case "nbf": + ensureType(key, value, Long.class); + builder.setNotBefore(Date.from(Instant.ofEpochSecond(Long.parseLong(value.toString())))); + break; + case "iat": + ensureType(key, value, Long.class); + builder.setIssuedAt(Date.from(Instant.ofEpochSecond(Long.parseLong(value.toString())))); + break; + case "jti": + ensureType(key, value, String.class); + builder.setId((String) value); + break; + default: + builder.claim(key, value); + } + }); + + builder.signWith(SignatureAlgorithm.HS256, secretService.getHS256SecretBytes()); + + return new JwtResponse(builder.compact()); + } + + private void ensureType(String registeredClaim, Object value, Class expectedType) { + boolean isCorrectType = + expectedType.isInstance(value) || + expectedType == Long.class && value instanceof Integer; + + if (!isCorrectType) { + String msg = "Expected type: " + expectedType.getCanonicalName() + " for registered claim: '" + + registeredClaim + "', but got value: " + value + " of type: " + value.getClass().getCanonicalName(); + throw new JwtException(msg); + } + } +} diff --git a/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/FormController.java b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/FormController.java new file mode 100644 index 0000000000..54123c63cb --- /dev/null +++ b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/FormController.java @@ -0,0 +1,29 @@ +package io.jsonwebtoken.jjwtfun.controller; + +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; + +import static org.springframework.web.bind.annotation.RequestMethod.GET; +import static org.springframework.web.bind.annotation.RequestMethod.POST; + +@Controller +public class FormController { + + @RequestMapping(value = "/jwt-csrf-form", method = GET) + public String csrfFormGet() { + return "jwt-csrf-form"; + } + + @RequestMapping(value = "/jwt-csrf-form", method = POST) + public String csrfFormPost(@RequestParam(name = "_csrf") String csrf, Model model) { + model.addAttribute("csrf", csrf); + return "jwt-csrf-form-result"; + } + + @RequestMapping("/expired-jwt") + public String expiredJwt() { + return "expired-jwt"; + } +} diff --git a/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/HomeController.java b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/HomeController.java new file mode 100644 index 0000000000..57cd14385e --- /dev/null +++ b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/HomeController.java @@ -0,0 +1,32 @@ +package io.jsonwebtoken.jjwtfun.controller; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletRequest; + +@RestController +public class HomeController { + + @RequestMapping("/") + public String home(HttpServletRequest req) { + String requestUrl = getUrl(req); + return "Available commands (assumes httpie - https://github.com/jkbrzt/httpie):\n\n" + + " http " + requestUrl + "/\n\tThis usage message\n\n" + + " http " + requestUrl + "/static-builder\n\tbuild JWT from hardcoded claims\n\n" + + " 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" + + " 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" + + " 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" + + " http " + requestUrl + "/parser?jwt=\n\tParse passed in JWT\n\n" + + " http " + requestUrl + "/parser-enforce?jwt=\n\tParse passed in JWT enforcing the 'iss' registered claim and the 'hasMotorcycle' custom claim\n\n" + + " http " + requestUrl + "/get-secrets\n\tShow the signing keys currently in use.\n\n" + + " http " + requestUrl + "/refresh-secrets\n\tGenerate new signing keys and show them.\n\n" + + " 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."; + } + + private String getUrl(HttpServletRequest req) { + return req.getScheme() + "://" + + req.getServerName() + + ((req.getServerPort() == 80 || req.getServerPort() == 443) ? "" : ":" + req.getServerPort()); + } +} diff --git a/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/SecretsController.java b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/SecretsController.java new file mode 100644 index 0000000000..1ca0973c33 --- /dev/null +++ b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/SecretsController.java @@ -0,0 +1,35 @@ +package io.jsonwebtoken.jjwtfun.controller; + +import io.jsonwebtoken.jjwtfun.service.SecretService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Map; + +import static org.springframework.web.bind.annotation.RequestMethod.GET; +import static org.springframework.web.bind.annotation.RequestMethod.POST; + +@RestController +public class SecretsController extends BaseController { + + @Autowired + SecretService secretService; + + @RequestMapping(value = "/get-secrets", method = GET) + public Map getSecrets() { + return secretService.getSecrets(); + } + + @RequestMapping(value = "/refresh-secrets", method = GET) + public Map refreshSecrets() { + return secretService.refreshSecrets(); + } + + @RequestMapping(value = "/set-secrets", method = POST) + public Map setSecrets(@RequestBody Map secrets) { + secretService.setSecrets(secrets); + return secretService.getSecrets(); + } +} diff --git a/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/StaticJWTController.java b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/StaticJWTController.java new file mode 100644 index 0000000000..83f5336978 --- /dev/null +++ b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/controller/StaticJWTController.java @@ -0,0 +1,64 @@ +package io.jsonwebtoken.jjwtfun.controller; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.jjwtfun.model.JwtResponse; +import io.jsonwebtoken.jjwtfun.service.SecretService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.io.UnsupportedEncodingException; +import java.time.Instant; +import java.util.Date; + +import static org.springframework.web.bind.annotation.RequestMethod.GET; + +@RestController +public class StaticJWTController extends BaseController { + + @Autowired + SecretService secretService; + + @RequestMapping(value = "/static-builder", method = GET) + public JwtResponse fixedBuilder() throws UnsupportedEncodingException { + String jws = Jwts.builder() + .setIssuer("Stormpath") + .setSubject("msilverman") + .claim("name", "Micah Silverman") + .claim("scope", "admins") + .setIssuedAt(Date.from(Instant.ofEpochSecond(1466796822L))) // Fri Jun 24 2016 15:33:42 GMT-0400 (EDT) + .setExpiration(Date.from(Instant.ofEpochSecond(4622470422L))) // Sat Jun 24 2116 15:33:42 GMT-0400 (EDT) + .signWith( + SignatureAlgorithm.HS256, + secretService.getHS256SecretBytes() + ) + .compact(); + + return new JwtResponse(jws); + } + + @RequestMapping(value = "/parser", method = GET) + public JwtResponse parser(@RequestParam String jwt) throws UnsupportedEncodingException { + + Jws jws = Jwts.parser() + .setSigningKeyResolver(secretService.getSigningKeyResolver()) + .parseClaimsJws(jwt); + + return new JwtResponse(jws); + } + + @RequestMapping(value = "/parser-enforce", method = GET) + public JwtResponse parserEnforce(@RequestParam String jwt) throws UnsupportedEncodingException { + Jws jws = Jwts.parser() + .requireIssuer("Stormpath") + .require("hasMotorcycle", true) + .setSigningKeyResolver(secretService.getSigningKeyResolver()) + .parseClaimsJws(jwt); + + return new JwtResponse(jws); + } +} diff --git a/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/model/JwtResponse.java b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/model/JwtResponse.java new file mode 100644 index 0000000000..491f003289 --- /dev/null +++ b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/model/JwtResponse.java @@ -0,0 +1,70 @@ +package io.jsonwebtoken.jjwtfun.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public class JwtResponse { + private String message; + private Status status; + private String exceptionType; + private String jwt; + private Jws jws; + + public enum Status { + SUCCESS, ERROR + } + + public JwtResponse() {} + + public JwtResponse(String jwt) { + this.jwt = jwt; + this.status = Status.SUCCESS; + } + + public JwtResponse(Jws jws) { + this.jws = jws; + this.status = Status.SUCCESS; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public Status getStatus() { + return status; + } + + public void setStatus(Status status) { + this.status = status; + } + + public String getExceptionType() { + return exceptionType; + } + + public void setExceptionType(String exceptionType) { + this.exceptionType = exceptionType; + } + + public String getJwt() { + return jwt; + } + + public void setJwt(String jwt) { + this.jwt = jwt; + } + + public Jws getJws() { + return jws; + } + + public void setJws(Jws jws) { + this.jws = jws; + } +} diff --git a/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/service/SecretService.java b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/service/SecretService.java new file mode 100644 index 0000000000..4311afa592 --- /dev/null +++ b/jjwt/src/main/java/io/jsonwebtoken/jjwtfun/service/SecretService.java @@ -0,0 +1,74 @@ +package io.jsonwebtoken.jjwtfun.service; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwsHeader; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.SigningKeyResolver; +import io.jsonwebtoken.SigningKeyResolverAdapter; +import io.jsonwebtoken.impl.TextCodec; +import io.jsonwebtoken.impl.crypto.MacProvider; +import io.jsonwebtoken.lang.Assert; +import org.springframework.stereotype.Service; + +import javax.annotation.PostConstruct; +import javax.crypto.SecretKey; +import java.util.HashMap; +import java.util.Map; + +@Service +public class SecretService { + + private Map secrets = new HashMap<>(); + + private SigningKeyResolver signingKeyResolver = new SigningKeyResolverAdapter() { + @Override + public byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) { + return TextCodec.BASE64.decode(secrets.get(header.getAlgorithm())); + } + }; + + @PostConstruct + public void setup() { + refreshSecrets(); + } + + public SigningKeyResolver getSigningKeyResolver() { + return signingKeyResolver; + } + + public Map getSecrets() { + return secrets; + } + + public void setSecrets(Map secrets) { + Assert.notNull(secrets); + Assert.hasText(secrets.get(SignatureAlgorithm.HS256.getValue())); + Assert.hasText(secrets.get(SignatureAlgorithm.HS384.getValue())); + Assert.hasText(secrets.get(SignatureAlgorithm.HS512.getValue())); + + this.secrets = secrets; + } + + public byte[] getHS256SecretBytes() { + return TextCodec.BASE64.decode(secrets.get(SignatureAlgorithm.HS256.getValue())); + } + + public byte[] getHS384SecretBytes() { + return TextCodec.BASE64.decode(secrets.get(SignatureAlgorithm.HS384.getValue())); + } + + public byte[] getHS512SecretBytes() { + return TextCodec.BASE64.decode(secrets.get(SignatureAlgorithm.HS384.getValue())); + } + + + public Map refreshSecrets() { + SecretKey key = MacProvider.generateKey(SignatureAlgorithm.HS256); + secrets.put(SignatureAlgorithm.HS256.getValue(), TextCodec.BASE64.encode(key.getEncoded())); + key = MacProvider.generateKey(SignatureAlgorithm.HS384); + secrets.put(SignatureAlgorithm.HS384.getValue(), TextCodec.BASE64.encode(key.getEncoded())); + key = MacProvider.generateKey(SignatureAlgorithm.HS512); + secrets.put(SignatureAlgorithm.HS512.getValue(), TextCodec.BASE64.encode(key.getEncoded())); + return secrets; + } +} diff --git a/jjwt/src/main/resources/application.properties b/jjwt/src/main/resources/application.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jjwt/src/main/resources/templates/expired-jwt.html b/jjwt/src/main/resources/templates/expired-jwt.html new file mode 100644 index 0000000000..7bf9ff258e --- /dev/null +++ b/jjwt/src/main/resources/templates/expired-jwt.html @@ -0,0 +1,17 @@ + + + + + +
+
+
+

JWT CSRF Token expired

+

+ + Back +
+
+
+ + \ No newline at end of file diff --git a/jjwt/src/main/resources/templates/fragments/head.html b/jjwt/src/main/resources/templates/fragments/head.html new file mode 100644 index 0000000000..2d5f54e5a0 --- /dev/null +++ b/jjwt/src/main/resources/templates/fragments/head.html @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + +

Nothing to see here, move along.

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

You made it!

+
+

BLARG

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

+

+ +
+
+
+
+ + \ No newline at end of file diff --git a/jjwt/src/test/java/io/jsonwebtoken/jjwtfun/DemoApplicationTests.java b/jjwt/src/test/java/io/jsonwebtoken/jjwtfun/DemoApplicationTests.java new file mode 100644 index 0000000000..82138ea23e --- /dev/null +++ b/jjwt/src/test/java/io/jsonwebtoken/jjwtfun/DemoApplicationTests.java @@ -0,0 +1,18 @@ +package io.jsonwebtoken.jjwtfun; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; + +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = JJWTFunApplication.class) +@WebAppConfiguration +public class DemoApplicationTests { + + @Test + public void contextLoads() { + } + +} diff --git a/pom.xml b/pom.xml index 26ceec36d3..82cf85208c 100644 --- a/pom.xml +++ b/pom.xml @@ -26,6 +26,7 @@ httpclient jackson javaxval + jjwt jooq-spring json-path mockito