Skip to content

Commit

Permalink
SMA-30: User Logout (#16)
Browse files Browse the repository at this point in the history
Co-authored-by: Ivana Machacikova <[email protected]>
  • Loading branch information
markpernia and ivamach authored Jan 30, 2024
1 parent 2f3772e commit 7228f5a
Show file tree
Hide file tree
Showing 15 changed files with 211 additions and 82 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.sportsmatch;

import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import com.sportsmatch.models.*;
import com.sportsmatch.repositories.*;
import lombok.AllArgsConstructor;
Expand All @@ -16,7 +15,6 @@

@AllArgsConstructor
@SpringBootApplication
@OpenAPIDefinition
@EnableWebMvc
public class SportsmatchApplication implements CommandLineRunner {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
import com.sportsmatch.models.User;
import com.sportsmatch.repositories.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;

@Service
@RequiredArgsConstructor
Expand All @@ -21,10 +23,13 @@ public class AuthService {

public void register(AuthRequestDTO authRequestDTO) {
User user = userMapper.registerToUser(authRequestDTO);
if (userRepository.existsByEmail(user.getEmail())) {
throw new ResponseStatusException(HttpStatus.CONFLICT);
}
userRepository.save(user);
}

public AuthResponseDTO authenticate(AuthRequestDTO authRequestDTO) {
public AuthResponseDTO login(AuthRequestDTO authRequestDTO) {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
authRequestDTO.getEmail(), authRequestDTO.getPassword()));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package com.sportsmatch.auth;

import com.sportsmatch.repositories.TokenRepository;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
Expand All @@ -14,14 +16,13 @@
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@Component
@RequiredArgsConstructor
public class JwtAuthFilter extends OncePerRequestFilter {

private final JwtService jwtService;
private final UserDetailsService userDetailsService;
private final TokenRepository tokenRepository;

@Override
protected void doFilterInternal(
Expand All @@ -30,31 +31,41 @@ protected void doFilterInternal(
@NonNull FilterChain filterChain)
throws ServletException, IOException {

final String authHeader = request.getHeader("Authorization"); // header that contains JWT Token
final String jwt;
final String userEmail;
final String authHeader = request.getHeader("Authorization");

if (authHeader == null
|| !authHeader.startsWith("Bearer ")) { // check header if contains JWT Token
if (isBearerTokenNotPresent(authHeader)) {
filterChain.doFilter(request, response);
return;
}

jwt = authHeader.substring(7); // takes JWT Token from header, index from "Bearer "
userEmail = jwtService.extractUserName(jwt); // takes userEmail from JWT Token
final String jwt = authHeader.substring(7);
final String userEmail = jwtService.extractUserName(jwt);

if (userEmail != null
&& SecurityContextHolder.getContext().getAuthentication()
== null) { // check if user is authenticated
if (isUserAuthenticated(userEmail)) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(userEmail);
if (jwtService.isTokenValid(jwt, userDetails)) {
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
if (jwtService.isTokenValid(jwt, userDetails) && !tokenRepository.existsByToken(jwt)) {
updateSecurityContext(request, userDetails);
}
}
filterChain.doFilter(request, response);
}

public boolean isBearerTokenNotPresent(String authHeader) {
if (authHeader == null) {
return true;
}
String[] tokenParts = authHeader.split(" ");
return !authHeader.startsWith("Bearer ") || tokenParts.length != 2;
}

public boolean isUserAuthenticated(String userEmail) {
return userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null;
}

public void updateSecurityContext(HttpServletRequest request, UserDetails userDetails) {
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,14 @@
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;

import javax.crypto.SecretKey;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import javax.crypto.SecretKey;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;

@Service
public class JwtService {
Expand All @@ -24,28 +23,21 @@ public String extractUserName(String token) {
return extractClaim(token, Claims::getSubject);
}

public <T> T extractClaim(
String token, Function<Claims, T> claimsResolver) { // Takes Single Claim from JWT Token
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}

public String generateToken(
UserDetails userDetails) { // Generates JWT Token with only userDetails
public String generateToken(UserDetails userDetails) {
return generateToken(new HashMap<>(), userDetails);
}

public String generateToken(
Map<String, Object> extraClaims,
UserDetails userDetails) { // Generates JWT Token with additional Claims
public String generateToken(Map<String, Object> extraClaims, UserDetails userDetails) {
return Jwts.builder()
.claims(extraClaims)
.subject(userDetails.getUsername())
.issuedAt(new Date(System.currentTimeMillis()))
.expiration(
new Date(
System.currentTimeMillis()
+ 1000 * 60 * 60 * 24)) // JWT Token valid 24h from time issued
.expiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24))
.signWith(getVerificationKey(), Jwts.SIG.HS256)
.compact();
}
Expand All @@ -63,15 +55,15 @@ private Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}

private Claims extractAllClaims(String token) { // Takes all the claims from JWT Token
private Claims extractAllClaims(String token) {
return Jwts.parser()
.verifyWith(getVerificationKey())
.build()
.parseSignedClaims(token)
.getPayload();
}

private SecretKey getVerificationKey() { // Sign JWT Token base from secret
private SecretKey getVerificationKey() {
byte[] keyBytes = Decoders.BASE64URL.decode(SECRET_KEY);
return Keys.hmacShaKeyFor(keyBytes);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.sportsmatch.auth;

import com.sportsmatch.models.Token;
import com.sportsmatch.models.TokenType;
import com.sportsmatch.models.User;
import com.sportsmatch.repositories.TokenRepository;
import com.sportsmatch.repositories.UserRepository;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class LogoutService implements LogoutHandler {

private final JwtAuthFilter authFilter;
private final TokenRepository tokenRepository;
private final UserRepository userRepository;
private final JwtService jwtService;

@Override
public void logout(
HttpServletRequest request, HttpServletResponse response, Authentication authentication) {

final String authHeader = request.getHeader("Authorization");
final String jwt = authHeader.substring(7);
final String userEmail = jwtService.extractUserName(jwt);
Optional<User> user = userRepository.findByEmail(userEmail);

if (authFilter.isBearerTokenNotPresent(authHeader) || user.isEmpty()) {
return;
}

tokenRepository.save(
Token.builder()
.token(jwt)
.user(user.get())
.tokenType(TokenType.BEARER)
.isValid(false)
.build());
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.sportsmatch.auth;

import static org.springframework.security.config.http.SessionCreationPolicy.STATELESS;

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
Expand All @@ -8,10 +10,10 @@
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import static org.springframework.security.config.http.SessionCreationPolicy.STATELESS;
import org.springframework.security.web.authentication.logout.LogoutHandler;

@Configuration
@EnableWebSecurity
Expand All @@ -20,24 +22,27 @@ public class SecurityConfig {

private static final String[] WHITE_LIST_URL = {
"/api/v1/auth/**", "/h2-console/**", "/v3/api-docs", "/v3/api-docs/**", "/swagger-ui/**"
// add endpoints that are not authenticated
};
private final JwtAuthFilter jwtAuthFilter;
private final AuthenticationProvider authenticationProvider;
private final LogoutHandler logoutHandler;

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http)
throws Exception { // set which endpoints are authenticated and not
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable)
.headers(
h ->
h.frameOptions(
HeadersConfigurer.FrameOptionsConfig::disable)) // remove in production
.headers(h -> h.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
.authorizeHttpRequests(
r -> r.requestMatchers(WHITE_LIST_URL).permitAll().anyRequest().authenticated())
.sessionManagement(s -> s.sessionCreationPolicy(STATELESS))
.authenticationProvider(authenticationProvider)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
.logout(
l ->
l.logoutUrl("/api/v1/auth/logout")
.addLogoutHandler(logoutHandler)
.logoutSuccessHandler(
((request, response, authentication) ->
SecurityContextHolder.clearContext())));
return http.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.sportsmatch.configs;

import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
import io.swagger.v3.oas.annotations.info.Contact;
import io.swagger.v3.oas.annotations.info.Info;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.security.SecurityScheme;
import io.swagger.v3.oas.annotations.servers.Server;
import org.springframework.context.annotation.Configuration;

@Configuration
@OpenAPIDefinition(
info =
@Info(
contact =
@Contact(
name = "Sportsmingle",
email = "[email protected]",
url = "sportsmingle.com"),
description = "Sport event scheduling and matchmaking",
title = "Sportsmingle",
version = "v1"),
servers = {@Server(description = "Local ENV", url = "http://localhost:8080")},
security = {@SecurityRequirement(name = "bearerAuth")})
@SecurityScheme(
name = "bearerAuth",
description = "JWT auth description",
scheme = "bearer",
type = SecuritySchemeType.HTTP,
bearerFormat = "JWT",
in = SecuritySchemeIn.HEADER)
public class OpenApiConfig {}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.sportsmatch.controllers;

import com.sportsmatch.models.User;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
Expand All @@ -11,6 +12,7 @@
public class APIController {

@GetMapping("/hello")
@Tag(name = "ex.secured endpoint")
public String hello(Authentication authentication) {
User user = (User) authentication.getPrincipal();
return "Welcome " + user.getName() + " to Secured Endpoint ";
Expand Down
Loading

0 comments on commit 7228f5a

Please sign in to comment.