├── .gitignore ├── LICENSE ├── README.md ├── etc └── blog.md ├── mvnw ├── mvnw.cmd ├── pom.xml └── src ├── main ├── java │ └── com │ │ └── svlada │ │ ├── CustomCorsFilter.java │ │ ├── SpringbootSecurityJwtApplication.java │ │ ├── common │ │ ├── ErrorCode.java │ │ ├── ErrorResponse.java │ │ ├── InMemoryDatabaseConfig.java │ │ └── WebUtil.java │ │ ├── entity │ │ ├── Role.java │ │ ├── User.java │ │ └── UserRole.java │ │ ├── profile │ │ └── endpoint │ │ │ └── ProfileEndpoint.java │ │ ├── security │ │ ├── RestAuthenticationEntryPoint.java │ │ ├── UserService.java │ │ ├── auth │ │ │ ├── JwtAuthenticationToken.java │ │ │ ├── ajax │ │ │ │ ├── AjaxAuthenticationProvider.java │ │ │ │ ├── AjaxAwareAuthenticationFailureHandler.java │ │ │ │ ├── AjaxAwareAuthenticationSuccessHandler.java │ │ │ │ ├── AjaxLoginProcessingFilter.java │ │ │ │ └── LoginRequest.java │ │ │ └── jwt │ │ │ │ ├── JwtAuthenticationProvider.java │ │ │ │ ├── JwtTokenAuthenticationProcessingFilter.java │ │ │ │ ├── SkipPathRequestMatcher.java │ │ │ │ ├── extractor │ │ │ │ ├── JwtHeaderTokenExtractor.java │ │ │ │ └── TokenExtractor.java │ │ │ │ └── verifier │ │ │ │ ├── BloomFilterTokenVerifier.java │ │ │ │ └── TokenVerifier.java │ │ ├── config │ │ │ ├── JwtSettings.java │ │ │ ├── PasswordEncoderConfig.java │ │ │ └── WebSecurityConfig.java │ │ ├── endpoint │ │ │ └── RefreshTokenEndpoint.java │ │ ├── exceptions │ │ │ ├── AuthMethodNotSupportedException.java │ │ │ ├── InvalidJwtToken.java │ │ │ └── JwtExpiredTokenException.java │ │ └── model │ │ │ ├── Scopes.java │ │ │ ├── UserContext.java │ │ │ └── token │ │ │ ├── AccessJwtToken.java │ │ │ ├── JwtToken.java │ │ │ ├── JwtTokenFactory.java │ │ │ ├── RawAccessJwtToken.java │ │ │ └── RefreshToken.java │ │ └── user │ │ ├── repository │ │ └── UserRepository.java │ │ └── service │ │ └── DatabaseUserService.java └── resources │ ├── application.yml │ ├── data.sql │ └── logback.xml └── test └── java └── com └── svlada └── SpringbootSecurityJwtApplicationTests.java /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /.mvn 3 | /.settings 4 | /.project 5 | /*.classpath 6 | /.apt_generated/ 7 | /.factorypath 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Vladimir Stanković 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # springboot-security-jwt 2 | 3 | This repository is created as an example for post I wrote on my blog: [JWT Authentication Tutorial: An example using Spring Boot](http://svlada.com/jwt-token-authentication-with-spring-boot/) 4 | -------------------------------------------------------------------------------- /etc/blog.md: -------------------------------------------------------------------------------- 1 | ## Table of contents: 2 | 1. Introduction 3 | 2. PRE-requisites 4 | 3. Ajax authentication 5 | 4. JWT Authentication 6 | 7 | ### Introduction 8 | 9 | This article will guide you on how you can implement JWT authentication with Spring Boot. 10 | 11 | We will cover the following two scenarios: 12 | 13 | 1. Ajax Authentication 14 | 2. JWT Token Authentication 15 | 16 | ### PRE-requisites 17 | 18 | Please check out the sample code/project from the following GitHub repository: https://github.com/svlada/springboot-security-jwt before going further reading the article. 19 | 20 | This project is using H2 in-memory database to store sample user information. To make things easier I have created data fixtures and configured Spring Boot to automatically load them on the application startup (```/jwt-demo/src/main/resources/data.sql```). 21 | 22 | Overall project structure is shown below: 23 | 24 | ``` 25 | +---main 26 | | +---java 27 | | | \---com 28 | | | \---svlada 29 | | | +---common 30 | | | +---entity 31 | | | +---profile 32 | | | | \---endpoint 33 | | | +---security 34 | | | | +---auth 35 | | | | | +---ajax 36 | | | | | \---jwt 37 | | | | | +---extractor 38 | | | | | \---verifier 39 | | | | +---config 40 | | | | +---endpoint 41 | | | | +---exceptions 42 | | | | \---model 43 | | | | \---token 44 | | | \---user 45 | | | +---repository 46 | | | \---service 47 | | \---resources 48 | | +---static 49 | | \---templates 50 | ``` 51 | 52 | ### Ajax authentication 53 | 54 | When we talk about Ajax authentication we usually refer to process where user is supplying credentials through JSON payload that is sent as a part of XMLHttpRequest. 55 | 56 | In the first part of this tutorial Ajax authentication is implemented by following standard patterns found in the Spring Security framework. 57 | 58 | Following is the list of components that we'll implement: 59 | 60 | 1. ```AjaxLoginProcessingFilter``` 61 | 2. ```AjaxAuthenticationProvider``` 62 | 3. ```AjaxAwareAuthenticationSuccessHandler``` 63 | 4. ```AjaxAwareAuthenticationFailureHandler``` 64 | 5. ```RestAuthenticationEntryPoint``` 65 | 6. ```WebSecurityConfig``` 66 | 67 | Before we get to the details of the implementation, let's look at the request/response authentication flow. 68 | 69 | **Ajax authentication request example** 70 | 71 | The Authentication API allows user to pass in credentials in order to receive authentication token. 72 | 73 | In our example, client initiates authentication process by invoking Authentication API endpoint (```/api/auth/login```). 74 | 75 | Raw HTTP request: 76 | 77 | ``` 78 | POST /api/auth/login HTTP/1.1 79 | Host: localhost:9966 80 | X-Requested-With: XMLHttpRequest 81 | Content-Type: application/json 82 | Cache-Control: no-cache 83 | 84 | { 85 | "username": "svlada@gmail.com", 86 | "password": "test1234" 87 | } 88 | ``` 89 | 90 | CURL: 91 | 92 | ``` 93 | curl -X POST -H "X-Requested-With: XMLHttpRequest" -H "Content-Type: application/json" -H "Cache-Control: no-cache" -d '{ 94 | "username": "svlada@gmail.com", 95 | "password": "test1234" 96 | }' "http://localhost:9966/api/auth/login" 97 | ``` 98 | 99 | **Ajax authentication response example** 100 | 101 | If client supplied credentials are valid, Authentication API will respond with the HTTP response including the following details: 102 | 103 | 1. HTTP status "200 OK" 104 | 2. Signed JWT Access and Refresh tokens are included in the response body 105 | 106 | **JWT Access token** - used to authenticate against protected API resources. It must be set in ```X-Authorization``` header. 107 | 108 | **JWT Refresh token** - used to acquire new Access Token. Token refresh is handled by the following API endpoint: ```/api/auth/token```. 109 | 110 | Raw HTTP Response: 111 | 112 | ``` 113 | { 114 | "token": "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJzdmxhZGFAZ21haWwuY29tIiwic2NvcGVzIjpbIlJPTEVfQURNSU4iLCJST0xFX1BSRU1JVU1fTUVNQkVSIl0sImlzcyI6Imh0dHA6Ly9zdmxhZGEuY29tIiwiaWF0IjoxNDcyMDMzMzA4LCJleHAiOjE0NzIwMzQyMDh9.41rxtplFRw55ffqcw1Fhy2pnxggssdWUU8CDOherC0Kw4sgt3-rw_mPSWSgQgsR0NLndFcMPh7LSQt5mkYqROQ", 115 | 116 | "refreshToken": "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJzdmxhZGFAZ21haWwuY29tIiwic2NvcGVzIjpbIlJPTEVfUkVGUkVTSF9UT0tFTiJdLCJpc3MiOiJodHRwOi8vc3ZsYWRhLmNvbSIsImp0aSI6IjkwYWZlNzhjLTFkMmUtNDg2OS1hNzdlLTFkNzU0YjYwZTBjZSIsImlhdCI6MTQ3MjAzMzMwOCwiZXhwIjoxNDcyMDM2OTA4fQ.SEEG60YRznBB2O7Gn_5X6YbRmyB3ml4hnpSOxqkwQUFtqA6MZo7_n2Am2QhTJBJA1Ygv74F2IxiLv0urxGLQjg" 117 | } 118 | ``` 119 | 120 | **JWT Access Token** 121 | 122 | JWT Access token can be used for authentication and authorization: 123 | 124 | 1. Authentication is performed by verifying JWT Access Token signature. If signature proves to be valid, access to requested API resource is granted. 125 | 2. Authorization is done by looking up privileges in the **scope** attribute of JWT Access token. 126 | 127 | Decoded JWT Access token has three parts: Header, Claims and Signature as shown below: 128 | 129 | Header 130 | ``` 131 | 132 | { 133 | "alg": "HS512" 134 | } 135 | ``` 136 | 137 | Claims 138 | ``` 139 | { 140 | "sub": "svlada@gmail.com", 141 | "scopes": [ 142 | "ROLE_ADMIN", 143 | "ROLE_PREMIUM_MEMBER" 144 | ], 145 | "iss": "http://svlada.com", 146 | "iat": 1472033308, 147 | "exp": 1472034208 148 | } 149 | ``` 150 | 151 | Signature (base64 encoded) 152 | ``` 153 | 41rxtplFRw55ffqcw1Fhy2pnxggssdWUU8CDOherC0Kw4sgt3-rw_mPSWSgQgsR0NLndFcMPh7LSQt5mkYqROQ 154 | ``` 155 | 156 | **JWT Refresh Token** 157 | 158 | Refresh token is long-lived token used to request new Access tokens. It's expiration time is greater than expiration time of Access token. 159 | 160 | In this tutorial we'll use ```jti``` claim to maintain list of blacklisted or revoked tokens. JWT ID(```jti```) claim is defined by [RFC7519](https://tools.ietf.org/html/rfc7519#section-4.1.7) with purpose to uniquely identify individual Refresh token. 161 | 162 | Decoded Refresh token has three parts: Header, Claims and Signature as shown below: 163 | 164 | Header 165 | ``` 166 | { 167 | "alg": "HS512" 168 | } 169 | ``` 170 | 171 | Claims 172 | ``` 173 | { 174 | "sub": "svlada@gmail.com", 175 | "scopes": [ 176 | "ROLE_REFRESH_TOKEN" 177 | ], 178 | "iss": "http://svlada.com", 179 | "jti": "90afe78c-1d2e-4869-a77e-1d754b60e0ce", 180 | "iat": 1472033308, 181 | "exp": 1472036908 182 | } 183 | ``` 184 | 185 | Signature (base64 encoded) 186 | ``` 187 | SEEG60YRznBB2O7Gn_5X6YbRmyB3ml4hnpSOxqkwQUFtqA6MZo7_n2Am2QhTJBJA1Ygv74F2IxiLv0urxGLQjg 188 | ``` 189 | 190 | #### AjaxLoginProcessingFilter 191 | 192 | First step is to extend ```AbstractAuthenticationProcessingFilter``` in order to provide custom processing of Ajax authentication requests. 193 | 194 | De-serialization and basic validation of the incoming JSON payload is done in the ```AjaxLoginProcessingFilter#attemptAuthentication``` method. Upon successful validation of the JSON payload authentication logic is delegated to AjaxAuthenticationProvider class. 195 | 196 | In case of a successful authentication ```AjaxLoginProcessingFilter#successfulAuthentication``` method is invoked. 197 | In case of failure authentication ```AjaxLoginProcessingFilter#unsuccessfulAuthentication``` method is invoked. 198 | 199 | ```language-java 200 | public class AjaxLoginProcessingFilter extends AbstractAuthenticationProcessingFilter { 201 | private static Logger logger = LoggerFactory.getLogger(AjaxLoginProcessingFilter.class); 202 | 203 | private final AuthenticationSuccessHandler successHandler; 204 | private final AuthenticationFailureHandler failureHandler; 205 | 206 | private final ObjectMapper objectMapper; 207 | 208 | public AjaxLoginProcessingFilter(String defaultProcessUrl, AuthenticationSuccessHandler successHandler, 209 | AuthenticationFailureHandler failureHandler, ObjectMapper mapper) { 210 | super(defaultProcessUrl); 211 | this.successHandler = successHandler; 212 | this.failureHandler = failureHandler; 213 | this.objectMapper = mapper; 214 | } 215 | 216 | @Override 217 | public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) 218 | throws AuthenticationException, IOException, ServletException { 219 | if (!HttpMethod.POST.name().equals(request.getMethod()) || !WebUtil.isAjax(request)) { 220 | if(logger.isDebugEnabled()) { 221 | logger.debug("Authentication method not supported. Request method: " + request.getMethod()); 222 | } 223 | throw new AuthMethodNotSupportedException("Authentication method not supported"); 224 | } 225 | 226 | LoginRequest loginRequest = objectMapper.readValue(request.getReader(), LoginRequest.class); 227 | 228 | if (StringUtils.isBlank(loginRequest.getUsername()) || StringUtils.isBlank(loginRequest.getPassword())) { 229 | throw new AuthenticationServiceException("Username or Password not provided"); 230 | } 231 | 232 | UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()); 233 | 234 | return this.getAuthenticationManager().authenticate(token); 235 | } 236 | 237 | @Override 238 | protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, 239 | Authentication authResult) throws IOException, ServletException { 240 | successHandler.onAuthenticationSuccess(request, response, authResult); 241 | } 242 | 243 | @Override 244 | protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, 245 | AuthenticationException failed) throws IOException, ServletException { 246 | SecurityContextHolder.clearContext(); 247 | failureHandler.onAuthenticationFailure(request, response, failed); 248 | } 249 | } 250 | ``` 251 | 252 | #### AjaxAuthenticationProvider 253 | 254 | Responsibility of the AjaxAuthenticationProvider class is to: 255 | 256 | 1. Verify user credentials against database, LDAP or some other system which holds the user data 257 | 2. If ```username``` and ```password``` do not match the record in the database authentication exception is thrown 258 | 3. Create UserContext and populate it with user data you need (in our case just ```username``` and ```user privileges```) 259 | 4. Upon successful authentication delegate creation of JWT Token to ```AjaxAwareAuthenticationSuccessHandler``` 260 | 261 | ```language-java 262 | @Component 263 | public class AjaxAuthenticationProvider implements AuthenticationProvider { 264 | private final BCryptPasswordEncoder encoder; 265 | private final DatabaseUserService userService; 266 | 267 | @Autowired 268 | public AjaxAuthenticationProvider(final DatabaseUserService userService, final BCryptPasswordEncoder encoder) { 269 | this.userService = userService; 270 | this.encoder = encoder; 271 | } 272 | 273 | @Override 274 | public Authentication authenticate(Authentication authentication) throws AuthenticationException { 275 | Assert.notNull(authentication, "No authentication data provided"); 276 | 277 | String username = (String) authentication.getPrincipal(); 278 | String password = (String) authentication.getCredentials(); 279 | 280 | User user = userService.getByUsername(username).orElseThrow(() -> new UsernameNotFoundException("User not found: " + username)); 281 | 282 | if (!encoder.matches(password, user.getPassword())) { 283 | throw new BadCredentialsException("Authentication Failed. Username or Password not valid."); 284 | } 285 | 286 | if (user.getRoles() == null) throw new InsufficientAuthenticationException("User has no roles assigned"); 287 | 288 | List authorities = user.getRoles().stream() 289 | .map(authority -> new SimpleGrantedAuthority(authority.getRole().authority())) 290 | .collect(Collectors.toList()); 291 | 292 | UserContext userContext = UserContext.create(user.getUsername(), authorities); 293 | 294 | return new UsernamePasswordAuthenticationToken(userContext, null, userContext.getAuthorities()); 295 | } 296 | 297 | @Override 298 | public boolean supports(Class authentication) { 299 | return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication)); 300 | } 301 | } 302 | ``` 303 | 304 | #### AjaxAwareAuthenticationSuccessHandler 305 | 306 | We'll implement AuthenticationSuccessHandler interface that is called when client has been successfully authenticated. 307 | 308 | AjaxAwareAuthenticationSuccessHandler class is our custom implementation of AuthenticationSuccessHandler interface. Responsibility of this class is to add JSON payload containing JWT Access and Refresh tokens into the HTTP response body. 309 | 310 | ```language-java 311 | @Component 312 | public class AjaxAwareAuthenticationSuccessHandler implements AuthenticationSuccessHandler { 313 | private final ObjectMapper mapper; 314 | private final JwtTokenFactory tokenFactory; 315 | 316 | @Autowired 317 | public AjaxAwareAuthenticationSuccessHandler(final ObjectMapper mapper, final JwtTokenFactory tokenFactory) { 318 | this.mapper = mapper; 319 | this.tokenFactory = tokenFactory; 320 | } 321 | 322 | @Override 323 | public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, 324 | Authentication authentication) throws IOException, ServletException { 325 | UserContext userContext = (UserContext) authentication.getPrincipal(); 326 | 327 | JwtToken accessToken = tokenFactory.createAccessJwtToken(userContext); 328 | JwtToken refreshToken = tokenFactory.createRefreshToken(userContext); 329 | 330 | Map tokenMap = new HashMap(); 331 | tokenMap.put("token", accessToken.getToken()); 332 | tokenMap.put("refreshToken", refreshToken.getToken()); 333 | 334 | response.setStatus(HttpStatus.OK.value()); 335 | response.setContentType(MediaType.APPLICATION_JSON_VALUE); 336 | mapper.writeValue(response.getWriter(), tokenMap); 337 | 338 | clearAuthenticationAttributes(request); 339 | } 340 | 341 | /** 342 | * Removes temporary authentication-related data which may have been stored 343 | * in the session during the authentication process.. 344 | * 345 | */ 346 | protected final void clearAuthenticationAttributes(HttpServletRequest request) { 347 | HttpSession session = request.getSession(false); 348 | 349 | if (session == null) { 350 | return; 351 | } 352 | 353 | session.removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION); 354 | } 355 | } 356 | ``` 357 | 358 | Let's focus for a moment on how JWT Access token is created. In this tutorial we are using [Java JWT](https://github.com/jwtk/jjwt) library created by [Stormpath](https://stormpath.com/). 359 | 360 | Make sure that ```JJWT``` dependency is included in your ```pom.xml```. 361 | 362 | ```language-xml 363 | 364 | io.jsonwebtoken 365 | jjwt 366 | ${jjwt.version} 367 | 368 | ``` 369 | 370 | We have created factory class (```JwtTokenFactory```) to decouple token creation logic. 371 | 372 | Method ```JwtTokenFactory#createAccessJwtToken``` creates signed JWT Access token. 373 | 374 | Method ```JwtTokenFactory#createRefreshToken``` creates signed JWT Refresh token. 375 | 376 | 377 | ```language-java 378 | @Component 379 | public class JwtTokenFactory { 380 | private final JwtSettings settings; 381 | 382 | @Autowired 383 | public JwtTokenFactory(JwtSettings settings) { 384 | this.settings = settings; 385 | } 386 | 387 | /** 388 | * Factory method for issuing new JWT Tokens. 389 | * 390 | * @param username 391 | * @param roles 392 | * @return 393 | */ 394 | public AccessJwtToken createAccessJwtToken(UserContext userContext) { 395 | if (StringUtils.isBlank(userContext.getUsername())) 396 | throw new IllegalArgumentException("Cannot create JWT Token without username"); 397 | 398 | if (userContext.getAuthorities() == null || userContext.getAuthorities().isEmpty()) 399 | throw new IllegalArgumentException("User doesn't have any privileges"); 400 | 401 | Claims claims = Jwts.claims().setSubject(userContext.getUsername()); 402 | claims.put("scopes", userContext.getAuthorities().stream().map(s -> s.toString()).collect(Collectors.toList())); 403 | 404 | DateTime currentTime = new DateTime(); 405 | 406 | String token = Jwts.builder() 407 | .setClaims(claims) 408 | .setIssuer(settings.getTokenIssuer()) 409 | .setIssuedAt(currentTime.toDate()) 410 | .setExpiration(currentTime.plusMinutes(settings.getTokenExpirationTime()).toDate()) 411 | .signWith(SignatureAlgorithm.HS512, settings.getTokenSigningKey()) 412 | .compact(); 413 | 414 | return new AccessJwtToken(token, claims); 415 | } 416 | 417 | public JwtToken createRefreshToken(UserContext userContext) { 418 | if (StringUtils.isBlank(userContext.getUsername())) { 419 | throw new IllegalArgumentException("Cannot create JWT Token without username"); 420 | } 421 | 422 | DateTime currentTime = new DateTime(); 423 | 424 | Claims claims = Jwts.claims().setSubject(userContext.getUsername()); 425 | claims.put("scopes", Arrays.asList(Scopes.REFRESH_TOKEN.authority())); 426 | 427 | String token = Jwts.builder() 428 | .setClaims(claims) 429 | .setIssuer(settings.getTokenIssuer()) 430 | .setId(UUID.randomUUID().toString()) 431 | .setIssuedAt(currentTime.toDate()) 432 | .setExpiration(currentTime.plusMinutes(settings.getRefreshTokenExpTime()).toDate()) 433 | .signWith(SignatureAlgorithm.HS512, settings.getTokenSigningKey()) 434 | .compact(); 435 | 436 | return new AccessJwtToken(token, claims); 437 | } 438 | } 439 | ``` 440 | 441 | Please note that if you are instantiating Claims object outside of ```Jwts.builder()``` make sure to first invoke ```Jwts.builder()#setClaims(claims)```. Why? Well, if you don't do that, Jwts.builder will, by default, create empty Claims object. What that means? Well if you call ```Jwts.builder()#setClaims()``` after you have set subject with ```Jwts.builder()#setSubject()``` your subject will be lost. Simply new instance of Claims class will overwrite default one created by Jwts.builder(). 442 | 443 | #### AjaxAwareAuthenticationFailureHandler 444 | 445 | AjaxAwareAuthenticationFailureHandler is invoked by ```AjaxLoginProcessingFilter``` in case of authentication failures. You can design specific error messages based on exception type that have occurred during the authentication process. 446 | 447 | ```language-java 448 | @Component 449 | public class AjaxAwareAuthenticationFailureHandler implements AuthenticationFailureHandler { 450 | private final ObjectMapper mapper; 451 | 452 | @Autowired 453 | public AjaxAwareAuthenticationFailureHandler(ObjectMapper mapper) { 454 | this.mapper = mapper; 455 | } 456 | 457 | @Override 458 | public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, 459 | AuthenticationException e) throws IOException, ServletException { 460 | 461 | response.setStatus(HttpStatus.UNAUTHORIZED.value()); 462 | response.setContentType(MediaType.APPLICATION_JSON_VALUE); 463 | 464 | if (e instanceof BadCredentialsException) { 465 | mapper.writeValue(response.getWriter(), ErrorResponse.of("Invalid username or password", ErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)); 466 | } else if (e instanceof JwtExpiredTokenException) { 467 | mapper.writeValue(response.getWriter(), ErrorResponse.of("Token has expired", ErrorCode.JWT_TOKEN_EXPIRED, HttpStatus.UNAUTHORIZED)); 468 | } else if (e instanceof AuthMethodNotSupportedException) { 469 | mapper.writeValue(response.getWriter(), ErrorResponse.of(e.getMessage(), ErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)); 470 | } 471 | 472 | mapper.writeValue(response.getWriter(), ErrorResponse.of("Authentication failed", ErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)); 473 | } 474 | } 475 | 476 | ``` 477 | 478 | ### JWT Authentication 479 | 480 | Token based authentication schema's became immensely popular in recent times, as they provide important benefits when compared to sessions/cookies: 481 | 482 | 1. CORS 483 | 2. No need for CSRF protection 484 | 3. Better integration with mobile 485 | 4. Reduced load on authorization server 486 | 5. No need for distributed session store 487 | 488 | Some trade-offs have to be made with this approach: 489 | 490 | 1. More vulnerable to XSS attacks 491 | 2. Access token can contain outdated authorization claims (e.g when some of the user privileges are revoked) 492 | 3. Access tokens can grow in size in case of increased number of claims 493 | 4. File download API can be tricky to implement 494 | 5. True statelessness and revocation are mutually exclusive 495 | 496 | In this article we'll investigate how JWT's can used for token based authentication. 497 | 498 | JWT Authentication flow is very simple: 499 | 500 | 1. User obtains Refresh and Access tokens by providing credentials to the Authorization server 501 | 2. User sends Access token with each request to access protected API resource 502 | 3. Access token is signed and contains user identity (e.g. user id) and authorization claims. 503 | 504 | It's important to note that authorization claims will be included with the Access token. Why is this important? Well, let's say that authorization claims (e.g user privileges in the database) are changed during the life time of Access token. Those changes will not become effective until new Access token is issued. In most cases this is not big issue, because Access tokens are short-lived. Otherwise go with the opaque token pattern. 505 | 506 | Before we get to the details of the implementation, let's look the sample request to protected API resource. 507 | 508 | **Signed request to protected API resource** 509 | 510 | Following pattern should be used when sending access tokens: ``` Bearer ```. 511 | 512 | In our example for header name (``````) we are using ```X-Authorization```. 513 | 514 | Raw HTTP request: 515 | ``` 516 | GET /api/me HTTP/1.1 517 | Host: localhost:9966 518 | X-Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJzdmxhZGFAZ21haWwuY29tIiwic2NvcGVzIjpbIlJPTEVfQURNSU4iLCJST0xFX1BSRU1JVU1fTUVNQkVSIl0sImlzcyI6Imh0dHA6Ly9zdmxhZGEuY29tIiwiaWF0IjoxNDcyMzkwMDY1LCJleHAiOjE0NzIzOTA5NjV9.Y9BR7q3f1npsSEYubz-u8tQ8dDOdBcVPFN7AIfWwO37KyhRugVzEbWVPO1obQlHNJWA0Nx1KrEqHqMEjuNWo5w 519 | Cache-Control: no-cache 520 | ``` 521 | 522 | CURL: 523 | ``` 524 | curl -X GET -H "X-Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJzdmxhZGFAZ21haWwuY29tIiwic2NvcGVzIjpbIlJPTEVfQURNSU4iLCJST0xFX1BSRU1JVU1fTUVNQkVSIl0sImlzcyI6Imh0dHA6Ly9zdmxhZGEuY29tIiwiaWF0IjoxNDcyMzkwMDY1LCJleHAiOjE0NzIzOTA5NjV9.Y9BR7q3f1npsSEYubz-u8tQ8dDOdBcVPFN7AIfWwO37KyhRugVzEbWVPO1obQlHNJWA0Nx1KrEqHqMEjuNWo5w" -H "Cache-Control: no-cache" "http://localhost:9966/api/me" 525 | ``` 526 | 527 | Let's see the implementation details. Following are components we need to implement JWT Authentication: 528 | 529 | 1. JwtTokenAuthenticationProcessingFilter 530 | 2. JwtAuthenticationProvider 531 | 3. SkipPathRequestMatcher 532 | 4. JwtHeaderTokenExtractor 533 | 5. BloomFilterTokenVerifier 534 | 6. WebSecurityConfig 535 | 536 | #### JwtTokenAuthenticationProcessingFilter 537 | 538 | JwtTokenAuthenticationProcessingFilter filter is applied to each API (```/api/**```) with exception of the refresh token endpoint (```/api/auth/token```) and login endpoint (```/api/auth/login```). 539 | 540 | This filter has the following responsibilities: 541 | 542 | 1. Check for access token in ```X-Authorization``` header. If Access token is found in the header, delegate authentication to ```JwtAuthenticationProvider``` otherwise throw authentication exception 543 | 2. Invokes success or failure strategies based on the outcome of authentication process performed by ```JwtAuthenticationProvider``` 544 | 545 | Please ensure that ```chain.doFilter(request, response)``` is invoked upon successful authentication. You want processing of the request to advance to the next filter, because very last one filter ```FilterSecurityInterceptor#doFilter``` is responsible to actually invoke method in your controller that is handling requested API resource. 546 | 547 | ```language-java 548 | public class JwtTokenAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter { 549 | private final AuthenticationFailureHandler failureHandler; 550 | private final TokenExtractor tokenExtractor; 551 | 552 | @Autowired 553 | public JwtTokenAuthenticationProcessingFilter(AuthenticationFailureHandler failureHandler, 554 | TokenExtractor tokenExtractor, RequestMatcher matcher) { 555 | super(matcher); 556 | this.failureHandler = failureHandler; 557 | this.tokenExtractor = tokenExtractor; 558 | } 559 | 560 | @Override 561 | public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) 562 | throws AuthenticationException, IOException, ServletException { 563 | String tokenPayload = request.getHeader(WebSecurityConfig.JWT_TOKEN_HEADER_PARAM); 564 | RawAccessJwtToken token = new RawAccessJwtToken(tokenExtractor.extract(tokenPayload)); 565 | return getAuthenticationManager().authenticate(new JwtAuthenticationToken(token)); 566 | } 567 | 568 | @Override 569 | protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, 570 | Authentication authResult) throws IOException, ServletException { 571 | SecurityContext context = SecurityContextHolder.createEmptyContext(); 572 | context.setAuthentication(authResult); 573 | SecurityContextHolder.setContext(context); 574 | chain.doFilter(request, response); 575 | } 576 | 577 | @Override 578 | protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, 579 | AuthenticationException failed) throws IOException, ServletException { 580 | SecurityContextHolder.clearContext(); 581 | failureHandler.onAuthenticationFailure(request, response, failed); 582 | } 583 | } 584 | ``` 585 | 586 | #### JwtHeaderTokenExtractor 587 | 588 | JwtHeaderTokenExtractor is very simple class used to extract Authorization token from header. You can extend TokenExtractor interface and provide your custom implementation that will for example extract token from URL. 589 | 590 | ```language-java 591 | @Component 592 | public class JwtHeaderTokenExtractor implements TokenExtractor { 593 | public static String HEADER_PREFIX = "Bearer "; 594 | 595 | @Override 596 | public String extract(String header) { 597 | if (StringUtils.isBlank(header)) { 598 | throw new AuthenticationServiceException("Authorization header cannot be blank!"); 599 | } 600 | 601 | if (header.length() < HEADER_PREFIX.length()) { 602 | throw new AuthenticationServiceException("Invalid authorization header size."); 603 | } 604 | 605 | return header.substring(HEADER_PREFIX.length(), header.length()); 606 | } 607 | } 608 | ``` 609 | 610 | #### JwtAuthenticationProvider 611 | 612 | JwtAuthenticationProvider has the following responsibilities: 613 | 614 | 1. Verify the access token's signature 615 | 2. Extract identity and authorization claims from Access token and use them to create UserContext 616 | 3. If Access token is malformed, expired or simply if token is not signed with the appropriate signing key Authentication exception will be thrown 617 | 618 | ```language-java 619 | @Component 620 | public class JwtAuthenticationProvider implements AuthenticationProvider { 621 | private final JwtSettings jwtSettings; 622 | 623 | @Autowired 624 | public JwtAuthenticationProvider(JwtSettings jwtSettings) { 625 | this.jwtSettings = jwtSettings; 626 | } 627 | 628 | @Override 629 | public Authentication authenticate(Authentication authentication) throws AuthenticationException { 630 | RawAccessJwtToken rawAccessToken = (RawAccessJwtToken) authentication.getCredentials(); 631 | 632 | Jws jwsClaims = rawAccessToken.parseClaims(jwtSettings.getTokenSigningKey()); 633 | String subject = jwsClaims.getBody().getSubject(); 634 | List scopes = jwsClaims.getBody().get("scopes", List.class); 635 | List authorities = scopes.stream() 636 | .map(authority -> new SimpleGrantedAuthority(authority)) 637 | .collect(Collectors.toList()); 638 | 639 | UserContext context = UserContext.create(subject, authorities); 640 | 641 | return new JwtAuthenticationToken(context, context.getAuthorities()); 642 | } 643 | 644 | @Override 645 | public boolean supports(Class authentication) { 646 | return (JwtAuthenticationToken.class.isAssignableFrom(authentication)); 647 | } 648 | } 649 | 650 | ``` 651 | 652 | #### SkipPathRequestMatcher 653 | 654 | JwtTokenAuthenticationProcessingFilter filter is configured to skip following endpoints: ```/api/auth/login``` and ```/api/auth/token```. This is achieved with ```SkipPathRequestMatcher``` implementation of ```RequestMatcher```. 655 | 656 | ```language-java 657 | public class SkipPathRequestMatcher implements RequestMatcher { 658 | private OrRequestMatcher matchers; 659 | private RequestMatcher processingMatcher; 660 | 661 | public SkipPathRequestMatcher(List pathsToSkip, String processingPath) { 662 | Assert.notNull(pathsToSkip); 663 | List m = pathsToSkip.stream().map(path -> new AntPathRequestMatcher(path)).collect(Collectors.toList()); 664 | matchers = new OrRequestMatcher(m); 665 | processingMatcher = new AntPathRequestMatcher(processingPath); 666 | } 667 | 668 | @Override 669 | public boolean matches(HttpServletRequest request) { 670 | if (matchers.matches(request)) { 671 | return false; 672 | } 673 | return processingMatcher.matches(request) ? true : false; 674 | } 675 | } 676 | ``` 677 | 678 | #### WebSecurityConfig 679 | 680 | WebSecurityConfig class extends WebSecurityConfigurerAdapter to provide custom security configuration. 681 | 682 | Following beans are configured and instantiated in this class: 683 | 684 | 1. AjaxLoginProcessingFilter 685 | 2. JwtTokenAuthenticationProcessingFilter 686 | 3. AuthenticationManager 687 | 4. BCryptPasswordEncoder 688 | 689 | Also, inside ```WebSecurityConfig#configure(HttpSecurity http)``` method we'll configure patterns to define protected/unprotected API endpoints. Please note that we have disabled CSRF protection because we are not using Cookies. 690 | 691 | ```language-java 692 | @Configuration 693 | @EnableWebSecurity 694 | public class WebSecurityConfig extends WebSecurityConfigurerAdapter { 695 | public static final String JWT_TOKEN_HEADER_PARAM = "X-Authorization"; 696 | public static final String FORM_BASED_LOGIN_ENTRY_POINT = "/api/auth/login"; 697 | public static final String TOKEN_BASED_AUTH_ENTRY_POINT = "/api/**"; 698 | public static final String TOKEN_REFRESH_ENTRY_POINT = "/api/auth/token"; 699 | 700 | @Autowired private RestAuthenticationEntryPoint authenticationEntryPoint; 701 | @Autowired private AuthenticationSuccessHandler successHandler; 702 | @Autowired private AuthenticationFailureHandler failureHandler; 703 | @Autowired private AjaxAuthenticationProvider ajaxAuthenticationProvider; 704 | @Autowired private JwtAuthenticationProvider jwtAuthenticationProvider; 705 | 706 | @Autowired private TokenExtractor tokenExtractor; 707 | 708 | @Autowired private AuthenticationManager authenticationManager; 709 | 710 | @Autowired private ObjectMapper objectMapper; 711 | 712 | protected AjaxLoginProcessingFilter buildAjaxLoginProcessingFilter() throws Exception { 713 | AjaxLoginProcessingFilter filter = new AjaxLoginProcessingFilter(FORM_BASED_LOGIN_ENTRY_POINT, successHandler, failureHandler, objectMapper); 714 | filter.setAuthenticationManager(this.authenticationManager); 715 | return filter; 716 | } 717 | 718 | protected JwtTokenAuthenticationProcessingFilter buildJwtTokenAuthenticationProcessingFilter() throws Exception { 719 | List pathsToSkip = Arrays.asList(TOKEN_REFRESH_ENTRY_POINT, FORM_BASED_LOGIN_ENTRY_POINT); 720 | SkipPathRequestMatcher matcher = new SkipPathRequestMatcher(pathsToSkip, TOKEN_BASED_AUTH_ENTRY_POINT); 721 | JwtTokenAuthenticationProcessingFilter filter 722 | = new JwtTokenAuthenticationProcessingFilter(failureHandler, tokenExtractor, matcher); 723 | filter.setAuthenticationManager(this.authenticationManager); 724 | return filter; 725 | } 726 | 727 | @Bean 728 | @Override 729 | public AuthenticationManager authenticationManagerBean() throws Exception { 730 | return super.authenticationManagerBean(); 731 | } 732 | 733 | @Override 734 | protected void configure(AuthenticationManagerBuilder auth) { 735 | auth.authenticationProvider(ajaxAuthenticationProvider); 736 | auth.authenticationProvider(jwtAuthenticationProvider); 737 | } 738 | 739 | @Bean 740 | protected BCryptPasswordEncoder passwordEncoder() { 741 | return new BCryptPasswordEncoder(); 742 | } 743 | 744 | @Override 745 | protected void configure(HttpSecurity http) throws Exception { 746 | http 747 | .csrf().disable() // We don't need CSRF for JWT based authentication 748 | .exceptionHandling() 749 | .authenticationEntryPoint(this.authenticationEntryPoint) 750 | 751 | .and() 752 | .sessionManagement() 753 | .sessionCreationPolicy(SessionCreationPolicy.STATELESS) 754 | 755 | .and() 756 | .authorizeRequests() 757 | .antMatchers(FORM_BASED_LOGIN_ENTRY_POINT).permitAll() // Login end-point 758 | .antMatchers(TOKEN_REFRESH_ENTRY_POINT).permitAll() // Token refresh end-point 759 | .antMatchers("/console").permitAll() // H2 Console Dash-board - only for testing 760 | .and() 761 | .authorizeRequests() 762 | .antMatchers(TOKEN_BASED_AUTH_ENTRY_POINT).authenticated() // Protected API End-points 763 | .and() 764 | .addFilterBefore(buildAjaxLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class) 765 | .addFilterBefore(buildJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class); 766 | } 767 | } 768 | ``` 769 | 770 | #### PasswordEncoderConfig 771 | 772 | BCrypt encoder that is in AjaxAuthenticationProvider. 773 | 774 | ```language-java 775 | @Configuration 776 | public class PasswordEncoderConfig { 777 | @Bean 778 | protected BCryptPasswordEncoder passwordEncoder() { 779 | return new BCryptPasswordEncoder(); 780 | } 781 | } 782 | ``` 783 | 784 | #### BloomFilterTokenVerifier 785 | 786 | This is dummy class. You should ideally implement your own TokenVerifier to check for revoked tokens. 787 | 788 | ```language-java 789 | @Component 790 | public class BloomFilterTokenVerifier implements TokenVerifier { 791 | @Override 792 | public boolean verify(String jti) { 793 | return true; 794 | } 795 | } 796 | ``` 797 | 798 | ### Conclusion 799 | 800 | I heard people whispering on the web that loosing a JWT token is like loosing your house keys. So be careful. 801 | 802 | ## References 803 | 804 | ### [I don’t see the point in Revoking or Blacklisting JWT](https://www.dinochiesa.net/?p=1388) 805 | 806 | ### [Spring Security Architecture - Dave Syer](https://github.com/dsyer/spring-security-architecture) 807 | 808 | ### [Invalidating JWT](http://stackoverflow.com/questions/21978658/invalidating-json-web-tokens/36884683#36884683) 809 | 810 | ### [Secure and stateless JWT implementation](http://stackoverflow.com/questions/38557379/secure-and-stateless-jwt-implementation) 811 | 812 | ### [Learn JWT](https://github.com/dwyl/learn-json-web-tokens) 813 | 814 | ### [Opaque access tokens and cloud foundry](https://www.cloudfoundry.org/opaque-access-tokens-cloud-foundry/) 815 | 816 | ### [The unspoken vulnerability of JWTS](http://by.jtl.xyz/2016/06/the-unspoken-vulnerability-of-jwts.html) 817 | 818 | ### [How To Control User Identity Within Micro-services](http://nordicapis.com/how-to-control-user-identity-within-microservices/) 819 | 820 | ### [Why Does OAuth v2 Have Both Access and Refresh Tokens?](http://stackoverflow.com/questions/3487991/why-does-oauth-v2-have-both-access-and-refresh-tokens/12885823) 821 | 822 | ### [RFC-6749](https://tools.ietf.org/html/rfc6749) 823 | 824 | ### [Are breaches of JWT-based servers more damaging?](https://www.sslvpn.online/are-breaches-of-jwt-based-servers-more-damaging/) 825 | 826 | 827 | 828 | 829 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven2 Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /etc/mavenrc ] ; then 40 | . /etc/mavenrc 41 | fi 42 | 43 | if [ -f "$HOME/.mavenrc" ] ; then 44 | . "$HOME/.mavenrc" 45 | fi 46 | 47 | fi 48 | 49 | # OS specific support. $var _must_ be set to either true or false. 50 | cygwin=false; 51 | darwin=false; 52 | mingw=false 53 | case "`uname`" in 54 | CYGWIN*) cygwin=true ;; 55 | MINGW*) mingw=true;; 56 | Darwin*) darwin=true 57 | # 58 | # Look for the Apple JDKs first to preserve the existing behaviour, and then look 59 | # for the new JDKs provided by Oracle. 60 | # 61 | if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK ] ; then 62 | # 63 | # Apple JDKs 64 | # 65 | export JAVA_HOME=/System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK/Home 66 | fi 67 | 68 | if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Java/JavaVirtualMachines/CurrentJDK ] ; then 69 | # 70 | # Apple JDKs 71 | # 72 | export JAVA_HOME=/System/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home 73 | fi 74 | 75 | if [ -z "$JAVA_HOME" ] && [ -L "/Library/Java/JavaVirtualMachines/CurrentJDK" ] ; then 76 | # 77 | # Oracle JDKs 78 | # 79 | export JAVA_HOME=/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home 80 | fi 81 | 82 | if [ -z "$JAVA_HOME" ] && [ -x "/usr/libexec/java_home" ]; then 83 | # 84 | # Apple JDKs 85 | # 86 | export JAVA_HOME=`/usr/libexec/java_home` 87 | fi 88 | ;; 89 | esac 90 | 91 | if [ -z "$JAVA_HOME" ] ; then 92 | if [ -r /etc/gentoo-release ] ; then 93 | JAVA_HOME=`java-config --jre-home` 94 | fi 95 | fi 96 | 97 | if [ -z "$M2_HOME" ] ; then 98 | ## resolve links - $0 may be a link to maven's home 99 | PRG="$0" 100 | 101 | # need this for relative symlinks 102 | while [ -h "$PRG" ] ; do 103 | ls=`ls -ld "$PRG"` 104 | link=`expr "$ls" : '.*-> \(.*\)$'` 105 | if expr "$link" : '/.*' > /dev/null; then 106 | PRG="$link" 107 | else 108 | PRG="`dirname "$PRG"`/$link" 109 | fi 110 | done 111 | 112 | saveddir=`pwd` 113 | 114 | M2_HOME=`dirname "$PRG"`/.. 115 | 116 | # make it fully qualified 117 | M2_HOME=`cd "$M2_HOME" && pwd` 118 | 119 | cd "$saveddir" 120 | # echo Using m2 at $M2_HOME 121 | fi 122 | 123 | # For Cygwin, ensure paths are in UNIX format before anything is touched 124 | if $cygwin ; then 125 | [ -n "$M2_HOME" ] && 126 | M2_HOME=`cygpath --unix "$M2_HOME"` 127 | [ -n "$JAVA_HOME" ] && 128 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 129 | [ -n "$CLASSPATH" ] && 130 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 131 | fi 132 | 133 | # For Migwn, ensure paths are in UNIX format before anything is touched 134 | if $mingw ; then 135 | [ -n "$M2_HOME" ] && 136 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 137 | [ -n "$JAVA_HOME" ] && 138 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 139 | # TODO classpath? 140 | fi 141 | 142 | if [ -z "$JAVA_HOME" ]; then 143 | javaExecutable="`which javac`" 144 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 145 | # readlink(1) is not available as standard on Solaris 10. 146 | readLink=`which readlink` 147 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 148 | if $darwin ; then 149 | javaHome="`dirname \"$javaExecutable\"`" 150 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 151 | else 152 | javaExecutable="`readlink -f \"$javaExecutable\"`" 153 | fi 154 | javaHome="`dirname \"$javaExecutable\"`" 155 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 156 | JAVA_HOME="$javaHome" 157 | export JAVA_HOME 158 | fi 159 | fi 160 | fi 161 | 162 | if [ -z "$JAVACMD" ] ; then 163 | if [ -n "$JAVA_HOME" ] ; then 164 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 165 | # IBM's JDK on AIX uses strange locations for the executables 166 | JAVACMD="$JAVA_HOME/jre/sh/java" 167 | else 168 | JAVACMD="$JAVA_HOME/bin/java" 169 | fi 170 | else 171 | JAVACMD="`which java`" 172 | fi 173 | fi 174 | 175 | if [ ! -x "$JAVACMD" ] ; then 176 | echo "Error: JAVA_HOME is not defined correctly." >&2 177 | echo " We cannot execute $JAVACMD" >&2 178 | exit 1 179 | fi 180 | 181 | if [ -z "$JAVA_HOME" ] ; then 182 | echo "Warning: JAVA_HOME environment variable is not set." 183 | fi 184 | 185 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 186 | 187 | # For Cygwin, switch paths to Windows format before running java 188 | if $cygwin; then 189 | [ -n "$M2_HOME" ] && 190 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 191 | [ -n "$JAVA_HOME" ] && 192 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 193 | [ -n "$CLASSPATH" ] && 194 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 195 | fi 196 | 197 | # traverses directory structure from process work directory to filesystem root 198 | # first directory with .mvn subdirectory is considered project base directory 199 | find_maven_basedir() { 200 | local basedir=$(pwd) 201 | local wdir=$(pwd) 202 | while [ "$wdir" != '/' ] ; do 203 | if [ -d "$wdir"/.mvn ] ; then 204 | basedir=$wdir 205 | break 206 | fi 207 | wdir=$(cd "$wdir/.."; pwd) 208 | done 209 | echo "${basedir}" 210 | } 211 | 212 | # concatenates all lines of a file 213 | concat_lines() { 214 | if [ -f "$1" ]; then 215 | echo "$(tr -s '\n' ' ' < "$1")" 216 | fi 217 | } 218 | 219 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-$(find_maven_basedir)} 220 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 221 | 222 | # Provide a "standardized" way to retrieve the CLI args that will 223 | # work with both Windows and non-Windows executions. 224 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 225 | export MAVEN_CMD_LINE_ARGS 226 | 227 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 228 | 229 | exec "$JAVACMD" \ 230 | $MAVEN_OPTS \ 231 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 232 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 233 | ${WRAPPER_LAUNCHER} "$@" 234 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM http://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven2 Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' 39 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 40 | 41 | @REM set %HOME% to equivalent of $HOME 42 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 43 | 44 | @REM Execute a user defined script before this one 45 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 46 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 47 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 48 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 49 | :skipRcPre 50 | 51 | @setlocal 52 | 53 | set ERROR_CODE=0 54 | 55 | @REM To isolate internal variables from possible post scripts, we use another setlocal 56 | @setlocal 57 | 58 | @REM ==== START VALIDATION ==== 59 | if not "%JAVA_HOME%" == "" goto OkJHome 60 | 61 | echo. 62 | echo Error: JAVA_HOME not found in your environment. >&2 63 | echo Please set the JAVA_HOME variable in your environment to match the >&2 64 | echo location of your Java installation. >&2 65 | echo. 66 | goto error 67 | 68 | :OkJHome 69 | if exist "%JAVA_HOME%\bin\java.exe" goto init 70 | 71 | echo. 72 | echo Error: JAVA_HOME is set to an invalid directory. >&2 73 | echo JAVA_HOME = "%JAVA_HOME%" >&2 74 | echo Please set the JAVA_HOME variable in your environment to match the >&2 75 | echo location of your Java installation. >&2 76 | echo. 77 | goto error 78 | 79 | @REM ==== END VALIDATION ==== 80 | 81 | :init 82 | 83 | set MAVEN_CMD_LINE_ARGS=%* 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | 121 | set WRAPPER_JAR="".\.mvn\wrapper\maven-wrapper.jar"" 122 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 123 | 124 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CMD_LINE_ARGS% 125 | if ERRORLEVEL 1 goto error 126 | goto end 127 | 128 | :error 129 | set ERROR_CODE=1 130 | 131 | :end 132 | @endlocal & set ERROR_CODE=%ERROR_CODE% 133 | 134 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 135 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 136 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 137 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 138 | :skipRcPost 139 | 140 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 141 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 142 | 143 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 144 | 145 | exit /B %ERROR_CODE% -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.svlada 7 | jwt-demo 8 | 1 9 | jar 10 | 11 | springboot-security-jwt 12 | Secure your API with JWT Tokens 13 | 14 | 15 | org.springframework.boot 16 | spring-boot-starter-parent 17 | 1.4.0.RELEASE 18 | 19 | 20 | 21 | 22 | UTF-8 23 | UTF-8 24 | 1.8 25 | 0.6.0 26 | 27 | 28 | 29 | 30 | org.springframework.boot 31 | spring-boot-starter-security 32 | 33 | 34 | org.springframework.boot 35 | spring-boot-starter-web 36 | 37 | 38 | io.jsonwebtoken 39 | jjwt 40 | ${jjwt.version} 41 | 42 | 43 | org.apache.commons 44 | commons-lang3 45 | 3.3.2 46 | 47 | 48 | org.springframework.boot 49 | spring-boot-starter-data-jpa 50 | 51 | 52 | com.fasterxml.jackson.core 53 | jackson-databind 54 | 55 | 56 | com.fasterxml.jackson.core 57 | jackson-core 58 | 59 | 60 | com.fasterxml.jackson.core 61 | jackson-annotations 62 | 63 | 64 | 65 | org.springframework.boot 66 | spring-boot-starter-test 67 | test 68 | 69 | 70 | org.springframework.boot 71 | spring-boot-configuration-processor 72 | true 73 | 74 | 75 | com.h2database 76 | h2 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | org.springframework.boot 85 | spring-boot-maven-plugin 86 | 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /src/main/java/com/svlada/CustomCorsFilter.java: -------------------------------------------------------------------------------- 1 | package com.svlada; 2 | 3 | import java.util.Arrays; 4 | 5 | import org.springframework.web.cors.CorsConfiguration; 6 | import org.springframework.web.cors.UrlBasedCorsConfigurationSource; 7 | import org.springframework.web.filter.CorsFilter; 8 | 9 | /** 10 | * CustomCorsFilter 11 | * 12 | * @author vladimir.stankovic 13 | * 14 | * Aug 3, 2016 15 | */ 16 | public class CustomCorsFilter extends CorsFilter { 17 | 18 | public CustomCorsFilter() { 19 | super(configurationSource()); 20 | } 21 | 22 | private static UrlBasedCorsConfigurationSource configurationSource() { 23 | CorsConfiguration config = new CorsConfiguration(); 24 | config.setAllowCredentials(true); 25 | config.addAllowedOrigin("*"); 26 | config.addAllowedHeader("*"); 27 | config.setMaxAge(36000L); 28 | config.setAllowedMethods(Arrays.asList("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")); 29 | UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); 30 | source.registerCorsConfiguration("/api/**", config); 31 | return source; 32 | } 33 | } -------------------------------------------------------------------------------- /src/main/java/com/svlada/SpringbootSecurityJwtApplication.java: -------------------------------------------------------------------------------- 1 | package com.svlada; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 6 | 7 | /** 8 | * Sample application for demonstrating security with JWT Tokens 9 | * 10 | * @author vladimir.stankovic 11 | * 12 | * Aug 3, 2016 13 | */ 14 | @SpringBootApplication 15 | @EnableConfigurationProperties 16 | public class SpringbootSecurityJwtApplication { 17 | public static void main(String[] args) { 18 | SpringApplication.run(SpringbootSecurityJwtApplication.class, args); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/svlada/common/ErrorCode.java: -------------------------------------------------------------------------------- 1 | package com.svlada.common; 2 | 3 | import com.fasterxml.jackson.annotation.JsonValue; 4 | 5 | /** 6 | * Enumeration of REST Error types. 7 | * 8 | * @author vladimir.stankovic 9 | * 10 | * Aug 3, 2016 11 | */ 12 | public enum ErrorCode { 13 | GLOBAL(2), 14 | 15 | AUTHENTICATION(10), JWT_TOKEN_EXPIRED(11); 16 | 17 | private int errorCode; 18 | 19 | private ErrorCode(int errorCode) { 20 | this.errorCode = errorCode; 21 | } 22 | 23 | @JsonValue 24 | public int getErrorCode() { 25 | return errorCode; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/svlada/common/ErrorResponse.java: -------------------------------------------------------------------------------- 1 | package com.svlada.common; 2 | 3 | import java.util.Date; 4 | 5 | import org.springframework.http.HttpStatus; 6 | 7 | /** 8 | * Error model for interacting with client. 9 | * 10 | * @author vladimir.stankovic 11 | * 12 | * Aug 3, 2016 13 | */ 14 | public class ErrorResponse { 15 | // HTTP Response Status Code 16 | private final HttpStatus status; 17 | 18 | // General Error message 19 | private final String message; 20 | 21 | // Error code 22 | private final ErrorCode errorCode; 23 | 24 | private final Date timestamp; 25 | 26 | protected ErrorResponse(final String message, final ErrorCode errorCode, HttpStatus status) { 27 | this.message = message; 28 | this.errorCode = errorCode; 29 | this.status = status; 30 | this.timestamp = new java.util.Date(); 31 | } 32 | 33 | public static ErrorResponse of(final String message, final ErrorCode errorCode, HttpStatus status) { 34 | return new ErrorResponse(message, errorCode, status); 35 | } 36 | 37 | public Integer getStatus() { 38 | return status.value(); 39 | } 40 | 41 | public String getMessage() { 42 | return message; 43 | } 44 | 45 | public ErrorCode getErrorCode() { 46 | return errorCode; 47 | } 48 | 49 | public Date getTimestamp() { 50 | return timestamp; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/svlada/common/InMemoryDatabaseConfig.java: -------------------------------------------------------------------------------- 1 | package com.svlada.common; 2 | 3 | import org.h2.server.web.WebServlet; 4 | import org.springframework.boot.web.servlet.ServletRegistrationBean; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | @Configuration 9 | public class InMemoryDatabaseConfig { 10 | @Bean 11 | public ServletRegistrationBean h2servletRegistration() { 12 | ServletRegistrationBean registration = new ServletRegistrationBean(new WebServlet()); 13 | registration.addUrlMappings("/console/*"); 14 | return registration; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/svlada/common/WebUtil.java: -------------------------------------------------------------------------------- 1 | package com.svlada.common; 2 | 3 | import javax.servlet.http.HttpServletRequest; 4 | 5 | import org.springframework.security.web.savedrequest.SavedRequest; 6 | 7 | /** 8 | * 9 | * @author vladimir.stankovic 10 | * 11 | * Aug 3, 2016 12 | */ 13 | public class WebUtil { 14 | private static final String XML_HTTP_REQUEST = "XMLHttpRequest"; 15 | private static final String X_REQUESTED_WITH = "X-Requested-With"; 16 | 17 | private static final String CONTENT_TYPE = "Content-type"; 18 | private static final String CONTENT_TYPE_JSON = "application/json"; 19 | 20 | public static boolean isAjax(HttpServletRequest request) { 21 | return XML_HTTP_REQUEST.equals(request.getHeader(X_REQUESTED_WITH)); 22 | } 23 | 24 | public static boolean isAjax(SavedRequest request) { 25 | return request.getHeaderValues(X_REQUESTED_WITH).contains(XML_HTTP_REQUEST); 26 | } 27 | 28 | public static boolean isContentTypeJson(SavedRequest request) { 29 | return request.getHeaderValues(CONTENT_TYPE).contains(CONTENT_TYPE_JSON); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/svlada/entity/Role.java: -------------------------------------------------------------------------------- 1 | package com.svlada.entity; 2 | 3 | /** 4 | * Enumerated {@link User} roles. 5 | * 6 | * @author vladimir.stankovic 7 | * 8 | * Aug 16, 2016 9 | */ 10 | public enum Role { 11 | ADMIN, PREMIUM_MEMBER, MEMBER; 12 | 13 | public String authority() { 14 | return "ROLE_" + this.name(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/svlada/entity/User.java: -------------------------------------------------------------------------------- 1 | package com.svlada.entity; 2 | 3 | import java.util.List; 4 | 5 | import javax.persistence.Column; 6 | import javax.persistence.Entity; 7 | import javax.persistence.GeneratedValue; 8 | import javax.persistence.GenerationType; 9 | import javax.persistence.Id; 10 | import javax.persistence.JoinColumn; 11 | import javax.persistence.OneToMany; 12 | import javax.persistence.Table; 13 | 14 | @Entity 15 | @Table(name="APP_USER") 16 | public class User { 17 | @Id @Column(name="ID") 18 | @GeneratedValue(strategy = GenerationType.IDENTITY) 19 | private Long id; 20 | 21 | @Column(name="username") 22 | private String username; 23 | 24 | @Column(name="password") 25 | private String password; 26 | 27 | @OneToMany 28 | @JoinColumn(name="APP_USER_ID", referencedColumnName="ID") 29 | private List roles; 30 | 31 | public User() { } 32 | 33 | public User(Long id, String username, String password, List roles) { 34 | this.id = id; 35 | this.username = username; 36 | this.password = password; 37 | this.roles = roles; 38 | } 39 | 40 | public Long getId() { 41 | return id; 42 | } 43 | 44 | public String getUsername() { 45 | return username; 46 | } 47 | 48 | public String getPassword() { 49 | return password; 50 | } 51 | 52 | public List getRoles() { 53 | return roles; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/svlada/entity/UserRole.java: -------------------------------------------------------------------------------- 1 | package com.svlada.entity; 2 | 3 | import java.io.Serializable; 4 | 5 | import javax.persistence.Column; 6 | import javax.persistence.Embeddable; 7 | import javax.persistence.EmbeddedId; 8 | import javax.persistence.Entity; 9 | import javax.persistence.EnumType; 10 | import javax.persistence.Enumerated; 11 | import javax.persistence.Table; 12 | 13 | /** 14 | * UserRole 15 | * 16 | * @author vladimir.stankovic 17 | * 18 | * Aug 18, 2016 19 | */ 20 | @Entity 21 | @Table(name = "USER_ROLE") 22 | public class UserRole { 23 | @Embeddable 24 | public static class Id implements Serializable { 25 | private static final long serialVersionUID = 1322120000551624359L; 26 | 27 | @Column(name = "APP_USER_ID") 28 | protected Long userId; 29 | 30 | @Enumerated(EnumType.STRING) 31 | @Column(name = "ROLE") 32 | protected Role role; 33 | 34 | public Id() { } 35 | 36 | public Id(Long userId, Role role) { 37 | this.userId = userId; 38 | this.role = role; 39 | } 40 | } 41 | 42 | @EmbeddedId 43 | Id id = new Id(); 44 | 45 | @Enumerated(EnumType.STRING) 46 | @Column(name = "ROLE", insertable=false, updatable=false) 47 | protected Role role; 48 | 49 | public Role getRole() { 50 | return role; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/svlada/profile/endpoint/ProfileEndpoint.java: -------------------------------------------------------------------------------- 1 | package com.svlada.profile.endpoint; 2 | 3 | import org.springframework.web.bind.annotation.RequestMapping; 4 | import org.springframework.web.bind.annotation.RequestMethod; 5 | import org.springframework.web.bind.annotation.ResponseBody; 6 | import org.springframework.web.bind.annotation.RestController; 7 | 8 | import com.svlada.security.auth.JwtAuthenticationToken; 9 | import com.svlada.security.model.UserContext; 10 | 11 | /** 12 | * End-point for retrieving logged-in user details. 13 | * 14 | * @author vladimir.stankovic 15 | * 16 | * Aug 4, 2016 17 | */ 18 | @RestController 19 | public class ProfileEndpoint { 20 | @RequestMapping(value="/api/me", method=RequestMethod.GET) 21 | public @ResponseBody UserContext get(JwtAuthenticationToken token) { 22 | return (UserContext) token.getPrincipal(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/svlada/security/RestAuthenticationEntryPoint.java: -------------------------------------------------------------------------------- 1 | package com.svlada.security; 2 | 3 | import java.io.IOException; 4 | 5 | import javax.servlet.ServletException; 6 | import javax.servlet.http.HttpServletRequest; 7 | import javax.servlet.http.HttpServletResponse; 8 | 9 | import org.springframework.http.HttpStatus; 10 | import org.springframework.security.core.AuthenticationException; 11 | import org.springframework.security.web.AuthenticationEntryPoint; 12 | import org.springframework.stereotype.Component; 13 | 14 | /** 15 | * 16 | * @author vladimir.stankovic 17 | * 18 | * Aug 4, 2016 19 | */ 20 | @Component 21 | public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint { 22 | @Override 23 | public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException ex) 24 | throws IOException, ServletException { 25 | response.sendError(HttpStatus.UNAUTHORIZED.value(), "Unauthorized"); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/svlada/security/UserService.java: -------------------------------------------------------------------------------- 1 | package com.svlada.security; 2 | 3 | import java.util.Optional; 4 | 5 | import com.svlada.entity.User; 6 | 7 | /** 8 | * 9 | * @author vladimir.stankovic 10 | * 11 | * Aug 17, 2016 12 | */ 13 | public interface UserService { 14 | public Optional getByUsername(String username); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/svlada/security/auth/JwtAuthenticationToken.java: -------------------------------------------------------------------------------- 1 | package com.svlada.security.auth; 2 | 3 | import java.util.Collection; 4 | 5 | import org.springframework.security.authentication.AbstractAuthenticationToken; 6 | import org.springframework.security.core.GrantedAuthority; 7 | 8 | import com.svlada.security.model.UserContext; 9 | import com.svlada.security.model.token.RawAccessJwtToken; 10 | 11 | /** 12 | * An {@link org.springframework.security.core.Authentication} implementation 13 | * that is designed for simple presentation of JwtToken. 14 | * 15 | * @author vladimir.stankovic 16 | * 17 | * May 23, 2016 18 | */ 19 | public class JwtAuthenticationToken extends AbstractAuthenticationToken { 20 | private static final long serialVersionUID = 2877954820905567501L; 21 | 22 | private RawAccessJwtToken rawAccessToken; 23 | private UserContext userContext; 24 | 25 | public JwtAuthenticationToken(RawAccessJwtToken unsafeToken) { 26 | super(null); 27 | this.rawAccessToken = unsafeToken; 28 | this.setAuthenticated(false); 29 | } 30 | 31 | public JwtAuthenticationToken(UserContext userContext, Collection authorities) { 32 | super(authorities); 33 | this.eraseCredentials(); 34 | this.userContext = userContext; 35 | super.setAuthenticated(true); 36 | } 37 | 38 | @Override 39 | public void setAuthenticated(boolean authenticated) { 40 | if (authenticated) { 41 | throw new IllegalArgumentException( 42 | "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead"); 43 | } 44 | super.setAuthenticated(false); 45 | } 46 | 47 | @Override 48 | public Object getCredentials() { 49 | return rawAccessToken; 50 | } 51 | 52 | @Override 53 | public Object getPrincipal() { 54 | return this.userContext; 55 | } 56 | 57 | @Override 58 | public void eraseCredentials() { 59 | super.eraseCredentials(); 60 | this.rawAccessToken = null; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/com/svlada/security/auth/ajax/AjaxAuthenticationProvider.java: -------------------------------------------------------------------------------- 1 | package com.svlada.security.auth.ajax; 2 | 3 | import java.util.List; 4 | import java.util.stream.Collectors; 5 | 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.security.authentication.AuthenticationProvider; 8 | import org.springframework.security.authentication.BadCredentialsException; 9 | import org.springframework.security.authentication.InsufficientAuthenticationException; 10 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 11 | import org.springframework.security.core.Authentication; 12 | import org.springframework.security.core.AuthenticationException; 13 | import org.springframework.security.core.GrantedAuthority; 14 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 15 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 16 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 17 | import org.springframework.stereotype.Component; 18 | import org.springframework.util.Assert; 19 | 20 | import com.svlada.entity.User; 21 | import com.svlada.security.model.UserContext; 22 | import com.svlada.user.service.DatabaseUserService; 23 | 24 | /** 25 | * 26 | * @author vladimir.stankovic 27 | * 28 | * Aug 3, 2016 29 | */ 30 | @Component 31 | public class AjaxAuthenticationProvider implements AuthenticationProvider { 32 | private final BCryptPasswordEncoder encoder; 33 | private final DatabaseUserService userService; 34 | 35 | @Autowired 36 | public AjaxAuthenticationProvider(final DatabaseUserService userService, final BCryptPasswordEncoder encoder) { 37 | this.userService = userService; 38 | this.encoder = encoder; 39 | } 40 | 41 | @Override 42 | public Authentication authenticate(Authentication authentication) throws AuthenticationException { 43 | Assert.notNull(authentication, "No authentication data provided"); 44 | 45 | String username = (String) authentication.getPrincipal(); 46 | String password = (String) authentication.getCredentials(); 47 | 48 | User user = userService.getByUsername(username).orElseThrow(() -> new UsernameNotFoundException("User not found: " + username)); 49 | 50 | if (!encoder.matches(password, user.getPassword())) { 51 | throw new BadCredentialsException("Authentication Failed. Username or Password not valid."); 52 | } 53 | 54 | if (user.getRoles() == null) throw new InsufficientAuthenticationException("User has no roles assigned"); 55 | 56 | List authorities = user.getRoles().stream() 57 | .map(authority -> new SimpleGrantedAuthority(authority.getRole().authority())) 58 | .collect(Collectors.toList()); 59 | 60 | UserContext userContext = UserContext.create(user.getUsername(), authorities); 61 | 62 | return new UsernamePasswordAuthenticationToken(userContext, null, userContext.getAuthorities()); 63 | } 64 | 65 | @Override 66 | public boolean supports(Class authentication) { 67 | return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication)); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/com/svlada/security/auth/ajax/AjaxAwareAuthenticationFailureHandler.java: -------------------------------------------------------------------------------- 1 | package com.svlada.security.auth.ajax; 2 | 3 | import java.io.IOException; 4 | 5 | import javax.servlet.ServletException; 6 | import javax.servlet.http.HttpServletRequest; 7 | import javax.servlet.http.HttpServletResponse; 8 | 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.http.HttpStatus; 11 | import org.springframework.http.MediaType; 12 | import org.springframework.security.authentication.BadCredentialsException; 13 | import org.springframework.security.core.AuthenticationException; 14 | import org.springframework.security.web.authentication.AuthenticationFailureHandler; 15 | import org.springframework.stereotype.Component; 16 | 17 | import com.fasterxml.jackson.databind.ObjectMapper; 18 | import com.svlada.common.ErrorCode; 19 | import com.svlada.common.ErrorResponse; 20 | import com.svlada.security.exceptions.AuthMethodNotSupportedException; 21 | import com.svlada.security.exceptions.JwtExpiredTokenException; 22 | 23 | /** 24 | * 25 | * @author vladimir.stankovic 26 | * 27 | * Aug 3, 2016 28 | */ 29 | @Component 30 | public class AjaxAwareAuthenticationFailureHandler implements AuthenticationFailureHandler { 31 | private final ObjectMapper mapper; 32 | 33 | @Autowired 34 | public AjaxAwareAuthenticationFailureHandler(ObjectMapper mapper) { 35 | this.mapper = mapper; 36 | } 37 | 38 | @Override 39 | public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, 40 | AuthenticationException e) throws IOException, ServletException { 41 | 42 | response.setStatus(HttpStatus.UNAUTHORIZED.value()); 43 | response.setContentType(MediaType.APPLICATION_JSON_VALUE); 44 | 45 | if (e instanceof BadCredentialsException) { 46 | mapper.writeValue(response.getWriter(), ErrorResponse.of("Invalid username or password", ErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)); 47 | } else if (e instanceof JwtExpiredTokenException) { 48 | mapper.writeValue(response.getWriter(), ErrorResponse.of("Token has expired", ErrorCode.JWT_TOKEN_EXPIRED, HttpStatus.UNAUTHORIZED)); 49 | } else if (e instanceof AuthMethodNotSupportedException) { 50 | mapper.writeValue(response.getWriter(), ErrorResponse.of(e.getMessage(), ErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)); 51 | } 52 | 53 | mapper.writeValue(response.getWriter(), ErrorResponse.of("Authentication failed", ErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/svlada/security/auth/ajax/AjaxAwareAuthenticationSuccessHandler.java: -------------------------------------------------------------------------------- 1 | package com.svlada.security.auth.ajax; 2 | 3 | import java.io.IOException; 4 | import java.util.HashMap; 5 | import java.util.Map; 6 | 7 | import javax.servlet.ServletException; 8 | import javax.servlet.http.HttpServletRequest; 9 | import javax.servlet.http.HttpServletResponse; 10 | import javax.servlet.http.HttpSession; 11 | 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.http.HttpStatus; 14 | import org.springframework.http.MediaType; 15 | import org.springframework.security.core.Authentication; 16 | import org.springframework.security.web.WebAttributes; 17 | import org.springframework.security.web.authentication.AuthenticationSuccessHandler; 18 | import org.springframework.stereotype.Component; 19 | 20 | import com.fasterxml.jackson.databind.ObjectMapper; 21 | import com.svlada.security.model.UserContext; 22 | import com.svlada.security.model.token.JwtToken; 23 | import com.svlada.security.model.token.JwtTokenFactory; 24 | 25 | /** 26 | * AjaxAwareAuthenticationSuccessHandler 27 | * 28 | * @author vladimir.stankovic 29 | * 30 | * Aug 3, 2016 31 | */ 32 | @Component 33 | public class AjaxAwareAuthenticationSuccessHandler implements AuthenticationSuccessHandler { 34 | private final ObjectMapper mapper; 35 | private final JwtTokenFactory tokenFactory; 36 | 37 | @Autowired 38 | public AjaxAwareAuthenticationSuccessHandler(final ObjectMapper mapper, final JwtTokenFactory tokenFactory) { 39 | this.mapper = mapper; 40 | this.tokenFactory = tokenFactory; 41 | } 42 | 43 | @Override 44 | public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, 45 | Authentication authentication) throws IOException, ServletException { 46 | UserContext userContext = (UserContext) authentication.getPrincipal(); 47 | 48 | JwtToken accessToken = tokenFactory.createAccessJwtToken(userContext); 49 | JwtToken refreshToken = tokenFactory.createRefreshToken(userContext); 50 | 51 | Map tokenMap = new HashMap(); 52 | tokenMap.put("token", accessToken.getToken()); 53 | tokenMap.put("refreshToken", refreshToken.getToken()); 54 | 55 | response.setStatus(HttpStatus.OK.value()); 56 | response.setContentType(MediaType.APPLICATION_JSON_VALUE); 57 | mapper.writeValue(response.getWriter(), tokenMap); 58 | 59 | clearAuthenticationAttributes(request); 60 | } 61 | 62 | /** 63 | * Removes temporary authentication-related data which may have been stored 64 | * in the session during the authentication process.. 65 | * 66 | */ 67 | protected final void clearAuthenticationAttributes(HttpServletRequest request) { 68 | HttpSession session = request.getSession(false); 69 | 70 | if (session == null) { 71 | return; 72 | } 73 | 74 | session.removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/com/svlada/security/auth/ajax/AjaxLoginProcessingFilter.java: -------------------------------------------------------------------------------- 1 | package com.svlada.security.auth.ajax; 2 | 3 | import java.io.IOException; 4 | 5 | import javax.servlet.FilterChain; 6 | import javax.servlet.ServletException; 7 | import javax.servlet.http.HttpServletRequest; 8 | import javax.servlet.http.HttpServletResponse; 9 | 10 | import org.apache.commons.lang3.StringUtils; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | import org.springframework.http.HttpMethod; 14 | import org.springframework.security.authentication.AuthenticationServiceException; 15 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 16 | import org.springframework.security.core.Authentication; 17 | import org.springframework.security.core.AuthenticationException; 18 | import org.springframework.security.core.context.SecurityContextHolder; 19 | import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; 20 | import org.springframework.security.web.authentication.AuthenticationFailureHandler; 21 | import org.springframework.security.web.authentication.AuthenticationSuccessHandler; 22 | 23 | import com.fasterxml.jackson.databind.ObjectMapper; 24 | import com.svlada.common.WebUtil; 25 | import com.svlada.security.exceptions.AuthMethodNotSupportedException; 26 | 27 | /** 28 | * AjaxLoginProcessingFilter 29 | * 30 | * @author vladimir.stankovic 31 | * 32 | * Aug 3, 2016 33 | */ 34 | public class AjaxLoginProcessingFilter extends AbstractAuthenticationProcessingFilter { 35 | private static Logger logger = LoggerFactory.getLogger(AjaxLoginProcessingFilter.class); 36 | 37 | private final AuthenticationSuccessHandler successHandler; 38 | private final AuthenticationFailureHandler failureHandler; 39 | 40 | private final ObjectMapper objectMapper; 41 | 42 | public AjaxLoginProcessingFilter(String defaultProcessUrl, AuthenticationSuccessHandler successHandler, 43 | AuthenticationFailureHandler failureHandler, ObjectMapper mapper) { 44 | super(defaultProcessUrl); 45 | this.successHandler = successHandler; 46 | this.failureHandler = failureHandler; 47 | this.objectMapper = mapper; 48 | } 49 | 50 | @Override 51 | public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) 52 | throws AuthenticationException, IOException, ServletException { 53 | if (!HttpMethod.POST.name().equals(request.getMethod()) || !WebUtil.isAjax(request)) { 54 | if(logger.isDebugEnabled()) { 55 | logger.debug("Authentication method not supported. Request method: " + request.getMethod()); 56 | } 57 | throw new AuthMethodNotSupportedException("Authentication method not supported"); 58 | } 59 | 60 | LoginRequest loginRequest = objectMapper.readValue(request.getReader(), LoginRequest.class); 61 | 62 | if (StringUtils.isBlank(loginRequest.getUsername()) || StringUtils.isBlank(loginRequest.getPassword())) { 63 | throw new AuthenticationServiceException("Username or Password not provided"); 64 | } 65 | 66 | UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()); 67 | 68 | return this.getAuthenticationManager().authenticate(token); 69 | } 70 | 71 | @Override 72 | protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, 73 | Authentication authResult) throws IOException, ServletException { 74 | successHandler.onAuthenticationSuccess(request, response, authResult); 75 | } 76 | 77 | @Override 78 | protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, 79 | AuthenticationException failed) throws IOException, ServletException { 80 | SecurityContextHolder.clearContext(); 81 | failureHandler.onAuthenticationFailure(request, response, failed); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/com/svlada/security/auth/ajax/LoginRequest.java: -------------------------------------------------------------------------------- 1 | package com.svlada.security.auth.ajax; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | 6 | /** 7 | * Model intended to be used for AJAX based authentication. 8 | * 9 | * @author vladimir.stankovic 10 | * 11 | * Aug 3, 2016 12 | */ 13 | 14 | public class LoginRequest { 15 | private String username; 16 | private String password; 17 | 18 | @JsonCreator 19 | public LoginRequest(@JsonProperty("username") String username, @JsonProperty("password") String password) { 20 | this.username = username; 21 | this.password = password; 22 | } 23 | 24 | public String getUsername() { 25 | return username; 26 | } 27 | 28 | public String getPassword() { 29 | return password; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/svlada/security/auth/jwt/JwtAuthenticationProvider.java: -------------------------------------------------------------------------------- 1 | package com.svlada.security.auth.jwt; 2 | 3 | import java.util.List; 4 | import java.util.stream.Collectors; 5 | 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.security.authentication.AuthenticationProvider; 8 | import org.springframework.security.core.Authentication; 9 | import org.springframework.security.core.AuthenticationException; 10 | import org.springframework.security.core.GrantedAuthority; 11 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 12 | import org.springframework.stereotype.Component; 13 | 14 | import com.svlada.security.auth.JwtAuthenticationToken; 15 | import com.svlada.security.config.JwtSettings; 16 | import com.svlada.security.model.UserContext; 17 | import com.svlada.security.model.token.JwtToken; 18 | import com.svlada.security.model.token.RawAccessJwtToken; 19 | 20 | import io.jsonwebtoken.Claims; 21 | import io.jsonwebtoken.Jws; 22 | 23 | /** 24 | * An {@link AuthenticationProvider} implementation that will use provided 25 | * instance of {@link JwtToken} to perform authentication. 26 | * 27 | * @author vladimir.stankovic 28 | * 29 | * Aug 5, 2016 30 | */ 31 | @Component 32 | @SuppressWarnings("unchecked") 33 | public class JwtAuthenticationProvider implements AuthenticationProvider { 34 | private final JwtSettings jwtSettings; 35 | 36 | @Autowired 37 | public JwtAuthenticationProvider(JwtSettings jwtSettings) { 38 | this.jwtSettings = jwtSettings; 39 | } 40 | 41 | @Override 42 | public Authentication authenticate(Authentication authentication) throws AuthenticationException { 43 | RawAccessJwtToken rawAccessToken = (RawAccessJwtToken) authentication.getCredentials(); 44 | 45 | Jws jwsClaims = rawAccessToken.parseClaims(jwtSettings.getTokenSigningKey()); 46 | String subject = jwsClaims.getBody().getSubject(); 47 | List scopes = jwsClaims.getBody().get("scopes", List.class); 48 | List authorities = scopes.stream() 49 | .map(SimpleGrantedAuthority::new) 50 | .collect(Collectors.toList()); 51 | 52 | UserContext context = UserContext.create(subject, authorities); 53 | 54 | return new JwtAuthenticationToken(context, context.getAuthorities()); 55 | } 56 | 57 | @Override 58 | public boolean supports(Class authentication) { 59 | return (JwtAuthenticationToken.class.isAssignableFrom(authentication)); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/svlada/security/auth/jwt/JwtTokenAuthenticationProcessingFilter.java: -------------------------------------------------------------------------------- 1 | package com.svlada.security.auth.jwt; 2 | 3 | import java.io.IOException; 4 | 5 | import javax.servlet.FilterChain; 6 | import javax.servlet.ServletException; 7 | import javax.servlet.http.HttpServletRequest; 8 | import javax.servlet.http.HttpServletResponse; 9 | 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.security.core.Authentication; 12 | import org.springframework.security.core.AuthenticationException; 13 | import org.springframework.security.core.context.SecurityContext; 14 | import org.springframework.security.core.context.SecurityContextHolder; 15 | import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; 16 | import org.springframework.security.web.authentication.AuthenticationFailureHandler; 17 | import org.springframework.security.web.util.matcher.RequestMatcher; 18 | 19 | import com.svlada.security.auth.JwtAuthenticationToken; 20 | import com.svlada.security.auth.jwt.extractor.TokenExtractor; 21 | import com.svlada.security.config.WebSecurityConfig; 22 | import com.svlada.security.model.token.RawAccessJwtToken; 23 | 24 | /** 25 | * Performs validation of provided JWT Token. 26 | * 27 | * @author vladimir.stankovic 28 | * 29 | * Aug 5, 2016 30 | */ 31 | public class JwtTokenAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter { 32 | private final AuthenticationFailureHandler failureHandler; 33 | private final TokenExtractor tokenExtractor; 34 | 35 | @Autowired 36 | public JwtTokenAuthenticationProcessingFilter(AuthenticationFailureHandler failureHandler, 37 | TokenExtractor tokenExtractor, RequestMatcher matcher) { 38 | super(matcher); 39 | this.failureHandler = failureHandler; 40 | this.tokenExtractor = tokenExtractor; 41 | } 42 | 43 | @Override 44 | public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) 45 | throws AuthenticationException, IOException, ServletException { 46 | String tokenPayload = request.getHeader(WebSecurityConfig.AUTHENTICATION_HEADER_NAME); 47 | RawAccessJwtToken token = new RawAccessJwtToken(tokenExtractor.extract(tokenPayload)); 48 | return getAuthenticationManager().authenticate(new JwtAuthenticationToken(token)); 49 | } 50 | 51 | @Override 52 | protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, 53 | Authentication authResult) throws IOException, ServletException { 54 | SecurityContext context = SecurityContextHolder.createEmptyContext(); 55 | context.setAuthentication(authResult); 56 | SecurityContextHolder.setContext(context); 57 | chain.doFilter(request, response); 58 | } 59 | 60 | @Override 61 | protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, 62 | AuthenticationException failed) throws IOException, ServletException { 63 | SecurityContextHolder.clearContext(); 64 | failureHandler.onAuthenticationFailure(request, response, failed); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/com/svlada/security/auth/jwt/SkipPathRequestMatcher.java: -------------------------------------------------------------------------------- 1 | package com.svlada.security.auth.jwt; 2 | 3 | import java.util.List; 4 | import java.util.stream.Collectors; 5 | 6 | import javax.servlet.http.HttpServletRequest; 7 | 8 | import org.springframework.security.web.util.matcher.AntPathRequestMatcher; 9 | import org.springframework.security.web.util.matcher.OrRequestMatcher; 10 | import org.springframework.security.web.util.matcher.RequestMatcher; 11 | import org.springframework.util.Assert; 12 | 13 | /** 14 | * SkipPathRequestMatcher 15 | * 16 | * @author vladimir.stankovic 17 | * 18 | * Aug 19, 2016 19 | */ 20 | public class SkipPathRequestMatcher implements RequestMatcher { 21 | private OrRequestMatcher matchers; 22 | private RequestMatcher processingMatcher; 23 | 24 | public SkipPathRequestMatcher(List pathsToSkip, String processingPath) { 25 | Assert.notNull(pathsToSkip); 26 | List m = pathsToSkip.stream().map(path -> new AntPathRequestMatcher(path)).collect(Collectors.toList()); 27 | matchers = new OrRequestMatcher(m); 28 | processingMatcher = new AntPathRequestMatcher(processingPath); 29 | } 30 | 31 | @Override 32 | public boolean matches(HttpServletRequest request) { 33 | if (matchers.matches(request)) { 34 | return false; 35 | } 36 | return processingMatcher.matches(request) ? true : false; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/svlada/security/auth/jwt/extractor/JwtHeaderTokenExtractor.java: -------------------------------------------------------------------------------- 1 | package com.svlada.security.auth.jwt.extractor; 2 | 3 | import org.apache.commons.lang3.StringUtils; 4 | import org.springframework.security.authentication.AuthenticationServiceException; 5 | import org.springframework.stereotype.Component; 6 | 7 | /** 8 | * An implementation of {@link TokenExtractor} extracts token from 9 | * Authorization: Bearer scheme. 10 | * 11 | * @author vladimir.stankovic 12 | * 13 | * Aug 5, 2016 14 | */ 15 | @Component 16 | public class JwtHeaderTokenExtractor implements TokenExtractor { 17 | public static String HEADER_PREFIX = "Bearer "; 18 | 19 | @Override 20 | public String extract(String header) { 21 | if (StringUtils.isBlank(header)) { 22 | throw new AuthenticationServiceException("Authorization header cannot be blank!"); 23 | } 24 | 25 | if (header.length() < HEADER_PREFIX.length()) { 26 | throw new AuthenticationServiceException("Invalid authorization header size."); 27 | } 28 | 29 | return header.substring(HEADER_PREFIX.length(), header.length()); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/svlada/security/auth/jwt/extractor/TokenExtractor.java: -------------------------------------------------------------------------------- 1 | package com.svlada.security.auth.jwt.extractor; 2 | 3 | /** 4 | * Implementations of this interface should always return raw base-64 encoded 5 | * representation of JWT Token. 6 | * 7 | * @author vladimir.stankovic 8 | * 9 | * Aug 5, 2016 10 | */ 11 | public interface TokenExtractor { 12 | public String extract(String payload); 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/svlada/security/auth/jwt/verifier/BloomFilterTokenVerifier.java: -------------------------------------------------------------------------------- 1 | package com.svlada.security.auth.jwt.verifier; 2 | 3 | import org.springframework.stereotype.Component; 4 | 5 | /** 6 | * BloomFilterTokenVerifier 7 | * 8 | * @author vladimir.stankovic 9 | * 10 | * Aug 17, 2016 11 | */ 12 | @Component 13 | public class BloomFilterTokenVerifier implements TokenVerifier { 14 | @Override 15 | public boolean verify(String jti) { 16 | return true; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/svlada/security/auth/jwt/verifier/TokenVerifier.java: -------------------------------------------------------------------------------- 1 | package com.svlada.security.auth.jwt.verifier; 2 | 3 | /** 4 | * 5 | * @author vladimir.stankovic 6 | * 7 | * Aug 17, 2016 8 | */ 9 | public interface TokenVerifier { 10 | public boolean verify(String jti); 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/svlada/security/config/JwtSettings.java: -------------------------------------------------------------------------------- 1 | package com.svlada.security.config; 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties; 4 | import org.springframework.context.annotation.Configuration; 5 | 6 | @Configuration 7 | @ConfigurationProperties(prefix = "demo.security.jwt") 8 | public class JwtSettings { 9 | /** 10 | * {@link JwtToken} will expire after this time. 11 | */ 12 | private Integer tokenExpirationTime; 13 | 14 | /** 15 | * Token issuer. 16 | */ 17 | private String tokenIssuer; 18 | 19 | /** 20 | * Key is used to sign {@link JwtToken}. 21 | */ 22 | private String tokenSigningKey; 23 | 24 | /** 25 | * {@link JwtToken} can be refreshed during this timeframe. 26 | */ 27 | private Integer refreshTokenExpTime; 28 | 29 | public Integer getRefreshTokenExpTime() { 30 | return refreshTokenExpTime; 31 | } 32 | 33 | public void setRefreshTokenExpTime(Integer refreshTokenExpTime) { 34 | this.refreshTokenExpTime = refreshTokenExpTime; 35 | } 36 | 37 | public Integer getTokenExpirationTime() { 38 | return tokenExpirationTime; 39 | } 40 | 41 | public void setTokenExpirationTime(Integer tokenExpirationTime) { 42 | this.tokenExpirationTime = tokenExpirationTime; 43 | } 44 | 45 | public String getTokenIssuer() { 46 | return tokenIssuer; 47 | } 48 | public void setTokenIssuer(String tokenIssuer) { 49 | this.tokenIssuer = tokenIssuer; 50 | } 51 | 52 | public String getTokenSigningKey() { 53 | return tokenSigningKey; 54 | } 55 | 56 | public void setTokenSigningKey(String tokenSigningKey) { 57 | this.tokenSigningKey = tokenSigningKey; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/com/svlada/security/config/PasswordEncoderConfig.java: -------------------------------------------------------------------------------- 1 | package com.svlada.security.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 6 | 7 | /** 8 | * PasswordEncoderConfig 9 | * 10 | * @author vladimir.stankovic 11 | * 12 | * Dec 27, 2016 13 | */ 14 | @Configuration 15 | public class PasswordEncoderConfig { 16 | @Bean 17 | protected BCryptPasswordEncoder passwordEncoder() { 18 | return new BCryptPasswordEncoder(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/svlada/security/config/WebSecurityConfig.java: -------------------------------------------------------------------------------- 1 | package com.svlada.security.config; 2 | 3 | import java.util.Arrays; 4 | import java.util.List; 5 | 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.security.authentication.AuthenticationManager; 10 | import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 11 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 12 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 13 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 14 | import org.springframework.security.config.http.SessionCreationPolicy; 15 | import org.springframework.security.web.authentication.AuthenticationFailureHandler; 16 | import org.springframework.security.web.authentication.AuthenticationSuccessHandler; 17 | import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; 18 | 19 | import com.fasterxml.jackson.databind.ObjectMapper; 20 | import com.svlada.CustomCorsFilter; 21 | import com.svlada.security.RestAuthenticationEntryPoint; 22 | import com.svlada.security.auth.ajax.AjaxAuthenticationProvider; 23 | import com.svlada.security.auth.ajax.AjaxLoginProcessingFilter; 24 | import com.svlada.security.auth.jwt.JwtAuthenticationProvider; 25 | import com.svlada.security.auth.jwt.JwtTokenAuthenticationProcessingFilter; 26 | import com.svlada.security.auth.jwt.SkipPathRequestMatcher; 27 | import com.svlada.security.auth.jwt.extractor.TokenExtractor; 28 | 29 | /** 30 | * WebSecurityConfig 31 | * 32 | * @author vladimir.stankovic 33 | * 34 | * Aug 3, 2016 35 | */ 36 | @Configuration 37 | @EnableWebSecurity 38 | public class WebSecurityConfig extends WebSecurityConfigurerAdapter { 39 | public static final String AUTHENTICATION_HEADER_NAME = "Authorization"; 40 | public static final String AUTHENTICATION_URL = "/api/auth/login"; 41 | public static final String REFRESH_TOKEN_URL = "/api/auth/token"; 42 | public static final String API_ROOT_URL = "/api/**"; 43 | 44 | @Autowired private RestAuthenticationEntryPoint authenticationEntryPoint; 45 | @Autowired private AuthenticationSuccessHandler successHandler; 46 | @Autowired private AuthenticationFailureHandler failureHandler; 47 | @Autowired private AjaxAuthenticationProvider ajaxAuthenticationProvider; 48 | @Autowired private JwtAuthenticationProvider jwtAuthenticationProvider; 49 | 50 | @Autowired private TokenExtractor tokenExtractor; 51 | 52 | @Autowired private AuthenticationManager authenticationManager; 53 | 54 | @Autowired private ObjectMapper objectMapper; 55 | 56 | protected AjaxLoginProcessingFilter buildAjaxLoginProcessingFilter(String loginEntryPoint) throws Exception { 57 | AjaxLoginProcessingFilter filter = new AjaxLoginProcessingFilter(loginEntryPoint, successHandler, failureHandler, objectMapper); 58 | filter.setAuthenticationManager(this.authenticationManager); 59 | return filter; 60 | } 61 | 62 | protected JwtTokenAuthenticationProcessingFilter buildJwtTokenAuthenticationProcessingFilter(List pathsToSkip, String pattern) throws Exception { 63 | SkipPathRequestMatcher matcher = new SkipPathRequestMatcher(pathsToSkip, pattern); 64 | JwtTokenAuthenticationProcessingFilter filter 65 | = new JwtTokenAuthenticationProcessingFilter(failureHandler, tokenExtractor, matcher); 66 | filter.setAuthenticationManager(this.authenticationManager); 67 | return filter; 68 | } 69 | 70 | @Bean 71 | @Override 72 | public AuthenticationManager authenticationManagerBean() throws Exception { 73 | return super.authenticationManagerBean(); 74 | } 75 | 76 | @Override 77 | protected void configure(AuthenticationManagerBuilder auth) { 78 | auth.authenticationProvider(ajaxAuthenticationProvider); 79 | auth.authenticationProvider(jwtAuthenticationProvider); 80 | } 81 | 82 | @Override 83 | protected void configure(HttpSecurity http) throws Exception { 84 | List permitAllEndpointList = Arrays.asList( 85 | AUTHENTICATION_URL, 86 | REFRESH_TOKEN_URL, 87 | "/console" 88 | ); 89 | 90 | http 91 | .csrf().disable() // We don't need CSRF for JWT based authentication 92 | .exceptionHandling() 93 | .authenticationEntryPoint(this.authenticationEntryPoint) 94 | 95 | .and() 96 | .sessionManagement() 97 | .sessionCreationPolicy(SessionCreationPolicy.STATELESS) 98 | 99 | .and() 100 | .authorizeRequests() 101 | .antMatchers(permitAllEndpointList.toArray(new String[permitAllEndpointList.size()])) 102 | .permitAll() 103 | .and() 104 | .authorizeRequests() 105 | .antMatchers(API_ROOT_URL).authenticated() // Protected API End-points 106 | .and() 107 | .addFilterBefore(new CustomCorsFilter(), UsernamePasswordAuthenticationFilter.class) 108 | .addFilterBefore(buildAjaxLoginProcessingFilter(AUTHENTICATION_URL), UsernamePasswordAuthenticationFilter.class) 109 | .addFilterBefore(buildJwtTokenAuthenticationProcessingFilter(permitAllEndpointList, 110 | API_ROOT_URL), UsernamePasswordAuthenticationFilter.class); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/main/java/com/svlada/security/endpoint/RefreshTokenEndpoint.java: -------------------------------------------------------------------------------- 1 | package com.svlada.security.endpoint; 2 | 3 | import java.io.IOException; 4 | import java.util.List; 5 | import java.util.stream.Collectors; 6 | 7 | import javax.servlet.ServletException; 8 | 9 | import javax.servlet.http.HttpServletRequest; 10 | import javax.servlet.http.HttpServletResponse; 11 | 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.beans.factory.annotation.Qualifier; 14 | import org.springframework.http.MediaType; 15 | import org.springframework.security.authentication.InsufficientAuthenticationException; 16 | import org.springframework.security.core.GrantedAuthority; 17 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 18 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 19 | import org.springframework.web.bind.annotation.RequestMapping; 20 | import org.springframework.web.bind.annotation.RequestMethod; 21 | import org.springframework.web.bind.annotation.ResponseBody; 22 | import org.springframework.web.bind.annotation.RestController; 23 | 24 | import com.svlada.entity.User; 25 | import com.svlada.security.UserService; 26 | import com.svlada.security.auth.jwt.extractor.TokenExtractor; 27 | import com.svlada.security.auth.jwt.verifier.TokenVerifier; 28 | import com.svlada.security.config.JwtSettings; 29 | import com.svlada.security.config.WebSecurityConfig; 30 | import com.svlada.security.exceptions.InvalidJwtToken; 31 | import com.svlada.security.model.UserContext; 32 | import com.svlada.security.model.token.JwtToken; 33 | import com.svlada.security.model.token.JwtTokenFactory; 34 | import com.svlada.security.model.token.RawAccessJwtToken; 35 | import com.svlada.security.model.token.RefreshToken; 36 | 37 | /** 38 | * RefreshTokenEndpoint 39 | * 40 | * @author vladimir.stankovic 41 | * 42 | * Aug 17, 2016 43 | */ 44 | @RestController 45 | public class RefreshTokenEndpoint { 46 | @Autowired private JwtTokenFactory tokenFactory; 47 | @Autowired private JwtSettings jwtSettings; 48 | @Autowired private UserService userService; 49 | @Autowired private TokenVerifier tokenVerifier; 50 | @Autowired @Qualifier("jwtHeaderTokenExtractor") private TokenExtractor tokenExtractor; 51 | 52 | @RequestMapping(value="/api/auth/token", method=RequestMethod.GET, produces={ MediaType.APPLICATION_JSON_VALUE }) 53 | public @ResponseBody JwtToken refreshToken(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { 54 | String tokenPayload = tokenExtractor.extract(request.getHeader(WebSecurityConfig.AUTHENTICATION_HEADER_NAME)); 55 | 56 | RawAccessJwtToken rawToken = new RawAccessJwtToken(tokenPayload); 57 | RefreshToken refreshToken = RefreshToken.create(rawToken, jwtSettings.getTokenSigningKey()).orElseThrow(() -> new InvalidJwtToken()); 58 | 59 | String jti = refreshToken.getJti(); 60 | if (!tokenVerifier.verify(jti)) { 61 | throw new InvalidJwtToken(); 62 | } 63 | 64 | String subject = refreshToken.getSubject(); 65 | User user = userService.getByUsername(subject).orElseThrow(() -> new UsernameNotFoundException("User not found: " + subject)); 66 | 67 | if (user.getRoles() == null) throw new InsufficientAuthenticationException("User has no roles assigned"); 68 | List authorities = user.getRoles().stream() 69 | .map(authority -> new SimpleGrantedAuthority(authority.getRole().authority())) 70 | .collect(Collectors.toList()); 71 | 72 | UserContext userContext = UserContext.create(user.getUsername(), authorities); 73 | 74 | return tokenFactory.createAccessJwtToken(userContext); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/com/svlada/security/exceptions/AuthMethodNotSupportedException.java: -------------------------------------------------------------------------------- 1 | package com.svlada.security.exceptions; 2 | 3 | import org.springframework.security.authentication.AuthenticationServiceException; 4 | 5 | /** 6 | * 7 | * @author vladimir.stankovic 8 | * 9 | * Aug 4, 2016 10 | */ 11 | public class AuthMethodNotSupportedException extends AuthenticationServiceException { 12 | private static final long serialVersionUID = 3705043083010304496L; 13 | 14 | public AuthMethodNotSupportedException(String msg) { 15 | super(msg); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/svlada/security/exceptions/InvalidJwtToken.java: -------------------------------------------------------------------------------- 1 | package com.svlada.security.exceptions; 2 | 3 | /** 4 | * JwtTokenNotValid 5 | * 6 | * @author vladimir.stankovic 7 | * 8 | * Aug 17, 2016 9 | */ 10 | public class InvalidJwtToken extends RuntimeException { 11 | private static final long serialVersionUID = -294671188037098603L; 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/svlada/security/exceptions/JwtExpiredTokenException.java: -------------------------------------------------------------------------------- 1 | package com.svlada.security.exceptions; 2 | 3 | import org.springframework.security.core.AuthenticationException; 4 | 5 | import com.svlada.security.model.token.JwtToken; 6 | 7 | /** 8 | * 9 | * @author vladimir.stankovic 10 | * 11 | * Aug 3, 2016 12 | */ 13 | public class JwtExpiredTokenException extends AuthenticationException { 14 | private static final long serialVersionUID = -5959543783324224864L; 15 | 16 | private JwtToken token; 17 | 18 | public JwtExpiredTokenException(String msg) { 19 | super(msg); 20 | } 21 | 22 | public JwtExpiredTokenException(JwtToken token, String msg, Throwable t) { 23 | super(msg, t); 24 | this.token = token; 25 | } 26 | 27 | public String token() { 28 | return this.token.getToken(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/svlada/security/model/Scopes.java: -------------------------------------------------------------------------------- 1 | package com.svlada.security.model; 2 | 3 | /** 4 | * Scopes 5 | * 6 | * @author vladimir.stankovic 7 | * 8 | * Aug 18, 2016 9 | */ 10 | public enum Scopes { 11 | REFRESH_TOKEN; 12 | 13 | public String authority() { 14 | return "ROLE_" + this.name(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/svlada/security/model/UserContext.java: -------------------------------------------------------------------------------- 1 | package com.svlada.security.model; 2 | 3 | import java.util.List; 4 | 5 | import org.apache.commons.lang3.StringUtils; 6 | import org.springframework.security.core.GrantedAuthority; 7 | 8 | /** 9 | * 10 | * @author vladimir.stankovic 11 | * 12 | * Aug 4, 2016 13 | */ 14 | public class UserContext { 15 | private final String username; 16 | private final List authorities; 17 | 18 | private UserContext(String username, List authorities) { 19 | this.username = username; 20 | this.authorities = authorities; 21 | } 22 | 23 | public static UserContext create(String username, List authorities) { 24 | if (StringUtils.isBlank(username)) throw new IllegalArgumentException("Username is blank: " + username); 25 | return new UserContext(username, authorities); 26 | } 27 | 28 | public String getUsername() { 29 | return username; 30 | } 31 | 32 | public List getAuthorities() { 33 | return authorities; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/svlada/security/model/token/AccessJwtToken.java: -------------------------------------------------------------------------------- 1 | package com.svlada.security.model.token; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | 5 | import io.jsonwebtoken.Claims; 6 | 7 | /** 8 | * Raw representation of JWT Token. 9 | * 10 | * @author vladimir.stankovic 11 | * 12 | * May 31, 2016 13 | */ 14 | public final class AccessJwtToken implements JwtToken { 15 | private final String rawToken; 16 | @JsonIgnore private Claims claims; 17 | 18 | protected AccessJwtToken(final String token, Claims claims) { 19 | this.rawToken = token; 20 | this.claims = claims; 21 | } 22 | 23 | public String getToken() { 24 | return this.rawToken; 25 | } 26 | 27 | public Claims getClaims() { 28 | return claims; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/svlada/security/model/token/JwtToken.java: -------------------------------------------------------------------------------- 1 | package com.svlada.security.model.token; 2 | 3 | public interface JwtToken { 4 | String getToken(); 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/com/svlada/security/model/token/JwtTokenFactory.java: -------------------------------------------------------------------------------- 1 | package com.svlada.security.model.token; 2 | 3 | import java.time.LocalDateTime; 4 | import java.time.ZoneId; 5 | import java.util.Arrays; 6 | import java.util.Date; 7 | import java.util.UUID; 8 | import java.util.stream.Collectors; 9 | 10 | import org.apache.commons.lang3.StringUtils; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.stereotype.Component; 13 | 14 | import com.svlada.security.config.JwtSettings; 15 | import com.svlada.security.model.Scopes; 16 | import com.svlada.security.model.UserContext; 17 | 18 | import io.jsonwebtoken.Claims; 19 | import io.jsonwebtoken.Jwts; 20 | import io.jsonwebtoken.SignatureAlgorithm; 21 | 22 | /** 23 | * Factory class that should be always used to create {@link JwtToken}. 24 | * 25 | * @author vladimir.stankovic 26 | * 27 | * May 31, 2016 28 | */ 29 | @Component 30 | public class JwtTokenFactory { 31 | private final JwtSettings settings; 32 | 33 | @Autowired 34 | public JwtTokenFactory(JwtSettings settings) { 35 | this.settings = settings; 36 | } 37 | 38 | /** 39 | * Factory method for issuing new JWT Tokens. 40 | * 41 | * @param username 42 | * @param roles 43 | * @return 44 | */ 45 | public AccessJwtToken createAccessJwtToken(UserContext userContext) { 46 | if (StringUtils.isBlank(userContext.getUsername())) 47 | throw new IllegalArgumentException("Cannot create JWT Token without username"); 48 | 49 | if (userContext.getAuthorities() == null || userContext.getAuthorities().isEmpty()) 50 | throw new IllegalArgumentException("User doesn't have any privileges"); 51 | 52 | Claims claims = Jwts.claims().setSubject(userContext.getUsername()); 53 | claims.put("scopes", userContext.getAuthorities().stream().map(s -> s.toString()).collect(Collectors.toList())); 54 | 55 | LocalDateTime currentTime = LocalDateTime.now(); 56 | 57 | String token = Jwts.builder() 58 | .setClaims(claims) 59 | .setIssuer(settings.getTokenIssuer()) 60 | .setIssuedAt(Date.from(currentTime.atZone(ZoneId.systemDefault()).toInstant())) 61 | .setExpiration(Date.from(currentTime 62 | .plusMinutes(settings.getTokenExpirationTime()) 63 | .atZone(ZoneId.systemDefault()).toInstant())) 64 | .signWith(SignatureAlgorithm.HS512, settings.getTokenSigningKey()) 65 | .compact(); 66 | 67 | return new AccessJwtToken(token, claims); 68 | } 69 | 70 | public JwtToken createRefreshToken(UserContext userContext) { 71 | if (StringUtils.isBlank(userContext.getUsername())) { 72 | throw new IllegalArgumentException("Cannot create JWT Token without username"); 73 | } 74 | 75 | LocalDateTime currentTime = LocalDateTime.now(); 76 | 77 | Claims claims = Jwts.claims().setSubject(userContext.getUsername()); 78 | claims.put("scopes", Arrays.asList(Scopes.REFRESH_TOKEN.authority())); 79 | 80 | String token = Jwts.builder() 81 | .setClaims(claims) 82 | .setIssuer(settings.getTokenIssuer()) 83 | .setId(UUID.randomUUID().toString()) 84 | .setIssuedAt(Date.from(currentTime.atZone(ZoneId.systemDefault()).toInstant())) 85 | .setExpiration(Date.from(currentTime 86 | .plusMinutes(settings.getRefreshTokenExpTime()) 87 | .atZone(ZoneId.systemDefault()).toInstant())) 88 | .signWith(SignatureAlgorithm.HS512, settings.getTokenSigningKey()) 89 | .compact(); 90 | 91 | return new AccessJwtToken(token, claims); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/main/java/com/svlada/security/model/token/RawAccessJwtToken.java: -------------------------------------------------------------------------------- 1 | package com.svlada.security.model.token; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.security.authentication.BadCredentialsException; 6 | 7 | import com.svlada.security.exceptions.JwtExpiredTokenException; 8 | 9 | import io.jsonwebtoken.Claims; 10 | import io.jsonwebtoken.ExpiredJwtException; 11 | import io.jsonwebtoken.Jws; 12 | import io.jsonwebtoken.Jwts; 13 | import io.jsonwebtoken.MalformedJwtException; 14 | import io.jsonwebtoken.SignatureException; 15 | import io.jsonwebtoken.UnsupportedJwtException; 16 | 17 | public class RawAccessJwtToken implements JwtToken { 18 | private static Logger logger = LoggerFactory.getLogger(RawAccessJwtToken.class); 19 | 20 | private String token; 21 | 22 | public RawAccessJwtToken(String token) { 23 | this.token = token; 24 | } 25 | 26 | /** 27 | * Parses and validates JWT Token signature. 28 | * 29 | * @throws BadCredentialsException 30 | * @throws JwtExpiredTokenException 31 | * 32 | */ 33 | public Jws parseClaims(String signingKey) { 34 | try { 35 | return Jwts.parser().setSigningKey(signingKey).parseClaimsJws(this.token); 36 | } catch (UnsupportedJwtException | MalformedJwtException | IllegalArgumentException | SignatureException ex) { 37 | logger.error("Invalid JWT Token", ex); 38 | throw new BadCredentialsException("Invalid JWT token: ", ex); 39 | } catch (ExpiredJwtException expiredEx) { 40 | logger.info("JWT Token is expired", expiredEx); 41 | throw new JwtExpiredTokenException(this, "JWT Token expired", expiredEx); 42 | } 43 | } 44 | 45 | @Override 46 | public String getToken() { 47 | return token; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/svlada/security/model/token/RefreshToken.java: -------------------------------------------------------------------------------- 1 | package com.svlada.security.model.token; 2 | 3 | import java.util.List; 4 | import java.util.Optional; 5 | 6 | import org.springframework.security.authentication.BadCredentialsException; 7 | 8 | import com.svlada.security.exceptions.JwtExpiredTokenException; 9 | import com.svlada.security.model.Scopes; 10 | 11 | import io.jsonwebtoken.Claims; 12 | import io.jsonwebtoken.Jws; 13 | 14 | /** 15 | * RefreshToken 16 | * 17 | * @author vladimir.stankovic 18 | * 19 | * Aug 19, 2016 20 | */ 21 | @SuppressWarnings("unchecked") 22 | public class RefreshToken implements JwtToken { 23 | private Jws claims; 24 | 25 | private RefreshToken(Jws claims) { 26 | this.claims = claims; 27 | } 28 | 29 | /** 30 | * Creates and validates Refresh token 31 | * 32 | * @param token 33 | * @param signingKey 34 | * 35 | * @throws BadCredentialsException 36 | * @throws JwtExpiredTokenException 37 | * 38 | * @return 39 | */ 40 | public static Optional create(RawAccessJwtToken token, String signingKey) { 41 | Jws claims = token.parseClaims(signingKey); 42 | 43 | List scopes = claims.getBody().get("scopes", List.class); 44 | if (scopes == null || scopes.isEmpty() 45 | || !scopes.stream().filter(scope -> Scopes.REFRESH_TOKEN.authority().equals(scope)).findFirst().isPresent()) { 46 | return Optional.empty(); 47 | } 48 | 49 | return Optional.of(new RefreshToken(claims)); 50 | } 51 | 52 | @Override 53 | public String getToken() { 54 | return null; 55 | } 56 | 57 | public Jws getClaims() { 58 | return claims; 59 | } 60 | 61 | public String getJti() { 62 | return claims.getBody().getId(); 63 | } 64 | 65 | public String getSubject() { 66 | return claims.getBody().getSubject(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/com/svlada/user/repository/UserRepository.java: -------------------------------------------------------------------------------- 1 | package com.svlada.user.repository; 2 | 3 | import java.util.Optional; 4 | 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | import org.springframework.data.jpa.repository.Query; 7 | import org.springframework.data.repository.query.Param; 8 | 9 | import com.svlada.entity.User; 10 | 11 | /** 12 | * UserRepository 13 | * 14 | * @author vladimir.stankovic 15 | * 16 | * Aug 16, 2016 17 | */ 18 | public interface UserRepository extends JpaRepository { 19 | @Query("select u from User u left join fetch u.roles r where u.username=:username") 20 | public Optional findByUsername(@Param("username") String username); 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/svlada/user/service/DatabaseUserService.java: -------------------------------------------------------------------------------- 1 | package com.svlada.user.service; 2 | 3 | import java.util.Optional; 4 | 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.stereotype.Service; 7 | 8 | import com.svlada.entity.User; 9 | import com.svlada.security.UserService; 10 | import com.svlada.user.repository.UserRepository; 11 | 12 | /** 13 | * Mock implementation. 14 | * 15 | * @author vladimir.stankovic 16 | * 17 | * Aug 4, 2016 18 | */ 19 | @Service 20 | public class DatabaseUserService implements UserService { 21 | private final UserRepository userRepository; 22 | 23 | @Autowired 24 | public DatabaseUserService(UserRepository userRepository) { 25 | this.userRepository = userRepository; 26 | } 27 | 28 | public UserRepository getUserRepository() { 29 | return userRepository; 30 | } 31 | 32 | @Override 33 | public Optional getByUsername(String username) { 34 | return this.userRepository.findByUsername(username); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server.port: 9966 2 | spring.profiles: default 3 | demo.security.jwt: 4 | tokenExpirationTime: 15 # Number of minutes 5 | refreshTokenExpTime: 60 # Minutes 6 | tokenIssuer: http://svlada.com 7 | tokenSigningKey: xm8EV6Hy5RMFK4EEACIDAwQus 8 | 9 | spring.datasource: 10 | url: "jdbc:h2:mem:testdb" 11 | driverClassName: org.h2.Driver 12 | username: sa 13 | password: "" 14 | data: "classpath*:data.sql" 15 | spring.jpa: 16 | database-platform: org.hibernate.dialect.H2Dialect 17 | spring.h2.console.enabled: true -------------------------------------------------------------------------------- /src/main/resources/data.sql: -------------------------------------------------------------------------------- 1 | insert into APP_USER(ID, PASSWORD, USERNAME) values(1, '$2a$10$bnC26zz//2cavYoSCrlHdecWF8tkGfPodlHcYwlACBBwJvcEf0p2G', 'svlada@gmail.com'); 2 | insert into USER_ROLE(APP_USER_ID, ROLE) values(1, 'ADMIN'); 3 | insert into USER_ROLE(APP_USER_ID, ROLE) values(1, 'PREMIUM_MEMBER'); -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/test/java/com/svlada/SpringbootSecurityJwtApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.svlada; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.test.context.junit4.SpringRunner; 7 | 8 | @RunWith(SpringRunner.class) 9 | @SpringBootTest 10 | public class SpringbootSecurityJwtApplicationTests { 11 | 12 | @Test 13 | public void contextLoads() { 14 | } 15 | 16 | } 17 | --------------------------------------------------------------------------------