RefreshToken refresh() method/endpoint and logic.
This commit is contained in:
parent
2eb2610832
commit
b17dddfca9
@ -1,5 +1,6 @@
|
|||||||
package app.mealsmadeeasy.api.auth;
|
package app.mealsmadeeasy.api.auth;
|
||||||
|
|
||||||
|
import app.mealsmadeeasy.api.security.AuthToken;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseCookie;
|
import org.springframework.http.ResponseCookie;
|
||||||
@ -10,6 +11,17 @@ import org.springframework.web.bind.annotation.*;
|
|||||||
@RequestMapping("/auth")
|
@RequestMapping("/auth")
|
||||||
public final class AuthController {
|
public final class AuthController {
|
||||||
|
|
||||||
|
private static ResponseCookie getRefreshTokenCookie(String token, long maxAge) {
|
||||||
|
final ResponseCookie.ResponseCookieBuilder b = ResponseCookie.from("refresh-token")
|
||||||
|
.httpOnly(true)
|
||||||
|
.secure(true)
|
||||||
|
.maxAge(maxAge);
|
||||||
|
if (token != null) {
|
||||||
|
b.value(token);
|
||||||
|
}
|
||||||
|
return b.build();
|
||||||
|
}
|
||||||
|
|
||||||
private final AuthService authService;
|
private final AuthService authService;
|
||||||
|
|
||||||
public AuthController(AuthService authService) {
|
public AuthController(AuthService authService) {
|
||||||
@ -20,13 +32,12 @@ public final class AuthController {
|
|||||||
public ResponseEntity<LoginView> login(@RequestBody LoginBody loginBody) {
|
public ResponseEntity<LoginView> login(@RequestBody LoginBody loginBody) {
|
||||||
try {
|
try {
|
||||||
final LoginDetails loginDetails = this.authService.login(loginBody.getUsername(), loginBody.getPassword());
|
final LoginDetails loginDetails = this.authService.login(loginBody.getUsername(), loginBody.getPassword());
|
||||||
final String serializedToken = loginDetails.getRefreshToken().getToken();
|
final AuthToken refreshToken = loginDetails.getRefreshToken();
|
||||||
final ResponseCookie refreshCookie = ResponseCookie.from("refresh-token", serializedToken)
|
final ResponseCookie refreshCookie = getRefreshTokenCookie(
|
||||||
.httpOnly(true)
|
refreshToken.getToken(),
|
||||||
.secure(true)
|
refreshToken.getLifetime()
|
||||||
.maxAge(loginDetails.getRefreshToken().getLifetime())
|
);
|
||||||
.build();
|
final var loginView = new LoginView(
|
||||||
final LoginView loginView = new LoginView(
|
|
||||||
loginDetails.getUsername(), loginDetails.getAccessToken().getToken()
|
loginDetails.getUsername(), loginDetails.getAccessToken().getToken()
|
||||||
);
|
);
|
||||||
return ResponseEntity.ok()
|
return ResponseEntity.ok()
|
||||||
@ -37,13 +48,32 @@ public final class AuthController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/refresh")
|
||||||
|
public ResponseEntity<LoginView> refresh(
|
||||||
|
@CookieValue(value = "refresh-token") String oldRefreshToken
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
final LoginDetails loginDetails = this.authService.refresh(oldRefreshToken);
|
||||||
|
final AuthToken newRefreshToken = loginDetails.getRefreshToken();
|
||||||
|
final ResponseCookie refreshCookie = getRefreshTokenCookie(
|
||||||
|
newRefreshToken.getToken(),
|
||||||
|
newRefreshToken.getLifetime()
|
||||||
|
);
|
||||||
|
final var loginView = new LoginView(loginDetails.getUsername(), loginDetails.getAccessToken().getToken());
|
||||||
|
return ResponseEntity.ok()
|
||||||
|
.header(HttpHeaders.SET_COOKIE, refreshCookie.toString())
|
||||||
|
.body(loginView);
|
||||||
|
} catch (LoginException loginException) {
|
||||||
|
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/logout")
|
@PostMapping("/logout")
|
||||||
public ResponseEntity<?> logout(@CookieValue("refresh-token") String refreshToken) {
|
public ResponseEntity<?> logout(@CookieValue(value = "refresh-token", required = false) String refreshToken) {
|
||||||
final ResponseCookie deleteRefreshCookie = ResponseCookie.from("refresh-token")
|
if (refreshToken != null) {
|
||||||
.httpOnly(true)
|
this.authService.logout(refreshToken);
|
||||||
.secure(true)
|
}
|
||||||
.maxAge(0)
|
final ResponseCookie deleteRefreshCookie = getRefreshTokenCookie(null, 0);
|
||||||
.build();
|
|
||||||
return ResponseEntity.ok()
|
return ResponseEntity.ok()
|
||||||
.header(HttpHeaders.SET_COOKIE, deleteRefreshCookie.toString())
|
.header(HttpHeaders.SET_COOKIE, deleteRefreshCookie.toString())
|
||||||
.build();
|
.build();
|
||||||
|
@ -2,4 +2,6 @@ package app.mealsmadeeasy.api.auth;
|
|||||||
|
|
||||||
public interface AuthService {
|
public interface AuthService {
|
||||||
LoginDetails login(String username, String password) throws LoginException;
|
LoginDetails login(String username, String password) throws LoginException;
|
||||||
|
void logout(String refreshToken);
|
||||||
|
LoginDetails refresh(String refreshToken) throws LoginException;
|
||||||
}
|
}
|
||||||
|
@ -1,34 +1,91 @@
|
|||||||
package app.mealsmadeeasy.api.auth;
|
package app.mealsmadeeasy.api.auth;
|
||||||
|
|
||||||
import app.mealsmadeeasy.api.security.JwtService;
|
import app.mealsmadeeasy.api.jwt.JwtService;
|
||||||
|
import app.mealsmadeeasy.api.user.UserEntity;
|
||||||
|
import io.jsonwebtoken.JwtException;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.security.authentication.AuthenticationManager;
|
import org.springframework.security.authentication.AuthenticationManager;
|
||||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.AuthenticationException;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public final class AuthServiceImpl implements AuthService {
|
public final class AuthServiceImpl implements AuthService {
|
||||||
|
|
||||||
private final AuthenticationManager authenticationManager;
|
private final AuthenticationManager authenticationManager;
|
||||||
private final JwtService jwtService;
|
private final JwtService jwtService;
|
||||||
|
private final RefreshTokenRepository refreshTokenRepository;
|
||||||
|
private final long refreshTokenLifetime;
|
||||||
|
|
||||||
public AuthServiceImpl(AuthenticationManager authenticationManager, JwtService jwtService) {
|
public AuthServiceImpl(
|
||||||
|
AuthenticationManager authenticationManager,
|
||||||
|
JwtService jwtService,
|
||||||
|
RefreshTokenRepository refreshTokenRepository,
|
||||||
|
@Value("${app.mealsmadeeasy.api.security.refresh-token-lifetime}") Long refreshTokenLifetime
|
||||||
|
) {
|
||||||
this.authenticationManager = authenticationManager;
|
this.authenticationManager = authenticationManager;
|
||||||
this.jwtService = jwtService;
|
this.jwtService = jwtService;
|
||||||
|
this.refreshTokenRepository = refreshTokenRepository;
|
||||||
|
this.refreshTokenLifetime = refreshTokenLifetime;
|
||||||
|
}
|
||||||
|
|
||||||
|
private RefreshToken createRefreshToken(UserEntity principal) {
|
||||||
|
final RefreshTokenEntity refreshTokenDraft = new RefreshTokenEntity();
|
||||||
|
refreshTokenDraft.setToken(UUID.randomUUID().toString());
|
||||||
|
refreshTokenDraft.setIssued(LocalDateTime.now());
|
||||||
|
refreshTokenDraft.setExpiration(LocalDateTime.now().plusSeconds(this.refreshTokenLifetime));
|
||||||
|
refreshTokenDraft.setOwner(principal);
|
||||||
|
return this.refreshTokenRepository.save(refreshTokenDraft);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public LoginDetails login(String username, String password) throws LoginException {
|
public LoginDetails login(String username, String password) throws LoginException {
|
||||||
try {
|
try {
|
||||||
this.authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(
|
final Authentication authentication = this.authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(
|
||||||
username,
|
username,
|
||||||
password
|
password
|
||||||
));
|
));
|
||||||
|
final UserEntity principal = (UserEntity) authentication.getPrincipal();
|
||||||
return new LoginDetails(
|
return new LoginDetails(
|
||||||
username,
|
username,
|
||||||
this.jwtService.generateAccessToken(username),
|
this.jwtService.generateAccessToken(username),
|
||||||
this.jwtService.generateRefreshToken(username)
|
this.createRefreshToken(principal)
|
||||||
);
|
);
|
||||||
} catch (Exception e) {
|
} catch (AuthenticationException e) {
|
||||||
|
throw new LoginException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void logout(String refreshToken) {
|
||||||
|
this.refreshTokenRepository.findByToken(refreshToken).ifPresent(this.refreshTokenRepository::delete);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LoginDetails refresh(String refreshToken) throws LoginException {
|
||||||
|
try {
|
||||||
|
final RefreshTokenEntity old = this.refreshTokenRepository.findByToken(refreshToken)
|
||||||
|
.orElseThrow(() -> new LoginException("No such refresh-token: " + refreshToken));
|
||||||
|
if (old.isRevoked()) {
|
||||||
|
throw new LoginException("RefreshToken is revoked.");
|
||||||
|
}
|
||||||
|
if (old.getExpiration().isBefore(LocalDateTime.now())) {
|
||||||
|
throw new LoginException("RefreshToken is expired.");
|
||||||
|
}
|
||||||
|
final UserEntity principal = old.getOwner();
|
||||||
|
this.refreshTokenRepository.delete(old);
|
||||||
|
|
||||||
|
final String username = principal.getUsername();
|
||||||
|
return new LoginDetails(
|
||||||
|
username,
|
||||||
|
this.jwtService.generateAccessToken(username),
|
||||||
|
this.createRefreshToken(principal)
|
||||||
|
);
|
||||||
|
} catch (JwtException e) {
|
||||||
throw new LoginException(e);
|
throw new LoginException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
11
src/main/java/app/mealsmadeeasy/api/auth/RefreshToken.java
Normal file
11
src/main/java/app/mealsmadeeasy/api/auth/RefreshToken.java
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package app.mealsmadeeasy.api.auth;
|
||||||
|
|
||||||
|
import app.mealsmadeeasy.api.security.AuthToken;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
public interface RefreshToken extends AuthToken {
|
||||||
|
LocalDateTime getIssued();
|
||||||
|
LocalDateTime getExpiration();
|
||||||
|
boolean isRevoked();
|
||||||
|
}
|
@ -0,0 +1,78 @@
|
|||||||
|
package app.mealsmadeeasy.api.auth;
|
||||||
|
|
||||||
|
import app.mealsmadeeasy.api.user.UserEntity;
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
|
|
||||||
|
@Entity(name = "RefreshToken")
|
||||||
|
public class RefreshTokenEntity implements RefreshToken {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@Column(unique = true, nullable = false)
|
||||||
|
private String token;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private LocalDateTime issued;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private LocalDateTime expiration;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private Boolean revoked = false;
|
||||||
|
|
||||||
|
@JoinColumn(nullable = false)
|
||||||
|
@ManyToOne
|
||||||
|
private UserEntity owner;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getToken() {
|
||||||
|
return this.token;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setToken(String token) {
|
||||||
|
this.token = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LocalDateTime getIssued() {
|
||||||
|
return this.issued;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIssued(LocalDateTime issued) {
|
||||||
|
this.issued = issued;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LocalDateTime getExpiration() {
|
||||||
|
return this.expiration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setExpiration(LocalDateTime expiration) {
|
||||||
|
this.expiration = expiration;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isRevoked() {
|
||||||
|
return this.revoked;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRevoked(Boolean revoked) {
|
||||||
|
this.revoked = revoked;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserEntity getOwner() {
|
||||||
|
return this.owner;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOwner(UserEntity owner) {
|
||||||
|
this.owner = owner;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getLifetime() {
|
||||||
|
return ChronoUnit.SECONDS.between(this.issued, this.expiration);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
package app.mealsmadeeasy.api.auth;
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public interface RefreshTokenRepository extends JpaRepository<RefreshTokenEntity, String> {
|
||||||
|
Optional<RefreshTokenEntity> findByToken(String token);
|
||||||
|
}
|
9
src/main/java/app/mealsmadeeasy/api/jwt/JwtService.java
Normal file
9
src/main/java/app/mealsmadeeasy/api/jwt/JwtService.java
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package app.mealsmadeeasy.api.jwt;
|
||||||
|
|
||||||
|
import app.mealsmadeeasy.api.security.AuthToken;
|
||||||
|
import io.jsonwebtoken.JwtException;
|
||||||
|
|
||||||
|
public interface JwtService {
|
||||||
|
AuthToken generateAccessToken(String username);
|
||||||
|
String getSubject(String token) throws JwtException;
|
||||||
|
}
|
@ -1,6 +1,9 @@
|
|||||||
package app.mealsmadeeasy.api.security;
|
package app.mealsmadeeasy.api.jwt;
|
||||||
|
|
||||||
|
import app.mealsmadeeasy.api.security.AuthToken;
|
||||||
|
import app.mealsmadeeasy.api.security.SimpleAuthToken;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import io.jsonwebtoken.JwtException;
|
||||||
import io.jsonwebtoken.Jwts;
|
import io.jsonwebtoken.Jwts;
|
||||||
import io.jsonwebtoken.io.Serializer;
|
import io.jsonwebtoken.io.Serializer;
|
||||||
import io.jsonwebtoken.jackson.io.JacksonSerializer;
|
import io.jsonwebtoken.jackson.io.JacksonSerializer;
|
||||||
@ -17,18 +20,15 @@ public final class JwtServiceImpl implements JwtService {
|
|||||||
|
|
||||||
private final Serializer<Map<String, ?>> serializer;
|
private final Serializer<Map<String, ?>> serializer;
|
||||||
private final long accessTokenLifetime;
|
private final long accessTokenLifetime;
|
||||||
private final long refreshTokenLifetime;
|
|
||||||
private final SecretKey secretKey;
|
private final SecretKey secretKey;
|
||||||
|
|
||||||
public JwtServiceImpl(
|
public JwtServiceImpl(
|
||||||
ObjectMapper objectMapper,
|
ObjectMapper objectMapper,
|
||||||
@Value("${app.mealsmadeeasy.api.security.access-token-lifetime}") Long accessTokenLifetime,
|
@Value("${app.mealsmadeeasy.api.security.access-token-lifetime}") Long accessTokenLifetime,
|
||||||
@Value("${app.mealsmadeeasy.api.security.refresh-token-lifetime}") Long refreshTokenLifetime,
|
|
||||||
SecretKey secretKey
|
SecretKey secretKey
|
||||||
) {
|
) {
|
||||||
this.serializer = new JacksonSerializer<>();
|
this.serializer = new JacksonSerializer<>();
|
||||||
this.accessTokenLifetime = accessTokenLifetime;
|
this.accessTokenLifetime = accessTokenLifetime;
|
||||||
this.refreshTokenLifetime = refreshTokenLifetime;
|
|
||||||
this.secretKey = secretKey;
|
this.secretKey = secretKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,20 +42,16 @@ public final class JwtServiceImpl implements JwtService {
|
|||||||
.signWith(this.secretKey)
|
.signWith(this.secretKey)
|
||||||
.json(this.serializer)
|
.json(this.serializer)
|
||||||
.compact();
|
.compact();
|
||||||
return new AuthToken(token, this.accessTokenLifetime);
|
return new SimpleAuthToken(token, this.accessTokenLifetime);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AuthToken generateRefreshToken(String username) {
|
public String getSubject(String token) throws JwtException {
|
||||||
final Instant now = Instant.now();
|
final var jws = Jwts.parser()
|
||||||
final String token = Jwts.builder()
|
.verifyWith(this.secretKey)
|
||||||
.subject(username)
|
.build()
|
||||||
.issuedAt(Date.from(now))
|
.parseSignedClaims(token);
|
||||||
.expiration(Date.from(now.plusSeconds(this.refreshTokenLifetime)))
|
return jws.getPayload().getSubject();
|
||||||
.signWith(this.secretKey)
|
|
||||||
.json(this.serializer)
|
|
||||||
.compact();
|
|
||||||
return new AuthToken(token, this.refreshTokenLifetime);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -1,21 +1,6 @@
|
|||||||
package app.mealsmadeeasy.api.security;
|
package app.mealsmadeeasy.api.security;
|
||||||
|
|
||||||
public final class AuthToken {
|
public interface AuthToken {
|
||||||
|
String getToken();
|
||||||
private final String token;
|
long getLifetime();
|
||||||
private final long lifetime;
|
|
||||||
|
|
||||||
public AuthToken(String token, long lifetime) {
|
|
||||||
this.token = token;
|
|
||||||
this.lifetime = lifetime;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getToken() {
|
|
||||||
return this.token;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getLifetime() {
|
|
||||||
return this.lifetime;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,29 +1,32 @@
|
|||||||
package app.mealsmadeeasy.api.security;
|
package app.mealsmadeeasy.api.security;
|
||||||
|
|
||||||
import io.jsonwebtoken.Jwts;
|
import app.mealsmadeeasy.api.jwt.JwtService;
|
||||||
import jakarta.servlet.FilterChain;
|
import jakarta.servlet.FilterChain;
|
||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.filter.OncePerRequestFilter;
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
|
|
||||||
import javax.crypto.SecretKey;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
|
@Lazy
|
||||||
|
@Component
|
||||||
public final class JwtFilter extends OncePerRequestFilter {
|
public final class JwtFilter extends OncePerRequestFilter {
|
||||||
|
|
||||||
private final SecretKey secretKey;
|
|
||||||
private final UserDetailsService userDetailsService;
|
private final UserDetailsService userDetailsService;
|
||||||
|
private final JwtService jwtService;
|
||||||
|
|
||||||
public JwtFilter(SecretKey secretKey, UserDetailsService userDetailsService) {
|
public JwtFilter(UserDetailsService userDetailsService, JwtService jwtService) {
|
||||||
this.secretKey = secretKey;
|
|
||||||
this.userDetailsService = userDetailsService;
|
this.userDetailsService = userDetailsService;
|
||||||
|
this.jwtService = jwtService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -38,11 +41,7 @@ public final class JwtFilter extends OncePerRequestFilter {
|
|||||||
if (authorizationHeader.startsWith("Bearer ")
|
if (authorizationHeader.startsWith("Bearer ")
|
||||||
&& authorizationHeader.length() > 7) {
|
&& authorizationHeader.length() > 7) {
|
||||||
final String token = authorizationHeader.substring(7);
|
final String token = authorizationHeader.substring(7);
|
||||||
final var jws = Jwts.parser()
|
final String username = this.jwtService.getSubject(token);
|
||||||
.verifyWith(this.secretKey)
|
|
||||||
.build()
|
|
||||||
.parseSignedClaims(token);
|
|
||||||
final String username = jws.getPayload().getSubject();
|
|
||||||
final UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
|
final UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
|
||||||
final var authenticationToken = new UsernamePasswordAuthenticationToken(
|
final var authenticationToken = new UsernamePasswordAuthenticationToken(
|
||||||
userDetails,
|
userDetails,
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
package app.mealsmadeeasy.api.security;
|
|
||||||
|
|
||||||
public interface JwtService {
|
|
||||||
AuthToken generateAccessToken(String username);
|
|
||||||
AuthToken generateRefreshToken(String username);
|
|
||||||
}
|
|
@ -1,6 +1,7 @@
|
|||||||
package app.mealsmadeeasy.api.security;
|
package app.mealsmadeeasy.api.security;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import org.springframework.beans.factory.BeanFactory;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.security.authentication.AuthenticationManager;
|
import org.springframework.security.authentication.AuthenticationManager;
|
||||||
@ -12,29 +13,26 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe
|
|||||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
|
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
|
||||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
|
||||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.security.web.SecurityFilterChain;
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||||
|
|
||||||
import javax.crypto.SecretKey;
|
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableWebSecurity
|
@EnableWebSecurity
|
||||||
public class SecurityConfiguration {
|
public class SecurityConfiguration {
|
||||||
|
|
||||||
private final SecretKey secretKey;
|
|
||||||
private final JpaUserDetailsService jpaUserDetailsService;
|
private final JpaUserDetailsService jpaUserDetailsService;
|
||||||
|
private final BeanFactory beanFactory;
|
||||||
|
|
||||||
public SecurityConfiguration(SecretKey secretKey, JpaUserDetailsService jpaUserDetailsService) {
|
public SecurityConfiguration(JpaUserDetailsService jpaUserDetailsService, BeanFactory beanFactory) {
|
||||||
this.secretKey = secretKey;
|
|
||||||
this.jpaUserDetailsService = jpaUserDetailsService;
|
this.jpaUserDetailsService = jpaUserDetailsService;
|
||||||
|
this.beanFactory = beanFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public WebSecurityCustomizer webSecurityCustomizer() {
|
public WebSecurityCustomizer webSecurityCustomizer() {
|
||||||
return web -> web.ignoring().requestMatchers("/greeting", "/auth/login", "/auth/logout");
|
return web -> web.ignoring().requestMatchers("/greeting", "/auth/**");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@ -51,17 +49,12 @@ public class SecurityConfiguration {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
httpSecurity.addFilterBefore(
|
httpSecurity.addFilterBefore(
|
||||||
new JwtFilter(this.secretKey, this.jpaUserDetailsService),
|
this.beanFactory.getBean(JwtFilter.class),
|
||||||
UsernamePasswordAuthenticationFilter.class
|
UsernamePasswordAuthenticationFilter.class
|
||||||
);
|
);
|
||||||
return httpSecurity.build();
|
return httpSecurity.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
|
||||||
public UserDetailsService userDetailsService() {
|
|
||||||
return this.jpaUserDetailsService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public PasswordEncoder passwordEncoder() {
|
public PasswordEncoder passwordEncoder() {
|
||||||
return new BCryptPasswordEncoder(10);
|
return new BCryptPasswordEncoder(10);
|
||||||
@ -70,7 +63,7 @@ public class SecurityConfiguration {
|
|||||||
@Bean
|
@Bean
|
||||||
public DaoAuthenticationProvider daoAuthenticationProvider() {
|
public DaoAuthenticationProvider daoAuthenticationProvider() {
|
||||||
final var provider = new DaoAuthenticationProvider();
|
final var provider = new DaoAuthenticationProvider();
|
||||||
provider.setUserDetailsService(this.userDetailsService());
|
provider.setUserDetailsService(this.jpaUserDetailsService);
|
||||||
provider.setPasswordEncoder(this.passwordEncoder());
|
provider.setPasswordEncoder(this.passwordEncoder());
|
||||||
return provider;
|
return provider;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,23 @@
|
|||||||
|
package app.mealsmadeeasy.api.security;
|
||||||
|
|
||||||
|
public final class SimpleAuthToken implements AuthToken {
|
||||||
|
|
||||||
|
private final String token;
|
||||||
|
private final long lifetime;
|
||||||
|
|
||||||
|
public SimpleAuthToken(String token, long lifetime) {
|
||||||
|
this.token = token;
|
||||||
|
this.lifetime = lifetime;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getToken() {
|
||||||
|
return this.token;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getLifetime() {
|
||||||
|
return this.lifetime;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,89 @@
|
|||||||
|
package app.mealsmadeeasy.api.auth;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import jakarta.servlet.http.Cookie;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
|
import org.springframework.test.web.servlet.MvcResult;
|
||||||
|
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.hamcrest.Matchers.is;
|
||||||
|
import static org.hamcrest.Matchers.not;
|
||||||
|
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||||
|
|
||||||
|
@SpringBootTest
|
||||||
|
@AutoConfigureMockMvc
|
||||||
|
public class AuthControllerTests {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private MockMvc mockMvc;
|
||||||
|
|
||||||
|
private MockHttpServletRequestBuilder getLoginRequest() throws Exception {
|
||||||
|
final Map<String, ?> body = Map.of(
|
||||||
|
"username", "test",
|
||||||
|
"password", "test"
|
||||||
|
);
|
||||||
|
return post("/auth/login")
|
||||||
|
.content(this.objectMapper.writeValueAsString(body))
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.with(user("test").password("test"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void simpleLogin() throws Exception {
|
||||||
|
this.mockMvc.perform(this.getLoginRequest())
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.username").value("test"))
|
||||||
|
.andExpect(jsonPath("$.accessToken").isString())
|
||||||
|
.andExpect(cookie().exists("refresh-token"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Cookie getRefreshTokenCookie() throws Exception {
|
||||||
|
final MvcResult loginResult = this.mockMvc.perform(this.getLoginRequest()).andReturn();
|
||||||
|
final Cookie refreshTokenCookie = loginResult.getResponse().getCookie("refresh-token");
|
||||||
|
if (refreshTokenCookie == null) {
|
||||||
|
throw new NullPointerException("refreshTokenCookie is null");
|
||||||
|
}
|
||||||
|
return refreshTokenCookie;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void simpleLogout() throws Exception {
|
||||||
|
final MockHttpServletRequestBuilder req = post("/auth/logout")
|
||||||
|
.cookie(this.getRefreshTokenCookie());
|
||||||
|
this.mockMvc.perform(req)
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(cookie().maxAge("refresh-token", 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void simpleRefresh() throws Exception {
|
||||||
|
final Cookie firstRefreshTokenCookie = this.getRefreshTokenCookie();
|
||||||
|
final MockHttpServletRequestBuilder req = post("/auth/refresh")
|
||||||
|
.cookie(firstRefreshTokenCookie);
|
||||||
|
final MvcResult res = this.mockMvc.perform(req)
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.username").value("test"))
|
||||||
|
.andExpect(jsonPath("$.accessToken").isString())
|
||||||
|
.andExpect(cookie().exists("refresh-token"))
|
||||||
|
.andReturn();
|
||||||
|
final Cookie secondRefreshTokenCookie = res.getResponse().getCookie("refresh-token");
|
||||||
|
if (secondRefreshTokenCookie == null) {
|
||||||
|
throw new NullPointerException("secondRefreshTokenCookie is null");
|
||||||
|
}
|
||||||
|
assertThat(firstRefreshTokenCookie.getValue(), is(not(secondRefreshTokenCookie.getValue())));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user