Skip to content

Commit

Permalink
Feat(AUTH-CPC-740): User Can Logout (#576)
Browse files Browse the repository at this point in the history
JIRA: link to jira ticket: 
https://champlainsaintlambert.atlassian.net/browse/CPC-740
## Context:
The logout functionality was incorrectly implemented, and it failed to
remove the user account cookie upon logout.
## Changes
Implemented a functional logout feature that clears the user account
session cookie and removes associated local storage keys (e.g., email,
roles, username, UUID).
- Added logout methods in ApiGateway to facilitate logouts.
- Enhanced the Frontend logout function to include the removal of the
Bearer account session cookie.
- Developed three comprehensive test cases to ensure the reliability of
the new logout functionality.

## Before and After UI (Required for UI-impacting PRs)
### Before

![Logout_Cookie](https://github.com/cgerard321/champlain_petclinic/assets/77691550/0886d925-5232-43e3-b42c-aeef46ec7b65)

![logout_UnremovedCookie](https://github.com/cgerard321/champlain_petclinic/assets/77691550/c1a86078-da75-4abc-924a-6ba946fa9b73)

### After

![Logout_Cookie](https://github.com/cgerard321/champlain_petclinic/assets/77691550/8d324ca3-25f5-4ff5-bef7-5f76a9324f6c)

![Logout_Success](https://github.com/cgerard321/champlain_petclinic/assets/77691550/78e3ffa4-da0f-4828-81a1-f4edcdd14132)

![Logout_CookieRemoved](https://github.com/cgerard321/champlain_petclinic/assets/77691550/cd1ede7d-0eba-4138-8b77-a8e50874e595)

![logout_localStorage](https://github.com/cgerard321/champlain_petclinic/assets/77691550/155c2048-e2e5-426b-8572-0ef9c05b4cdc)

## Dev notes (Optional)
None
## Linked pull requests (Optional)
None

---------

Co-authored-by: Dylan Brassard <[email protected]>
  • Loading branch information
PaulJ2001 and DylanBrass authored Oct 23, 2023
1 parent b8d05b4 commit 0ec7ee4
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,16 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.time.Duration;
import java.util.List;
import java.util.UUID;

import static org.springframework.http.HttpStatus.BAD_REQUEST;
Expand Down Expand Up @@ -242,6 +246,26 @@ public Mono<ResponseEntity<UserPasswordLessDTO>> login(final Mono<Login> login)
}
}

public Mono<ResponseEntity<Void>> logout(ServerHttpRequest request, ServerHttpResponse response) {
log.info("Entered AuthServiceClient logout method");
List<HttpCookie> cookies = request.getCookies().get("Bearer");
if (cookies != null && !cookies.isEmpty()) {
ResponseCookie cookie = ResponseCookie.from("Bearer", "")
.httpOnly(true)
.secure(true)
.path("/api/gateway")
.domain("localhost")
.maxAge(Duration.ofSeconds(0))
.sameSite("Lax").build();
response.addCookie(cookie);
log.info("Logout Success: Account session ended");
return Mono.just(ResponseEntity.noContent().build());
} else {
log.warn("Logout Error: Problem removing account cookies, Session may have expired, redirecting to login page");
return Mono.just(ResponseEntity.status(HttpStatus.UNAUTHORIZED).build());
}
}


public Mono<ResponseEntity<Void>> sendForgottenEmail(Mono<UserEmailRequestDTO> emailRequestDTOMono) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.Resource;
import org.springframework.http.*;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
Expand Down Expand Up @@ -777,6 +779,12 @@ public Mono<ResponseEntity<UserPasswordLessDTO>> login(@RequestBody Mono<Login>

}

@SecuredEndpoint(allowedRoles = {Roles.ANONYMOUS})
@PostMapping("/users/logout")
public Mono<ResponseEntity<Void>> logout(ServerHttpRequest request, ServerHttpResponse response) {
return authServiceClient.logout(request, response);
}


@SecuredEndpoint(allowedRoles = {Roles.ANONYMOUS})
@PostMapping(value = "/users/forgot_password")
Expand Down
29 changes: 22 additions & 7 deletions api-gateway/src/main/resources/static/scripts/fragments/nav.html
Original file line number Diff line number Diff line change
@@ -1,10 +1,25 @@
<script>
function purgeUser() {
localStorage.removeItem("username")
localStorage.removeItem("email")
localStorage.removeItem("UUID")
localStorage.removeItem("roles")
}
function logoutUser() {
logoutUser.$inject = ['$http'];
var $http = angular.injector(['ng']).get('$http');
$http({
method: 'POST',
url: '/api/gateway/users/logout'
}).then(function successCallback(response) {
alert("Logout successful")
localStorage.removeItem("username")
localStorage.removeItem("email")
localStorage.removeItem("UUID")
localStorage.removeItem("roles")
window.location.href = '/#!/login';
}, function errorCallback(response) {
alert("Logout Error: Session may have expired. Redirecting to login page")
localStorage.removeItem("username")
localStorage.removeItem("email")
localStorage.removeItem("UUID")
localStorage.removeItem("roles")
window.location.href = '/#!/login';
}); }
</script>

<nav class="navbar navbar-expand-lg navbar-light bg-light">
Expand Down Expand Up @@ -49,7 +64,7 @@
<a class="nav-link" ui-sref="AdminPanel" >Admin-Panel</a>
</li>
<li class="nav-item">
<a class="nav-link" onclick="purgeUser()" style="cursor:pointer" ui-sref="login">Logout</a>
<a class="nav-link" onclick="logoutUser()" style="cursor:pointer">Logout</a>
</li>
</ul>
<ul class="navbar-nav mr-auto justify-content-end" ng-if="!isLoggedIn">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@
import org.junit.jupiter.api.*;
import org.mockito.Mockito;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.HttpCookie;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.http.server.reactive.MockServerHttpResponse;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
Expand Down Expand Up @@ -331,7 +333,33 @@ void ShouldLoginUser_ShouldReturnOk() throws Exception {
.verifyComplete();
}

@Test
@DisplayName("Should logout a user")
void shouldLogoutUser_shouldReturnNoContent() throws Exception {
final MockResponse loginMockResponse = new MockResponse()
.setHeader("Content-Type", "application/json")
.setResponseCode(200);
server.enqueue(loginMockResponse);
ServerHttpRequest loginRequest = MockServerHttpRequest.post("/users/login").build();
Login login = Login.builder()
.email("email")
.password("password")
.build();
final Mono<ResponseEntity<UserPasswordLessDTO>> validatedTokenResponse = authServiceClient.login(Mono.just(login));
ServerHttpRequest logoutRequest = MockServerHttpRequest.post("/users/logout")
.cookie(new HttpCookie("Bearer", "some_valid_token"))
.build();
MockServerHttpResponse logoutMockResponse = new MockServerHttpResponse();

final Mono<ResponseEntity<Void>> logoutResponse = authServiceClient.logout(logoutRequest, logoutMockResponse);

StepVerifier.create(logoutResponse)
.consumeNextWith(responseEntity -> {
// Verify the HTTP status code directly
assertEquals(HttpStatus.NO_CONTENT, responseEntity.getStatusCode());
})
.verifyComplete();
}

@Test
@DisplayName("Should send a forgotten email")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import org.springframework.core.io.Resource;
import org.springframework.http.*;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
Expand All @@ -60,6 +61,7 @@
import javax.print.attribute.standard.Media;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
Expand Down Expand Up @@ -2926,6 +2928,35 @@ void login_invalid() throws Exception {
}


@Test
@DisplayName("Should Logout with a Valid Session, Clearing Bearer Cookie, and Returning 204")
void logout_shouldClearBearerCookie() {
MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
headers.add(HttpHeaders.COOKIE, "Bearer=some.token.value; Path=/; HttpOnly; SameSite=Lax");
when(authServiceClient.logout(any(ServerHttpRequest.class), any(ServerHttpResponse.class)))
.thenReturn(Mono.just(ResponseEntity.noContent().build()));
client.post()
.uri("/api/gateway/users/logout")
.headers(httpHeaders -> httpHeaders.putAll(headers))
.exchange()
.expectStatus().isNoContent()
.expectHeader().doesNotExist(HttpHeaders.SET_COOKIE);
}

@Test
@DisplayName("Given Expired Session, Logout Should Return 401")
void logout_shouldReturnUnauthorizedForExpiredSession() {
MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
when(authServiceClient.logout(any(ServerHttpRequest.class), any(ServerHttpResponse.class)))
.thenReturn(Mono.just(ResponseEntity.status(HttpStatus.UNAUTHORIZED).build()));
client.post()
.uri("/api/gateway/users/logout")
.headers(httpHeaders -> httpHeaders.putAll(headers))
.exchange()
.expectStatus().isUnauthorized()
.expectHeader().doesNotExist(HttpHeaders.SET_COOKIE);
}


private InventoryResponseDTO buildInventoryDTO(){
return InventoryResponseDTO.builder()
Expand Down Expand Up @@ -3306,6 +3337,9 @@ void testAddProductToInventory_InvalidInventoryId_ShouldReturnNotFoundException(






private ProductResponseDTO buildProductDTO(){
return ProductResponseDTO.builder()
.id("1")
Expand Down

0 comments on commit 0ec7ee4

Please sign in to comment.