Added JWT CSRF expired handling.

This commit is contained in:
Micah Silverman 2016-06-27 18:51:07 -04:00
parent f1630a0199
commit 38e829ef35
8 changed files with 91 additions and 16 deletions

View File

@ -1,7 +1,5 @@
package io.jsonwebtoken.jjwtfun.config; package io.jsonwebtoken.jjwtfun.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
@ -11,9 +9,6 @@ import org.springframework.security.web.csrf.CsrfTokenRepository;
@Configuration @Configuration
public class CSRFConfig { public class CSRFConfig {
private static final Logger log = LoggerFactory.getLogger(CSRFConfig.class);
@Value("#{ @environment['jjwtfun.secret'] ?: 'secret' }") @Value("#{ @environment['jjwtfun.secret'] ?: 'secret' }")
String secret; String secret;

View File

@ -17,9 +17,9 @@ import java.util.UUID;
public class JWTCsrfTokenRepository implements CsrfTokenRepository { public class JWTCsrfTokenRepository implements CsrfTokenRepository {
private static final Logger log = LoggerFactory.getLogger(JWTCsrfTokenRepository.class);
private static final String DEFAULT_CSRF_TOKEN_ATTR_NAME = CSRFConfig.class.getName().concat(".CSRF_TOKEN"); private static final String DEFAULT_CSRF_TOKEN_ATTR_NAME = CSRFConfig.class.getName().concat(".CSRF_TOKEN");
private static final Logger log = LoggerFactory.getLogger(JWTCsrfTokenRepository.class);
private String secret; private String secret;
public JWTCsrfTokenRepository(String secret) { public JWTCsrfTokenRepository(String secret) {
@ -32,7 +32,7 @@ public class JWTCsrfTokenRepository implements CsrfTokenRepository {
String id = UUID.randomUUID().toString().replace("-", ""); String id = UUID.randomUUID().toString().replace("-", "");
Date now = new Date(); Date now = new Date();
Date exp = new Date(System.currentTimeMillis() + (1000*60)); // 1 minute Date exp = new Date(System.currentTimeMillis() + (1000*30)); // 30 seconds
String token; String token;
try { try {
@ -68,7 +68,7 @@ public class JWTCsrfTokenRepository implements CsrfTokenRepository {
@Override @Override
public CsrfToken loadToken(HttpServletRequest request) { public CsrfToken loadToken(HttpServletRequest request) {
HttpSession session = request.getSession(false); HttpSession session = request.getSession(false);
if (session == null) { if (session == null || "GET".equals(request.getMethod())) {
return null; return null;
} }
return (CsrfToken) session.getAttribute(DEFAULT_CSRF_TOKEN_ATTR_NAME); return (CsrfToken) session.getAttribute(DEFAULT_CSRF_TOKEN_ATTR_NAME);

View File

@ -1,23 +1,72 @@
package io.jsonwebtoken.jjwtfun.config; package io.jsonwebtoken.jjwtfun.config;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 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.security.web.csrf.CsrfTokenRepository;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Configuration @Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter { public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Value("#{ @environment['jjwtfun.secret'] ?: 'secret' }")
String secret;
@Autowired @Autowired
CsrfTokenRepository jwtCsrfTokenRepository; CsrfTokenRepository jwtCsrfTokenRepository;
@Override @Override
protected void configure(HttpSecurity http) throws Exception { protected void configure(HttpSecurity http) throws Exception {
http http
.csrf().csrfTokenRepository(jwtCsrfTokenRepository).and() .addFilterAfter(new JwtCsrfValidatorFilter(), CsrfFilter.class)
.authorizeRequests() .csrf()
.antMatchers("/**") .csrfTokenRepository(jwtCsrfTokenRepository)
.permitAll(); .ignoringAntMatchers("/dynamic-builder-general")
.ignoringAntMatchers("/dynamic-builder-specific")
.and().authorizeRequests()
.antMatchers("/**")
.permitAll();
}
private class JwtCsrfValidatorFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// NOTE: A real implementation should have a nonce cache so the token cannot be reused
CsrfToken token = (CsrfToken) request.getAttribute("_csrf");
// CsrfFilter already made sure the token matched.
// Here, we'll make sure it's not expired
if ("POST".equals(request.getMethod()) && token != null) {
try {
Jwts.parser()
.setSigningKey(secret.getBytes("UTF-8"))
.parseClaimsJws(token.getToken());
} catch (JwtException e) {
// most likely an ExpiredJwtException, but this will handle any
request.setAttribute("exception", e);
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
RequestDispatcher dispatcher = request.getRequestDispatcher("expired-jwt");
dispatcher.forward(request, response);
}
}
filterChain.doFilter(request, response);
}
} }
} }

View File

@ -18,9 +18,12 @@ public class FormController {
@RequestMapping(value = "/jwt-csrf-form", method = POST) @RequestMapping(value = "/jwt-csrf-form", method = POST)
public String csrfFormPost(@RequestParam(name = "_csrf") String csrf, Model model) { public String csrfFormPost(@RequestParam(name = "_csrf") String csrf, Model model) {
model.addAttribute("csrf", csrf); model.addAttribute("csrf", csrf);
return "jwt-csrf-form-result"; return "jwt-csrf-form-result";
} }
@RequestMapping("/expired-jwt")
public String expiredJwt() {
return "expired-jwt";
}
} }

View File

@ -0,0 +1,17 @@
<html xmlns:th="http://www.thymeleaf.org">
<head>
<!--/*/ <th:block th:include="fragments/head :: head"/> /*/-->
</head>
<body>
<div class="container-fluid">
<div class="row">
<div class="box col-md-6 col-md-offset-3">
<h1>JWT CSRF Token expired</h1>
<h3 th:text="${exception.message}"></h3>
<a href="/jwt-csrf-form" class="btn btn-primary">Back</a>
</div>
</div>
</div>
</body>
</html>

View File

@ -8,6 +8,17 @@
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300italic,300,400italic,400,600italic,600,700italic,700,800italic,800" rel="stylesheet" type="text/css"/> <link href="https://fonts.googleapis.com/css?family=Open+Sans:300italic,300,400italic,400,600italic,600,700italic,700,800italic,800" rel="stylesheet" type="text/css"/>
<link href="https://netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css" rel="stylesheet" type="text/css"/> <link href="https://netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css" rel="stylesheet" type="text/css"/>
<style>
body {
margin-top: 60px;
}
.box {
padding: 50px;
text-align: center;
vertical-align: middle;
}
</style>
<!--[if lt IE 9]> <!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.2/html5shiv.js"></script> <script src="https://oss.maxcdn.com/libs/html5shiv/3.7.2/html5shiv.js"></script>
<script src="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script> <script src="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script>

View File

@ -6,7 +6,7 @@
<body> <body>
<div class="container-fluid"> <div class="container-fluid">
<div class="row"> <div class="row">
<div class="box col-md-6 col-md-offset-1"> <div class="box col-md-6 col-md-offset-3">
<h1>You made it!</h1> <h1>You made it!</h1>
<div style="overflow: scroll"> <div style="overflow: scroll">
<h4 th:text="${csrf}">BLARG</h4> <h4 th:text="${csrf}">BLARG</h4>

View File

@ -6,7 +6,7 @@
<body> <body>
<div class="container-fluid"> <div class="container-fluid">
<div class="row"> <div class="row">
<div class="box col-md-6 col-md-offset-1"> <div class="box col-md-6 col-md-offset-3">
<p/> <p/>
<form method="post" th:action="@{/jwt-csrf-form}"> <form method="post" th:action="@{/jwt-csrf-form}">
<input type="submit" class="btn btn-primary" value="Click Me!"/> <input type="submit" class="btn btn-primary" value="Click Me!"/>