Skip to content

Commit

Permalink
Merge pull request #17 from szymonpoltorak/logout
Browse files Browse the repository at this point in the history
Logout
  • Loading branch information
szymonpoltorak authored Apr 26, 2023
2 parents 5e94aab + 633dd9d commit a7b8581
Show file tree
Hide file tree
Showing 33 changed files with 283 additions and 44 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import razepl.dev.socialappbackend.auth.apicalls.*;
import razepl.dev.socialappbackend.auth.interfaces.AuthInterface;
import razepl.dev.socialappbackend.auth.interfaces.AuthServiceInterface;
import razepl.dev.socialappbackend.exceptions.validators.NullChecker;

import static razepl.dev.socialappbackend.auth.constants.AuthMappings.*;
import static razepl.dev.socialappbackend.constants.GlobalConstants.FRONTEND_ADDRESS;

/**
* Class to control auth endpoints.
Expand All @@ -22,7 +24,6 @@
@RequiredArgsConstructor
@RestController
@RequestMapping(value = AUTH_MAPPING)
@CrossOrigin(origins = FRONTEND_ADDRESS)
public class AuthController implements AuthInterface {
private final AuthServiceInterface authService;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package razepl.dev.socialappbackend.auth;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.stereotype.Service;
import razepl.dev.socialappbackend.auth.jwt.JwtToken;
import razepl.dev.socialappbackend.auth.jwt.interfaces.TokenRepository;

import static razepl.dev.socialappbackend.config.constants.Headers.*;

/**
* Service class for logging user out.
*/
@Service
@RequiredArgsConstructor
public class LogoutService implements LogoutHandler {
private final TokenRepository tokenRepository;

@Override
public final void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
String authHeader = request.getHeader(AUTH_HEADER);

if (authHeader == null || !authHeader.startsWith(TOKEN_HEADER)) {
return;
}
String jwt = authHeader.substring(TOKEN_START_INDEX);
JwtToken token = tokenRepository.findByToken(jwt).orElse(null);

if (token == null) {
return;
}
token.setExpired(true);
token.setRevoked(true);
tokenRepository.save(token);

SecurityContextHolder.clearContext();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,12 @@ public interface AuthInterface {
*/
ResponseEntity<AuthResponse> refreshUserToken(HttpServletRequest request, HttpServletResponse response);

/**
* Authenticates a user with the given token request.
*
* @param request the token request
* @return a ResponseEntity with a TokenResponse as the response body
*/
ResponseEntity<TokenResponse> authenticateUser(TokenRequest request);

}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ public interface AuthServiceInterface {
*/
AuthResponse refreshToken(HttpServletRequest request, HttpServletResponse response);

/**
* Validates the user's tokens using the given token request.
*
* @param request the token request containing the user's tokens
* @return a token response containing information about the validity of the tokens
*/
TokenResponse validateUsersTokens(TokenRequest request);

}

Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import razepl.dev.socialappbackend.exceptions.validators.NullChecker;
import razepl.dev.socialappbackend.auth.apicalls.AuthResponse;
import razepl.dev.socialappbackend.auth.jwt.interfaces.TokenManager;
import razepl.dev.socialappbackend.auth.jwt.interfaces.TokenRepository;
import razepl.dev.socialappbackend.config.interfaces.JwtServiceInterface;
import razepl.dev.socialappbackend.exceptions.validators.NullChecker;
import razepl.dev.socialappbackend.user.User;

import java.util.List;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ public interface TokenRepository extends JpaRepository<JwtToken, Long> {
* @return List of {@link JwtToken} of the user
*/
@Query("""
select t from JwtToken as t
inner join User as u on (t.user.userId = u.userId)
where u.userId = :id and (t.isExpired = false or t.isRevoked = false)
""")
select t from JwtToken as t
inner join User as u on (t.user.userId = u.userId)
where u.userId = :id and (t.isExpired = false or t.isRevoked = false)
""")
List<JwtToken> findAllValidTokensByUserId(Long id);
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,18 @@
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import razepl.dev.socialappbackend.config.interfaces.AppConfigInterface;
import razepl.dev.socialappbackend.exceptions.AuthManagerInstanceException;
import razepl.dev.socialappbackend.user.interfaces.UserRepository;

import java.util.List;

import static razepl.dev.socialappbackend.config.constants.CorsConfig.*;
import static razepl.dev.socialappbackend.config.constants.Headers.AUTH_HEADER;

/**
* Class made to provide necessary Beans for Spring app.
* It implements {@link AppConfigInterface}.
Expand All @@ -31,6 +39,21 @@ public UserDetailsService userDetailsService() {
.orElseThrow(() -> new UsernameNotFoundException("User not found!"));
}

@Bean
@Override
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();

configuration.setAllowedOrigins(FRONTEND_ADDRESS);
configuration.setAllowedMethods(ALLOWED_REQUESTS);
configuration.setAllowedHeaders(List.of(AUTH_HEADER, CONTENT_TYPE_HEADER));

source.registerCorsConfiguration(API_PATTERN, configuration);

return source;
}

@Bean
@Override
public AuthenticationProvider authenticationProvider() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import razepl.dev.socialappbackend.config.interfaces.SecurityConfigInterface;
import razepl.dev.socialappbackend.exceptions.SecurityChainException;

import static razepl.dev.socialappbackend.config.constants.Headers.LOGOUT_URL;
import static razepl.dev.socialappbackend.config.constants.Headers.WHITE_LIST;

/**
Expand All @@ -24,24 +27,32 @@
public class SecurityConfiguration implements SecurityConfigInterface {
private final AuthenticationProvider authenticationProvider;
private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final LogoutHandler logoutHandler;

@Bean
@Override
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) {
try {
httpSecurity.csrf()
httpSecurity
.csrf()
.disable()
.authorizeHttpRequests()
.requestMatchers(WHITE_LIST)
.permitAll()
.anyRequest()
.authenticated()
.and()
.cors()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authenticationProvider(authenticationProvider)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.logout()
.logoutUrl(LOGOUT_URL)
.addLogoutHandler(logoutHandler)
.logoutSuccessHandler(((request, response, authentication) -> SecurityContextHolder.clearContext()));

return httpSecurity.build();
} catch (Exception exception) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package razepl.dev.socialappbackend.config.constants;

import java.util.List;

/**
* Configuration for Cross-Origin Resource Sharing (CORS) settings.
*/
public final class CorsConfig {

/**
* List of allowed HTTP request methods.
*/
public static final List<String> ALLOWED_REQUESTS = List.of("GET", "POST", "PUT", "DELETE", "OPTIONS");

/**
* List of allowed frontend server addresses.
*/
public static final List<String> FRONTEND_ADDRESS = List.of("http://localhost:4200");

/**
* HTTP header for specifying the content type of request or response.
*/
public static final String CONTENT_TYPE_HEADER = "Content-Type";

/**
* API pattern for CORS configuration.
*/
public static final String API_PATTERN = "/api/**";

/**
* Private constructor to prevent instantiation of this class.
*/
private CorsConfig() {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ public final class Headers {
public static final String[] WHITE_LIST = {AUTH_MATCHERS, SWAGGER_JSON,
SWAGGER_JSON_MATCHERS, SWAGGER_UI, SWAGGER_UI_MATCHERS};

/**
* The URL for logging out.
*/
public static final String LOGOUT_URL = "/api/auth/logout";

/**
* Private constructor to prevent instantiation of this class.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.cors.CorsConfigurationSource;
import razepl.dev.socialappbackend.exceptions.AuthManagerInstanceException;

/**
Expand Down Expand Up @@ -40,4 +41,11 @@ public interface AppConfigInterface {
* @throws AuthManagerInstanceException if there is an error instantiating the AuthenticationManager.
*/
AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws AuthManagerInstanceException;

/**
* Returns the cors config bean.
*
* @return CorsConfigurationSource
*/
CorsConfigurationSource corsConfigurationSource();
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import io.jsonwebtoken.Claims;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.lang.NonNull;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Map;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,6 @@
* A utility class that contains global constants for the application.
*/
public final class GlobalConstants {
/**
* The address of the frontend server.
*/
public static final String FRONTEND_ADDRESS = "http://localhost:4200";

/**
* The name of the user database table.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package razepl.dev.socialappbackend.exceptions;

public class NullArgumentException extends IllegalArgumentException{
public class NullArgumentException extends IllegalArgumentException {
public NullArgumentException(String message) {
super(message);
}
Expand Down
15 changes: 12 additions & 3 deletions social-app-frontend/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { HttpClientModule } from "@angular/common/http";
import { HTTP_INTERCEPTORS, HttpClientModule } from "@angular/common/http";
import { AuthInterceptor } from "./core/interceptors/auth.interceptor";


@NgModule({
Expand All @@ -17,8 +18,16 @@ import { HttpClientModule } from "@angular/common/http";
BrowserAnimationsModule,
HttpClientModule
],
providers: [],
bootstrap: [AppComponent]
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: AuthInterceptor,
multi: true
}
],
bootstrap: [
AppComponent
]
})
export class AppModule {
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Router } from "@angular/router";
import { UserService } from "../services/user.service";
import { LocalStorageService } from "../services/local-storage.service";
import { RoutePaths } from "../enums/RoutePaths";
import { StorageKeys } from "../enums/StorageKeys";

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
Expand All @@ -14,6 +15,14 @@ export class AuthInterceptor implements HttpInterceptor {
}

intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
const header: string = `${ this.localStorageService.getValueFromStorage(StorageKeys.AUTH_TOKEN) }`;

request = request.clone({
setHeaders: {
"Authorization": `Bearer ${ header.substring(1, header.length - 1) }`
}
});

return next.handle(request).pipe(
catchError((error: any) => {
if (error.status === 401) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { RegisterRequest } from "../data/register-request";
import { LoginRequest } from "../data/login-request";
import { RegisterRequest } from "../../data/register-request";
import { LoginRequest } from "../../data/login-request";
import { Observable } from "rxjs";
import { AuthResponse } from "../data/auth-response";
import { TokenResponse } from "../data/token-response";
import { AuthResponse } from "../../data/auth-response";
import { TokenResponse } from "../../data/token-response";

/**
* The interface for Auth Service.
Expand Down
Loading

0 comments on commit a7b8581

Please sign in to comment.