Skip to content

Commit

Permalink
Merge branch 'release/5.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
mohamed-taman committed May 10, 2023
2 parents 845b049 + e0ef4af commit 9cf5fed
Show file tree
Hide file tree
Showing 90 changed files with 1,501 additions and 1,152 deletions.
2 changes: 1 addition & 1 deletion README.md

Large diffs are not rendered by default.

Binary file modified docs/images/SystemAPI.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
23 changes: 14 additions & 9 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.0-M2</version>
<version>3.1.0-RC2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>

<groupId>org.siriusxi.htec</groupId>
<groupId>org.siriusxi.fa</groupId>
<artifactId>flight-advisor</artifactId>
<version>3.0</version>
<version>5.0</version>
<name>Flight Advisor API</name>
<description>Flight Advisor API Spring Boot based Application.</description>
<packaging>jar</packaging>
Expand Down Expand Up @@ -58,18 +58,18 @@
</licenses>

<properties>
<maven.compiler.release>20</maven.compiler.release>
<maven.compiler.source>20</maven.compiler.source>
<maven.compiler.target>20</maven.compiler.target>
<java.version>20</java.version>
<maven.compiler.release>${java.version}</maven.compiler.release>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>

<!-- Dependencies versions properties -->
<maven.surefire.plugin.version>3.0.0</maven.surefire.plugin.version>
<maven.failsafe.plugin.version>3.0.0</maven.failsafe.plugin.version>
<io.jsonwebtoken.version>0.11.5</io.jsonwebtoken.version>
<org.mapstruct.version>1.5.3.Final</org.mapstruct.version>
<org.mapstruct.version>1.5.4.Final</org.mapstruct.version>
<org.springdoc.version>2.1.0</org.springdoc.version>
<opencsv.version>5.7.1</opencsv.version>
<hipster-core.version>1.0.1</hipster-core.version>
Expand Down Expand Up @@ -114,6 +114,12 @@
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${org.springdoc.version}</version>
<exclusions>
<exclusion>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
</exclusion>
</exclusions>
</dependency>

<dependency>
Expand Down Expand Up @@ -180,8 +186,7 @@
<version>${org.mapstruct.version}</version>
</dependency>

<!-- library for writing, reading, serializing, deserializing,
and/or parsing .csv files! -->
<!-- library for writing, reading, serializing, deserializing, and/or parsing .csv files! -->
<dependency>
<groupId>com.opencsv</groupId>
<artifactId>opencsv</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.siriusxi.htec.fa;
package org.siriusxi.fa;

import lombok.extern.log4j.Log4j2;
import org.springframework.boot.ApplicationRunner;
Expand Down
212 changes: 212 additions & 0 deletions src/main/java/org/siriusxi/fa/api/controller/AuthController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
package org.siriusxi.fa.api.controller;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.siriusxi.fa.api.model.request.AuthRequest;
import org.siriusxi.fa.api.model.request.ChangePasswordRequest;
import org.siriusxi.fa.api.model.request.CreateUserRequest;
import org.siriusxi.fa.api.model.request.TokenRefreshRequest;
import org.siriusxi.fa.api.model.response.TokenRefreshResponse;
import org.siriusxi.fa.api.model.response.UserResponse;
import org.siriusxi.fa.domain.User;
import org.siriusxi.fa.infra.exception.NotAllowedException;
import org.siriusxi.fa.infra.exception.RefreshTokenException;
import org.siriusxi.fa.service.UserService;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
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 org.springframework.web.client.HttpClientErrorException;

import java.util.Optional;

import static org.siriusxi.fa.infra.security.jwt.JwtTokenHelper.generateJwtToken;
import static org.springframework.http.HttpStatus.UNAUTHORIZED;

/**
* Authentication controller used to handle users authentication.
*
* @author Mohamed Taman
* @version 1.0
*/

@Log4j2
@Tag(name = "Authentication",
description = "A set of public APIs, for managing user authentication, and the registration.")
@RestController
@RequestMapping("auth")
@RequiredArgsConstructor
public class AuthController {

private final AuthenticationManager authenticationManager;
private final UserService userService;

@Operation(description = """
An API call to authenticate user before using the system,
and if successful a valid token is returned.
""")
@PostMapping(value = "signin")
public ResponseEntity<UserResponse> authenticate(@RequestBody @Valid AuthRequest request) {
try {
var authenticate = this.authenticationManager
.authenticate(new UsernamePasswordAuthenticationToken(
request.username(),
request.password()));

User user = (User) authenticate.getPrincipal();

// Generate refresh token
user = this.userService.generateRefreshToken(user);

return ResponseEntity.ok()
.header(HttpHeaders.AUTHORIZATION,
generateJwtToken(
user.getId(),
user.getUsername()))
.body(this.userService.mapper().toView(user));
} catch (BadCredentialsException ex) {
throw new HttpClientErrorException(UNAUTHORIZED, UNAUTHORIZED.getReasonPhrase());
}
}

@Operation(description = """
An API call, to register the user,
to be able to authenticate and use the system.
""")
@PostMapping(value = "signup")
public UserResponse register(@RequestBody @Valid CreateUserRequest userRequest) {

log.debug("User to be created: {}", userRequest);
return this.userService.create(userRequest);
}


/**
* <pre>
* +--------+ +---------------+
* | |--(A)------- Authorization Grant --------->| |
* | | | |
* | |<-(B)----------- Access Token -------------| |
* | | & Refresh Token | |
* | | | |
* | | +----------+ | |
* | |--(C)---- Access Token ---->| | | |
* | | | | | |
* | |<-(D)- Protected Resource --| Resource | | Authorization |
* | Client | | Server | | Server |
* | |--(E)---- Access Token ---->| | | |
* | | | | | |
* | |<-(F)- Invalid Token Error -| | | |
* | | +----------+ | |
* | | | |
* | |--(G)----------- Refresh Token ----------->| |
* | | | |
* | |<-(H)----------- Access Token -------------| |
* +--------+ & Optional Refresh Token +---------------+
* </pre>
* <ul>
* <li>(A) The client requests an access token by authenticating with the
* authorization server and presenting an authorization grant.</br></li>
*
* <li>(B) The authorization server authenticates the client and validates
* the authorization grant, and if valid, issues an access token
* and a refresh token.</br></li>
*
* <li>(C) The client makes a protected resource request to the resource
* server by presenting the access token.</br></li>
*
* <li>(D) The resource server validates the access token, and if valid,
* serves the request.</br></li>
*
* <li>(E) Steps (C) and (D) repeat until the access token expires. If the
* client knows the access token expired, it skips to step (G);
* otherwise, it makes another protected resource request.</br></li>
*
* <li>(F) Since the access token is invalid, the resource server returns
* an invalid token error.</br></li>
*
* <li>(G) The client requests a new access token by authenticating with
* the authorization server and presenting the refresh token. The
* client authentication requirements are based on the client type
* and on the authorization server policies.</br></li>
*
* <li>(H) The authorization server authenticates the client and validates
* the refresh token, and if valid, issues a new access token (and,
* optionally, a new refresh token).</br></li>
* </ul>
*
* @param tokenRefreshRequest the request for generating a new token, and refresh token.
* @return the new access token, and a new refresh token if not expired.
*/
@Operation(description = """
An API call, to refresh user JWT token without signing again,
to be able to continue be authenticated and use the system.
""")
@PostMapping(value = "refresh_token")
public TokenRefreshResponse refreshToken(@RequestBody @Valid TokenRefreshRequest tokenRefreshRequest) {
log.debug("Token to be Refreshed: {}", tokenRefreshRequest);

return this.userService
.findByRefreshToken(tokenRefreshRequest.refreshToken())
.map(this.userService::verifyRefreshTokenExpiration)
.map(this.userService::generateRefreshToken)
.map(user ->
new TokenRefreshResponse(generateJwtToken(
user.getId(),
user.getUsername()), user.getRefreshToken().toString())
)
.orElseThrow(() -> new RefreshTokenException(tokenRefreshRequest.refreshToken(), "Invalid refresh token, can't generate a new one!"));
}

@Operation(description = """
An API call, to logout the user,
But has to re-authenticate again to access the system.
""",
security = {@SecurityRequirement(name = "bearer-key")})
@PostMapping("/signout")
public ResponseEntity<String> logoutUser() {
getUserFromSecurityContext()
.ifPresentOrElse(user -> {
this.userService.invalidateRefreshTokenById(user.getId());
SecurityContextHolder.getContext().setAuthentication(null);
}, () -> {
throw new NotAllowedException("Can't logout, please signin!");
});

return ResponseEntity.ok("Logged out successful!");
}

@Operation(description = """
An API call, to change user password,
But user has to re-authenticate again to access the system with new password.
""",
security = {@SecurityRequirement(name = "bearer-key")})
@PostMapping("/change_password")
public ResponseEntity<String> changePassword(@RequestBody @Valid ChangePasswordRequest changePassRequest) {
getUserFromSecurityContext()
.ifPresentOrElse(user -> {
this.userService.updatePassword(user.getId(), changePassRequest);
SecurityContextHolder.getContext().setAuthentication(null);
}, () -> {
throw new NotAllowedException("Can't change password, please signin!");
});

return ResponseEntity.ok("Password changed successful!");
}

private Optional<User> getUserFromSecurityContext() {
var auth = SecurityContextHolder.getContext().getAuthentication();
return auth != null && auth.getPrincipal() instanceof User user ? Optional.of(user) : Optional.empty();
}

}
Loading

0 comments on commit 9cf5fed

Please sign in to comment.