6 Commits

Author SHA1 Message Date
Hanif Hakim
f430a2239a Refactor : More informative exception for Token Validation 2024-01-21 17:17:22 +07:00
Hanif Hakim
c607230f51 Rearrange project structure 2024-01-20 20:56:02 +07:00
Hanif Fauzi Hakim
0308a33594 Merge pull request #2 from hakimfauzi23/with-http-headers
Add Readme.md & update application.properties
2024-01-07 12:41:04 +07:00
Hanif
cdcfbc4a1d Add Readme.md & update application.properties 2024-01-07 12:40:00 +07:00
Hanif Fauzi Hakim
af945478b5 Merge pull request #1 from hakimfauzi23/with-http-headers
Change from Http Cookies to Http Headers JWT Mechanism
2024-01-04 13:35:17 +07:00
Hanif
94ce6a6424 Change from Http Cookies to Http Headers JWT Mechanism 2024-01-04 13:33:40 +07:00
34 changed files with 662 additions and 407 deletions

144
README.md Normal file
View File

@@ -0,0 +1,144 @@
# Spring Security REST API Boilerplate
___
## Overview
This is a boilerplate Spring Boot project designed to kickstart the development of RESTful APIs with built-in Spring Security for authentication and authorization.
## Features
- **Spring Boot:** Utilize the power of the Spring Boot framework for building robust and scalable applications.
- **Spring Security:** Implement secure authentication and authorization mechanisms to protect your REST API.
- **RESTful API:** Design and develop a clean and efficient RESTful API to handle your application's business logic.
- **Customizable:** Easily extend and customize the project to fit your specific requirements.
## Getting Started
1. Clone the repository: `git clone https://github.com/hakimfauzi23/boilerplate-spring-security.git`
2. Navigate to the project directory `cd boilerplate-spring-security`
3. Configure `src/main/resources/application.properties`
```properties
spring.datasource.url= jdbc:mysql://localhost:3306/testdb?useSSL=false
spring.datasource.username= root
spring.datasource.password=
spring.jpa.hibernate.ddl-auto= update
# App Properties
spring.app.jwtSecret= ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
spring.app.jwtExpirationMs= 60000
spring.app.jwtRefreshExpirationMs= 259200000
```
4. Create a new database that has a same name in `spring.datasource.url` properties
5. Build the application JAR File : `mvn clean package`
6. Run the application by running the JAR File : `java -jar target/boilerplate-spring-security-0.0.1-SNAPSHOT.jar`
7. Application is running, now you can experiment on the Authentication & Authorization in this project!
## Authentication Feature
The Authentication feature involves generating a JSON Web Token (JWT) for inclusion in the header of each API request. The feature encompasses three distinct endpoints: `api/auth/signup`, `api/auth/signin`, and `api/auth/refresh-token`. Below is a breakdown of each API endpoint:
___
### Sign Up Endpoint
This is for create new user credentials so the authentication login can be done with the user credential.
**API Endpoint:** `http://localhost:8080/api/auth/signup`
**Request:**
```json
{
"username":"user1",
"email":"user1@mail.com",
"password":"12345678",
"role": ["user"]
}
```
**Response:**
```json
{
"message": "User registered successfully!"
}
```
___
### Sign In Endpoint
This process involves generating an Access Token (JWT) by sending a request to the sign-in endpoint with the user credentials previously created through the sign-up endpoint.
**API Endpoint:** `http://localhost:8080/api/auth/signin`
**Request**
```json
{
"username" : "user1",
"password" : "12345678"
}
```
**Response**
```json
{
"refreshToken": "80b5f84f-c812-4efb-90a8-94893ec460a9",
"id": 2,
"username": "user1",
"email": "user1@mail.com",
"roles": [
"ROLE_USER"
],
"tokenType": "Bearer",
"accessToken": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOi........"
}
```
### Refresh Token Endpoint
The access token comes with an expiration time. In situations where the access token has expired, but the refresh token is still valid, the refresh token can be employed to generate a new access token.
**API Endpoint:** `http://localhost:8080/api/auth/signin`
**Request**
```json
{
"refreshToken" : "6c276542-4fdf-4d7c-ba2d-dbd42cc3cfe9"
}
```
**Response**
```json
{
"accessToken": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyMSIs............",
"refreshToken": "80b5f84f-c812-4efb-90a8-94893ec460a9",
"tokenType": "Bearer"
}
```
___
## Authorization Feature
After gaining an understanding of Authentication and learning how to generate the Access Token, the next step is to comprehend the Authorization feature. This feature is employed to filter the JWT Token based on whether it possesses a role that grants access to a specific endpoint. If the Authorization feature determines that the token lacks the requisite role, access will be denied.
Here's how to make the endpoint have the authorization, please use `@PreAuthorize("hasRole('__ROLE NAME__')")` annotation like below:
```java
@RestController
@RequestMapping("/api/test")
public class TestController {
@GetMapping("/all")
public String allAccess() {
return "Public Content.";
}
@GetMapping("/user")
@PreAuthorize("hasRole('USER') or hasRole('ADMIN')")
public String userAccess() {
return "User Content.";
}
@GetMapping("/admin")
@PreAuthorize("hasRole('ADMIN')")
public String adminAccess() {
return "Admin Board.";
}
}
```
This is how to use Access Token that generated in Authentication Feature, you can use the JWT token in the header `Authorization` with starts of `Bearer` then your Access Token.
```shell
curl -X GET http://localhost:8080/api/test/user \
-H "Authorization: Bearer __ACCESS TOKEN__" \
-H "Other-Header: Header-Value"
```

View File

@@ -1,8 +1,8 @@
package com.hakimfauzi23.boilerplatespringsecurity.modules.auth;
package com.hakimfauzi23.boilerplatespringsecurity;
import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.jwt.AuthEntryPointJwt;
import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.jwt.AuthTokenFilter;
import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.service.UserDetailsServiceImpl;
import com.hakimfauzi23.boilerplatespringsecurity.jwt.AuthEntryPointJwt;
import com.hakimfauzi23.boilerplatespringsecurity.jwt.AuthTokenFilter;
import com.hakimfauzi23.boilerplatespringsecurity.service.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

View File

@@ -0,0 +1,150 @@
package com.hakimfauzi23.boilerplatespringsecurity.controllers;
import com.hakimfauzi23.boilerplatespringsecurity.data.ERole;
import com.hakimfauzi23.boilerplatespringsecurity.data.RefreshToken;
import com.hakimfauzi23.boilerplatespringsecurity.data.Role;
import com.hakimfauzi23.boilerplatespringsecurity.data.User;
import com.hakimfauzi23.boilerplatespringsecurity.data.payload.request.LoginRequest;
import com.hakimfauzi23.boilerplatespringsecurity.data.payload.request.SignupRequest;
import com.hakimfauzi23.boilerplatespringsecurity.data.payload.request.TokenRefreshRequest;
import com.hakimfauzi23.boilerplatespringsecurity.data.payload.response.JwtResponse;
import com.hakimfauzi23.boilerplatespringsecurity.data.payload.response.MessageResponse;
import com.hakimfauzi23.boilerplatespringsecurity.data.payload.response.TokenRefreshResponse;
import com.hakimfauzi23.boilerplatespringsecurity.exception.exception.SignInException;
import com.hakimfauzi23.boilerplatespringsecurity.jwt.JwtUtils;
import com.hakimfauzi23.boilerplatespringsecurity.exception.exception.TokenRefreshException;
import com.hakimfauzi23.boilerplatespringsecurity.repository.RoleRepository;
import com.hakimfauzi23.boilerplatespringsecurity.repository.UserRepository;
import com.hakimfauzi23.boilerplatespringsecurity.service.RefreshTokenService;
import com.hakimfauzi23.boilerplatespringsecurity.service.UserDetailsImpl;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*;
import java.sql.SQLException;
import java.sql.SQLIntegrityConstraintViolationException;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@CrossOrigin(origins = "*", maxAge = 3600)
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
AuthenticationManager authenticationManager;
@Autowired
UserRepository userRepository;
@Autowired
RoleRepository roleRepository;
@Autowired
PasswordEncoder encoder;
@Autowired
JwtUtils jwtUtils;
@Autowired
RefreshTokenService refreshTokenService;
@PostMapping("/signup")
public ResponseEntity<?> registerUser(@Valid @RequestBody SignupRequest signUpRequest) {
if (userRepository.existsByUsername(signUpRequest.getUsername())) {
return ResponseEntity.badRequest().body(new MessageResponse("Error: Username is already taken!"));
}
if (userRepository.existsByEmail(signUpRequest.getEmail())) {
return ResponseEntity.badRequest().body(new MessageResponse("Error: Email is already in use!"));
}
User user = new User(signUpRequest.getUsername(),
signUpRequest.getEmail(),
encoder.encode(signUpRequest.getPassword()));
Set<String> strRoles = signUpRequest.getRole();
Set<Role> roles = new HashSet<>();
if (strRoles == null) {
Role userRole = roleRepository.findByName(ERole.ROLE_USER)
.orElseThrow(() -> new RuntimeException("Error: Role is not found."));
roles.add(userRole);
} else {
strRoles.forEach(role -> {
if (role.equals("admin")) {
Role adminRole = roleRepository.findByName(ERole.ROLE_ADMIN)
.orElseThrow(() -> new RuntimeException("Error: Role is not found."));
roles.add(adminRole);
} else {
Role userRole = roleRepository.findByName(ERole.ROLE_USER)
.orElseThrow(() -> new RuntimeException("Error: Role is not found."));
roles.add(userRole);
}
});
}
user.setRoles(roles);
userRepository.save(user);
return ResponseEntity.ok(new MessageResponse("User registered successfully!"));
}
@PostMapping("/signin")
public ResponseEntity<?> authenticateUser(@Valid @RequestBody LoginRequest loginRequest) {
try {
Authentication authentication = authenticationManager
.authenticate(new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()));
SecurityContextHolder.getContext().setAuthentication(authentication);
String jwt = jwtUtils.generateJwtToken(authentication);
UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();
List<String> roles = userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList());
RefreshToken refreshToken = refreshTokenService.createRefreshToken(userDetails.getId());
return ResponseEntity.ok(new JwtResponse(
jwt,
refreshToken.getToken(),
userDetails.getId(),
userDetails.getUsername(),
userDetails.getEmail(),
roles));
} catch (AuthenticationException exception) {
throw new SignInException(exception.getMessage());
}
}
@PostMapping("/refresh-token")
public ResponseEntity<?> refreshToken(@Valid @RequestBody TokenRefreshRequest request) {
String requestRefreshToken = request.getRefreshToken();
return refreshTokenService.findByToken(requestRefreshToken)
.map(refreshTokenService::verifyExpiration)
.map(RefreshToken::getUser)
.map(user -> {
String token = jwtUtils.generateTokenFromUsername(user.getUsername());
return ResponseEntity.ok(new TokenRefreshResponse(token, requestRefreshToken));
})
.orElseThrow(() -> new TokenRefreshException(requestRefreshToken,
"Refresh token is not in database!"));
}
}

View File

@@ -1,4 +1,4 @@
package com.hakimfauzi23.boilerplatespringsecurity.modules.auth.controllers;
package com.hakimfauzi23.boilerplatespringsecurity.controllers;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.CrossOrigin;

View File

@@ -0,0 +1,6 @@
package com.hakimfauzi23.boilerplatespringsecurity.data;
public enum ERole {
ROLE_USER,
ROLE_ADMIN
}

View File

@@ -1,4 +1,4 @@
package com.hakimfauzi23.boilerplatespringsecurity.modules.auth.data;
package com.hakimfauzi23.boilerplatespringsecurity.data;
import jakarta.persistence.*;

View File

@@ -1,4 +1,4 @@
package com.hakimfauzi23.boilerplatespringsecurity.modules.auth.data;
package com.hakimfauzi23.boilerplatespringsecurity.data;
import jakarta.persistence.*;

View File

@@ -1,4 +1,4 @@
package com.hakimfauzi23.boilerplatespringsecurity.modules.auth.data;
package com.hakimfauzi23.boilerplatespringsecurity.data;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotBlank;

View File

@@ -1,4 +1,4 @@
package com.hakimfauzi23.boilerplatespringsecurity.modules.auth.data.payload.request;
package com.hakimfauzi23.boilerplatespringsecurity.data.payload.request;
import jakarta.validation.constraints.NotBlank;

View File

@@ -1,4 +1,4 @@
package com.hakimfauzi23.boilerplatespringsecurity.modules.auth.data.payload.request;
package com.hakimfauzi23.boilerplatespringsecurity.data.payload.request;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;

View File

@@ -0,0 +1,18 @@
package com.hakimfauzi23.boilerplatespringsecurity.data.payload.request;
import jakarta.validation.constraints.NotBlank;
public class TokenRefreshRequest {
@NotBlank
private String refreshToken;
public String getRefreshToken() {
return refreshToken;
}
public void setRefreshToken(String refreshToken) {
this.refreshToken = refreshToken;
}
}

View File

@@ -0,0 +1,75 @@
package com.hakimfauzi23.boilerplatespringsecurity.data.payload.response;
import java.util.List;
public class JwtResponse {
private String token;
private String type = "Bearer";
private String refreshToken;
private Long id;
private String username;
private String email;
private List<String> roles;
public JwtResponse(String token, String refreshToken, Long id, String username, String email, List<String> roles) {
this.token = token;
this.refreshToken = refreshToken;
this.id = id;
this.username = username;
this.email = email;
this.roles = roles;
}
public String getAccessToken() {
return token;
}
public void setAccessToken(String accessToken) {
this.token = accessToken;
}
public String getRefreshToken() {
return refreshToken;
}
public void setRefreshToken(String refreshToken) {
this.refreshToken = refreshToken;
}
public String getTokenType() {
return type;
}
public void setTokenType(String tokenType) {
this.type = tokenType;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public List<String> getRoles() {
return roles;
}
}

View File

@@ -1,4 +1,4 @@
package com.hakimfauzi23.boilerplatespringsecurity.modules.auth.data.payload.response;
package com.hakimfauzi23.boilerplatespringsecurity.data.payload.response;
public class MessageResponse {
private String message;

View File

@@ -0,0 +1,37 @@
package com.hakimfauzi23.boilerplatespringsecurity.data.payload.response;
public class TokenRefreshResponse {
private String accessToken;
private String refreshToken;
private String tokenType = "Bearer";
public TokenRefreshResponse(String accessToken, String refreshToken) {
this.accessToken = accessToken;
this.refreshToken = refreshToken;
}
public String getAccessToken() {
return accessToken;
}
public void setAccessToken(String accessToken) {
this.accessToken = accessToken;
}
public String getRefreshToken() {
return refreshToken;
}
public void setRefreshToken(String refreshToken) {
this.refreshToken = refreshToken;
}
public String getTokenType() {
return tokenType;
}
public void setTokenType(String tokenType) {
this.tokenType = tokenType;
}
}

View File

@@ -0,0 +1,39 @@
package com.hakimfauzi23.boilerplatespringsecurity.exception.advice;
import com.hakimfauzi23.boilerplatespringsecurity.exception.exception.ErrorMessage;
import com.hakimfauzi23.boilerplatespringsecurity.exception.exception.SignInException;
import com.hakimfauzi23.boilerplatespringsecurity.exception.exception.TokenRefreshException;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import java.util.Date;
@RestControllerAdvice
public class AuthControllerAdvice {
@ExceptionHandler(value = SignInException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorMessage handleLoginException(SignInException ex, WebRequest request) {
return new ErrorMessage(
HttpStatus.BAD_REQUEST.value(),
new Date(),
ex.getMessage(),
request.getDescription(false)
);
}
@ExceptionHandler(value = TokenRefreshException.class)
@ResponseStatus(HttpStatus.FORBIDDEN)
public ErrorMessage handleTokenRefreshException(TokenRefreshException ex, WebRequest request) {
return new ErrorMessage(
HttpStatus.FORBIDDEN.value(),
new Date(),
ex.getMessage(),
request.getDescription(false)
);
}
}

View File

@@ -1,4 +1,4 @@
package com.hakimfauzi23.boilerplatespringsecurity.modules.auth.jwt.exception;
package com.hakimfauzi23.boilerplatespringsecurity.exception.exception;
import java.util.Date;

View File

@@ -0,0 +1,14 @@
package com.hakimfauzi23.boilerplatespringsecurity.exception.exception;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(HttpStatus.BAD_REQUEST)
public class SignInException extends RuntimeException {
private static final long serialVersionUID = 1L;
public SignInException(String message) {
super(message);
}
}

View File

@@ -1,4 +1,4 @@
package com.hakimfauzi23.boilerplatespringsecurity.modules.auth.jwt.exception;
package com.hakimfauzi23.boilerplatespringsecurity.exception.exception;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

View File

@@ -0,0 +1,13 @@
package com.hakimfauzi23.boilerplatespringsecurity.exception.exception;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public class TokenValidationException extends AuthenticationException {
public TokenValidationException(String message) {
super(message);
}
}

View File

@@ -1,17 +1,23 @@
package com.hakimfauzi23.boilerplatespringsecurity.modules.auth.jwt;
package com.hakimfauzi23.boilerplatespringsecurity.jwt;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.hakimfauzi23.boilerplatespringsecurity.exception.exception.ErrorMessage;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@@ -25,14 +31,22 @@ public class AuthEntryPointJwt implements AuthenticationEntryPoint {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
ErrorMessage body = new ErrorMessage(
HttpStatus.UNAUTHORIZED.value(),
new Date(),
"General Authentication Error (Invalid JWT, Unknown JWT Format), Please get new JWT Token!",
String.format("uri=%s", request.getRequestURI())
);
final Map<String, Object> body = new HashMap<>();
body.put("status", HttpServletResponse.SC_UNAUTHORIZED);
body.put("error", "Unauthorized");
body.put("message", authException.getMessage());
body.put("path", request.getServletPath());
final ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
mapper.setDateFormat(dateFormat);
mapper.writeValue(response.getOutputStream(), body);
}
}

View File

@@ -1,18 +1,28 @@
package com.hakimfauzi23.boilerplatespringsecurity.modules.auth.jwt;
package com.hakimfauzi23.boilerplatespringsecurity.jwt;
import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.service.UserDetailsServiceImpl;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.hakimfauzi23.boilerplatespringsecurity.exception.exception.ErrorMessage;
import com.hakimfauzi23.boilerplatespringsecurity.exception.exception.TokenValidationException;
import com.hakimfauzi23.boilerplatespringsecurity.service.UserDetailsServiceImpl;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class AuthTokenFilter extends OncePerRequestFilter {
@@ -27,6 +37,7 @@ public class AuthTokenFilter extends OncePerRequestFilter {
throws ServletException, IOException {
try {
String jwt = parseJwt(request);
if (jwt != null && jwtUtils.validateJwtToken(jwt)) {
String username = jwtUtils.getUserNameFromJwtToken(jwt);
@@ -41,15 +52,37 @@ public class AuthTokenFilter extends OncePerRequestFilter {
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception e) {
logger.error("Cannot set user authentication: {}", e);
} catch (TokenValidationException e) {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
ErrorMessage body = new ErrorMessage(
HttpStatus.UNAUTHORIZED.value(),
new Date(),
e.getMessage(),
String.format("uri=%s", request.getRequestURI())
);
final ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
mapper.setDateFormat(dateFormat);
mapper.writeValue(response.getOutputStream(), body);
return;
}
filterChain.doFilter(request, response);
}
private String parseJwt(HttpServletRequest request) {
String jwt = jwtUtils.getJwtFromCookies(request);
return jwt;
String headerAuth = request.getHeader("Authorization");
if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) {
return headerAuth.substring(7);
}
return null;
}
}

View File

@@ -0,0 +1,66 @@
package com.hakimfauzi23.boilerplatespringsecurity.jwt;
import com.hakimfauzi23.boilerplatespringsecurity.exception.exception.TokenValidationException;
import com.hakimfauzi23.boilerplatespringsecurity.service.UserDetailsImpl;
import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.SignatureException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
import java.security.Key;
import java.util.Date;
@Component
public class JwtUtils {
private static final Logger LOGGER = LoggerFactory.getLogger(JwtUtils.class);
@Value("${spring.app.jwtSecret}")
private String jwtSecret;
@Value("${spring.app.jwtExpirationMs}")
private int jwtExpirationMs;
public String generateJwtToken(Authentication authentication) {
UserDetailsImpl userPrincipal = (UserDetailsImpl) authentication.getPrincipal();
return generateTokenFromUsername(userPrincipal.getUsername());
}
private Key key() {
return Keys.hmacShaKeyFor(Decoders.BASE64.decode(jwtSecret));
}
public String generateTokenFromUsername(String username) {
return Jwts.builder()
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(new Date((new Date()).getTime() + jwtExpirationMs))
.signWith(SignatureAlgorithm.HS256, jwtSecret)
.compact();
}
public String getUserNameFromJwtToken(String token) {
return Jwts.parserBuilder().setSigningKey(key()).build()
.parseClaimsJws(token).getBody().getSubject();
}
public boolean validateJwtToken(String authToken) throws TokenValidationException {
try {
Jwts.parserBuilder().setSigningKey(key()).build().parse(authToken);
return true;
} catch (MalformedJwtException | ExpiredJwtException | UnsupportedJwtException | IllegalArgumentException |
SignatureException e) {
LOGGER.error("Invalid JWT token: {}", e.getMessage());
throw new TokenValidationException(e.getMessage());
}
}
}

View File

@@ -1,166 +0,0 @@
package com.hakimfauzi23.boilerplatespringsecurity.modules.auth.controllers;
import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.data.ERole;
import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.data.RefreshToken;
import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.data.Role;
import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.data.User;
import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.data.payload.request.LoginRequest;
import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.data.payload.request.SignupRequest;
import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.data.payload.response.MessageResponse;
import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.data.payload.response.UserInfoResponse;
import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.jwt.JwtUtils;
import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.jwt.exception.TokenRefreshException;
import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.repository.RoleRepository;
import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.repository.UserRepository;
import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.service.RefreshTokenService;
import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.service.UserDetailsImpl;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseCookie;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
@CrossOrigin(origins = "*", maxAge = 3600)
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
AuthenticationManager authenticationManager;
@Autowired
UserRepository userRepository;
@Autowired
RoleRepository roleRepository;
@Autowired
PasswordEncoder encoder;
@Autowired
JwtUtils jwtUtils;
@Autowired
RefreshTokenService refreshTokenService;
@PostMapping("/signup")
public ResponseEntity<?> registerUser(@Valid @RequestBody SignupRequest signUpRequest) {
if (userRepository.existsByUsername(signUpRequest.getUsername())) {
return ResponseEntity.badRequest().body(new MessageResponse("Error: Username is already taken!"));
}
if (userRepository.existsByEmail(signUpRequest.getEmail())) {
return ResponseEntity.badRequest().body(new MessageResponse("Error: Email is already in use!"));
}
User user = new User(signUpRequest.getUsername(),
signUpRequest.getEmail(),
encoder.encode(signUpRequest.getPassword()));
Set<String> strRoles = signUpRequest.getRole();
Set<Role> roles = new HashSet<>();
if (strRoles == null) {
Role userRole = roleRepository.findByName(ERole.ROLE_USER)
.orElseThrow(() -> new RuntimeException("Error: Role is not found."));
roles.add(userRole);
} else {
strRoles.forEach(role -> {
if (role.equals("admin")) {
Role adminRole = roleRepository.findByName(ERole.ROLE_ADMIN)
.orElseThrow(() -> new RuntimeException("Error: Role is not found."));
roles.add(adminRole);
} else {
Role userRole = roleRepository.findByName(ERole.ROLE_USER)
.orElseThrow(() -> new RuntimeException("Error: Role is not found."));
roles.add(userRole);
}
});
}
user.setRoles(roles);
userRepository.save(user);
return ResponseEntity.ok(new MessageResponse("User registered successfully!"));
}
@PostMapping("/signin")
public ResponseEntity<?> authenticateUser(@Valid @RequestBody LoginRequest loginRequest) {
Authentication authentication = authenticationManager
.authenticate(new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()));
SecurityContextHolder.getContext().setAuthentication(authentication);
UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();
ResponseCookie jwtCookie = jwtUtils.generateJwtCookie(userDetails);
List<String> roles = userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList());
RefreshToken refreshToken = refreshTokenService.createRefreshToken(userDetails.getId());
ResponseCookie jwtRefreshCookie = jwtUtils.generateRefreshJwtCookie(refreshToken.getToken());
return ResponseEntity.ok()
.header(HttpHeaders.SET_COOKIE, jwtCookie.toString())
.header(HttpHeaders.SET_COOKIE, jwtRefreshCookie.toString())
.body(new UserInfoResponse(userDetails.getId(), userDetails.getUsername(), userDetails.getEmail(), roles));
}
@PostMapping("/signout")
public ResponseEntity<?> logoutUser() {
Object principle = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (!Objects.equals(principle.toString(), "anonymousUser")) {
Long userId = ((UserDetailsImpl) principle).getId();
refreshTokenService.deleteByUserId(userId);
}
ResponseCookie jwtCookie = jwtUtils.getCleanJwtCookie();
ResponseCookie jwtRefreshCookie = jwtUtils.getCleanJwtRefreshCookie();
return ResponseEntity.ok()
.header(HttpHeaders.SET_COOKIE, jwtCookie.toString())
.header(HttpHeaders.SET_COOKIE, jwtRefreshCookie.toString())
.body(new MessageResponse("You've been signed out!"));
}
@PostMapping("/refresh-token")
public ResponseEntity<?> refreshToken(HttpServletRequest request) {
String refreshToken = jwtUtils.getJwtRefreshFromCookies(request);
if ((refreshToken != null) && (refreshToken.length() > 0)) {
return refreshTokenService.findByToken(refreshToken)
.map(refreshTokenService::verifyExpiration)
.map(RefreshToken::getUser)
.map(user -> {
ResponseCookie jwtCookie = jwtUtils.generateJwtCookie(user);
return ResponseEntity.ok()
.header(HttpHeaders.SET_COOKIE, jwtCookie.toString())
.body(new MessageResponse("Token is refreshed successfully!"));
})
.orElseThrow(() -> new TokenRefreshException(refreshToken,
"Refresh token is not in database!"));
}
return ResponseEntity.badRequest().body(new MessageResponse("Refresh Token is empty!"));
}
}

View File

@@ -1,6 +0,0 @@
package com.hakimfauzi23.boilerplatespringsecurity.modules.auth.data;
public enum ERole {
ROLE_USER,
ROLE_ADMIN
}

View File

@@ -1,45 +0,0 @@
package com.hakimfauzi23.boilerplatespringsecurity.modules.auth.data.payload.response;
import java.util.List;
public class UserInfoResponse {
private Long id;
private String username;
private String email;
private List<String> roles;
public UserInfoResponse(Long id, String username, String email, List<String> roles) {
this.id = id;
this.username = username;
this.email = email;
this.roles = roles;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public List<String> getRoles() {
return roles;
}
}

View File

@@ -1,115 +0,0 @@
package com.hakimfauzi23.boilerplatespringsecurity.modules.auth.jwt;
import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.data.User;
import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.service.UserDetailsImpl;
import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseCookie;
import org.springframework.stereotype.Component;
import org.springframework.web.util.WebUtils;
import java.security.Key;
import java.util.Date;
@Component
public class JwtUtils {
private static final Logger LOGGER = LoggerFactory.getLogger(JwtUtils.class);
@Value("${spring.app.jwtSecret}")
private String jwtSecret;
@Value("${spring.app.jwtExpirationMs}")
private int jwtExpirationMs;
@Value("${spring.app.jwtCookieName}")
private String jwtCookie;
@Value("${spring.app.jwtRefreshCookieName}")
private String jwtRefreshCookie;
public String getJwtFromCookies(HttpServletRequest request) {
return getCookieValueByName(request, jwtCookie);
}
public String getJwtRefreshFromCookies(HttpServletRequest request) {
return getCookieValueByName(request, jwtRefreshCookie);
}
public ResponseCookie getCleanJwtCookie() {
return ResponseCookie.from(jwtCookie, null).path("/api").build();
}
public ResponseCookie getCleanJwtRefreshCookie() {
return ResponseCookie.from(jwtRefreshCookie, null).path("/api/auth/refresh-token").build();
}
public ResponseCookie generateJwtCookie(UserDetailsImpl userDetails) {
String jwt = generateTokenFromUsername(userDetails.getUsername());
return generateCookie(jwtCookie, jwt, "/api");
}
public ResponseCookie generateJwtCookie(User user) {
String jwt = generateTokenFromUsername(user.getUsername());
return generateCookie(jwtCookie, jwt, "/api");
}
public ResponseCookie generateRefreshJwtCookie(String refreshToken) {
return generateCookie(jwtRefreshCookie, refreshToken, "/api/auth/refresh-token");
}
public String getUserNameFromJwtToken(String token) {
return Jwts.parserBuilder().setSigningKey(key()).build()
.parseClaimsJws(token).getBody().getSubject();
}
private Key key() {
return Keys.hmacShaKeyFor(Decoders.BASE64.decode(jwtSecret));
}
public boolean validateJwtToken(String authToken) {
try {
Jwts.parserBuilder().setSigningKey(key()).build().parse(authToken);
return true;
} catch (MalformedJwtException e) {
LOGGER.error("Invalid JWT token: {}", e.getMessage());
} catch (ExpiredJwtException e) {
LOGGER.error("JWT token is expired: {}", e.getMessage());
} catch (UnsupportedJwtException e) {
LOGGER.error("JWT token is unsupported: {}", e.getMessage());
} catch (IllegalArgumentException e) {
LOGGER.error("JWT claims string is empty: {}", e.getMessage());
}
return false;
}
public String generateTokenFromUsername(String username) {
return Jwts.builder()
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(new Date((new Date()).getTime() + jwtExpirationMs))
.signWith(key(), SignatureAlgorithm.HS256)
.compact();
}
private ResponseCookie generateCookie(String name, String value, String path) {
return ResponseCookie.from(name, value).path(path).maxAge(24 * 60 * 60).httpOnly(true).build();
}
private String getCookieValueByName(HttpServletRequest request, String name) {
Cookie cookie = WebUtils.getCookie(request, name);
if (cookie != null) {
return cookie.getValue();
} else {
return null;
}
}
}

View File

@@ -1,26 +0,0 @@
package com.hakimfauzi23.boilerplatespringsecurity.modules.auth.jwt.advice;
import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.jwt.exception.ErrorMessage;
import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.jwt.exception.TokenRefreshException;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import java.util.Date;
@RestControllerAdvice
public class TokenControllerAdvice {
@ExceptionHandler(value = TokenRefreshException.class)
@ResponseStatus(HttpStatus.FORBIDDEN)
public ErrorMessage handleTokenRefreshException(TokenRefreshException ex, WebRequest request) {
return new ErrorMessage(
HttpStatus.FORBIDDEN.value(),
new Date(),
ex.getMessage(),
request.getDescription(false)
);
}
}

View File

@@ -1,7 +1,7 @@
package com.hakimfauzi23.boilerplatespringsecurity.modules.auth.repository;
package com.hakimfauzi23.boilerplatespringsecurity.repository;
import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.data.RefreshToken;
import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.data.User;
import com.hakimfauzi23.boilerplatespringsecurity.data.RefreshToken;
import com.hakimfauzi23.boilerplatespringsecurity.data.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.stereotype.Repository;
@@ -13,6 +13,9 @@ public interface RefreshTokenRepository extends JpaRepository<RefreshToken, Long
Optional<RefreshToken> findByToken(String token);
Optional<RefreshToken> findByUserId(Long userId);
@Modifying
int deleteByUser(User user);
}

View File

@@ -1,7 +1,7 @@
package com.hakimfauzi23.boilerplatespringsecurity.modules.auth.repository;
package com.hakimfauzi23.boilerplatespringsecurity.repository;
import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.data.ERole;
import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.data.Role;
import com.hakimfauzi23.boilerplatespringsecurity.data.ERole;
import com.hakimfauzi23.boilerplatespringsecurity.data.Role;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

View File

@@ -1,6 +1,6 @@
package com.hakimfauzi23.boilerplatespringsecurity.modules.auth.repository;
package com.hakimfauzi23.boilerplatespringsecurity.repository;
import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.data.User;
import com.hakimfauzi23.boilerplatespringsecurity.data.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

View File

@@ -1,9 +1,10 @@
package com.hakimfauzi23.boilerplatespringsecurity.modules.auth.service;
package com.hakimfauzi23.boilerplatespringsecurity.service;
import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.data.RefreshToken;
import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.jwt.exception.TokenRefreshException;
import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.repository.RefreshTokenRepository;
import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.repository.UserRepository;
import com.hakimfauzi23.boilerplatespringsecurity.data.RefreshToken;
import com.hakimfauzi23.boilerplatespringsecurity.exception.exception.SignInException;
import com.hakimfauzi23.boilerplatespringsecurity.exception.exception.TokenRefreshException;
import com.hakimfauzi23.boilerplatespringsecurity.repository.RefreshTokenRepository;
import com.hakimfauzi23.boilerplatespringsecurity.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
@@ -30,8 +31,10 @@ public class RefreshTokenService {
}
public RefreshToken createRefreshToken(Long userId) {
RefreshToken refreshToken = new RefreshToken();
Optional<RefreshToken> isAlreadyExist = refreshTokenRepository.findByUserId(userId);
isAlreadyExist.ifPresent(refreshToken -> refreshTokenRepository.delete(refreshToken));
RefreshToken refreshToken = new RefreshToken();
refreshToken.setUser(userRepository.findById(userId).get());
refreshToken.setExpirationDate(Instant.now().plusMillis(refreshTokenDurationMs));
refreshToken.setToken(UUID.randomUUID().toString());

View File

@@ -1,7 +1,7 @@
package com.hakimfauzi23.boilerplatespringsecurity.modules.auth.service;
package com.hakimfauzi23.boilerplatespringsecurity.service;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.data.User;
import com.hakimfauzi23.boilerplatespringsecurity.data.User;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

View File

@@ -1,7 +1,7 @@
package com.hakimfauzi23.boilerplatespringsecurity.modules.auth.service;
package com.hakimfauzi23.boilerplatespringsecurity.service;
import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.data.User;
import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.repository.UserRepository;
import com.hakimfauzi23.boilerplatespringsecurity.data.User;
import com.hakimfauzi23.boilerplatespringsecurity.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;

View File

@@ -1,11 +1,9 @@
spring.datasource.url= jdbc:mysql://localhost:3306/testdb?useSSL=false
spring.datasource.username= root
spring.datasource.username=root
spring.datasource.password=
spring.jpa.hibernate.ddl-auto= update
# App Properties
spring.app.jwtCookieName= cookie-jwt-for-security
spring.app.jwtRefreshCookieName= cookie-refresh-jwt-for-security
spring.app.jwtSecret= 9sL3p2mGzN7oR4Dx8QcY1uKwF5BhVtX6EaJgU3iZqOyMlIbCnAeHrWfPd0
spring.app.jwtExpirationMs= 86400000
spring.app.jwtSecret= ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
spring.app.jwtExpirationMs= 60000
spring.app.jwtRefreshExpirationMs= 259200000