2 Commits

Author SHA1 Message Date
Hanif
cdcfbc4a1d Add Readme.md & update application.properties 2024-01-07 12:40:00 +07:00
Hanif
94ce6a6424 Change from Http Cookies to Http Headers JWT Mechanism 2024-01-04 13:33:40 +07:00
8 changed files with 282 additions and 116 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

@@ -6,19 +6,18 @@ 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.request.TokenRefreshRequest;
import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.data.payload.response.JwtResponse;
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.data.payload.response.TokenRefreshResponse;
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;
@@ -30,7 +29,6 @@ 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;
@@ -106,61 +104,37 @@ public class AuthController {
.authenticate(new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()));
SecurityContextHolder.getContext().setAuthentication(authentication);
String jwt = jwtUtils.generateJwtToken(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!"));
return ResponseEntity.ok(new JwtResponse(
jwt,
refreshToken.getToken(),
userDetails.getId(),
userDetails.getUsername(),
userDetails.getEmail(),
roles));
}
@PostMapping("/refresh-token")
public ResponseEntity<?> refreshToken(HttpServletRequest request) {
String refreshToken = jwtUtils.getJwtRefreshFromCookies(request);
public ResponseEntity<?> refreshToken(@Valid @RequestBody TokenRefreshRequest request) {
String requestRefreshToken = request.getRefreshToken();
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!"));
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

@@ -0,0 +1,18 @@
package com.hakimfauzi23.boilerplatespringsecurity.modules.auth.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

@@ -2,19 +2,49 @@ package com.hakimfauzi23.boilerplatespringsecurity.modules.auth.data.payload.res
import java.util.List;
public class UserInfoResponse {
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 UserInfoResponse(Long id, String username, String email, 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;
}

View File

@@ -0,0 +1,37 @@
package com.hakimfauzi23.boilerplatespringsecurity.modules.auth.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

@@ -10,6 +10,7 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticatio
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;
@@ -49,7 +50,12 @@ public class AuthTokenFilter extends OncePerRequestFilter {
}
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

@@ -11,6 +11,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseCookie;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
import org.springframework.web.util.WebUtils;
@@ -28,52 +29,31 @@ public class JwtUtils {
@Value("${spring.app.jwtExpirationMs}")
private int jwtExpirationMs;
@Value("${spring.app.jwtCookieName}")
private String jwtCookie;
public String generateJwtToken(Authentication authentication) {
@Value("${spring.app.jwtRefreshCookieName}")
private String jwtRefreshCookie;
UserDetailsImpl userPrincipal = (UserDetailsImpl) authentication.getPrincipal();
return generateTokenFromUsername(userPrincipal.getUsername());
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 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) {
try {
Jwts.parserBuilder().setSigningKey(key()).build().parse(authToken);
@@ -91,25 +71,4 @@ public class JwtUtils {
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

@@ -4,8 +4,6 @@ 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