Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions api/floaty-open-api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/User'

/flights/{userId}:
get:
tags:
Expand All @@ -68,7 +67,9 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/Flight'
type: array
items:
$ref: '#/components/schemas/Flight'
/flights/{flightId}:
delete:
tags:
Expand Down Expand Up @@ -270,7 +271,9 @@ components:
User:
type: object
required:
- id
- name
- emailVerified
properties:
id:
type: string
Expand All @@ -282,6 +285,10 @@ components:
type: string
example: 'Free Willy'
description: 'The name of the user.'
emailVerified:
type: boolean
example: false
description: 'If the email has been verified by the user already or not.'
Flight:
type: object
required:
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ services:
- "5001:80" # Expose the web UI on localhost:5001
- "2525:25" # Expose the SMTP server on localhost:2525
floaty-backend:
image: matthaeusheer/floaty-backend:latest
image: matthaeusheer/floaty-backend:0.0.4
build: .
ports:
- "8080:8080"
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/ch/floaty/domain/model/TimedToken.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@ public abstract class TimedToken {
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String token;
private boolean revoked;

public TimedToken(User user, Supplier<String> tokenGenerator, int ttlMinutes) {
this.user = user;
this.token = tokenGenerator.get();
this.expirationTime = LocalDateTime.now().plusMinutes(ttlMinutes);
this.revoked = false;
}

@OneToOne
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ public class AuthenticationExceptions {

public static class UserNotFoundException extends RuntimeException {
public UserNotFoundException() {
super("Authentication failed.");
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import ch.floaty.domain.model.SessionToken;
import ch.floaty.domain.model.User;
import ch.floaty.infrastructure.EmailService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
Expand All @@ -20,6 +21,7 @@

@Service
@Transactional
@Slf4j
public class AuthenticationService implements IAuthenticationService{

@Value("${app.base.url}")
Expand Down Expand Up @@ -111,6 +113,7 @@ public void verifyEmail(String inputEmailVerificationToken) throws EmailAlreadyV
user.setEmailVerified(true);
userRepository.save(user);
}
log.info("Verified email '{}' for user '{}'", user.getEmail(), user.getUsername());
}

@Override
Expand All @@ -123,6 +126,7 @@ public Optional<PasswordResetToken> initiatePasswordReset(String email) {
String eMailText = "Your password reset code: " + passwordResetToken.getToken();
String eMailSubject = "Floaty Password Reset";
emailService.sendSimpleEmail(user.getEmail(), eMailSubject, eMailText);
log.info("Initiated password reset for user '{}' and sent mail with reset token to '{}'", user.getUsername(), user.getEmail());
return Optional.of(passwordResetToken);
}
return Optional.empty();
Expand All @@ -132,25 +136,30 @@ public Optional<PasswordResetToken> initiatePasswordReset(String email) {
public void resetPassword(String inputPasswordResetToken, String newPassword) throws UserNotFoundException {
PasswordResetToken passwordResetToken = this.passwordResetTokenRepository.findByToken(inputPasswordResetToken)
.orElseThrow(() -> new InvalidPasswordResetTokenException("Token not found."));
User user = passwordResetToken.getUser();
if (passwordResetToken.hasExpired()) {
log.info("Password reset for user '{}': token expired.", user.getUsername());
throw new TokenExpiredException();
}
if (passwordResetToken.isUsed()) {
log.info("Password reset for user '{}': token already used.", user.getUsername());
throw new InvalidPasswordResetTokenException("Token already used.");
}
validatePasswordStrength(newPassword);
User user = passwordResetToken.getUser();

passwordResetToken.setUsed(true);
sessionTokenRepository.findByUserId(user.getId()).forEach(sessionTokenRepository::delete);
passwordResetTokenRepository.save(passwordResetToken);
user.setHashedPassword(bCryptPasswordEncoder.encode(newPassword));
userRepository.save(user);
log.info("Did reset password for user '{}'", user.getUsername());
}


@Override
public void logout() {
// TODO: invalidate any empty session tokens & potentially unset cookie or what do we do here?
log.warn("Logout not yet implemented!");
}


Expand All @@ -159,6 +168,7 @@ private void validatePasswordStrength(String password) throws InsecurePasswordEx
if (password == null || password.length() < 8) {
throw new InsecurePasswordException();
}

}

private void validateEmailForm(String email) throws EmailInvalidException {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package ch.floaty.infrastructure;

import ch.floaty.domain.repository.IUserRepository;
import ch.floaty.domain.service.AuthenticationExceptions.UserNotFoundException;
import ch.floaty.domain.service.AuthenticationExceptions.WrongPasswordException;
import ch.floaty.domain.service.AuthenticationService;
Expand All @@ -9,6 +10,7 @@
import ch.floaty.generated.RegisterRequestDto;
import ch.floaty.generated.ResetPasswordRequestDto;
import ch.floaty.generated.UserDto;
import lombok.extern.slf4j.Slf4j;
import org.modelmapper.ModelMapper;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
Expand All @@ -23,36 +25,43 @@
import static org.springframework.http.HttpStatus.UNAUTHORIZED;

@RestController
@Slf4j
public class AuthenticationController {

AuthenticationService authenticationService;
IUserRepository userRepository;
ModelMapper modelMapper = new ModelMapper();

public AuthenticationController(AuthenticationService authenticationService, EmailService emailService) {
public AuthenticationController(AuthenticationService authenticationService, IUserRepository userRepository) {
this.authenticationService = authenticationService;
this.userRepository = userRepository;
}

@PostMapping("/auth/register")
public ResponseEntity<UserDto> register(@RequestBody RegisterRequestDto registerRequestDto) {
User newUser = authenticationService.register(registerRequestDto.getUsername(), registerRequestDto.getEmail(), registerRequestDto.getPassword());
URI location = URI.create("/users/" + newUser.getId());
UserDto responseUserDto = modelMapper.map(newUser, UserDto.class);
log.info("Registered user '{}' and sent email verification link to {}.", registerRequestDto.getUsername(), registerRequestDto.getEmail());
return ResponseEntity.created(location).body(responseUserDto);
}

@PostMapping("/auth/login")
public ResponseEntity<Void> login(@RequestBody LoginRequestDto loginRequestDto, HttpServletResponse response) {
public ResponseEntity<UserDto> login(@RequestBody LoginRequestDto loginRequestDto, HttpServletResponse response) {
SessionToken sessionToken;
try {
sessionToken = authenticationService.login(loginRequestDto.getName(), loginRequestDto.getPassword());
} catch (UserNotFoundException | WrongPasswordException exception) {
log.info("Login attempt failed for '{}'. Reason: {} {}", loginRequestDto.getName(), exception.getClass().getName(), exception.getMessage());
return ResponseEntity.status(UNAUTHORIZED).body(null);
}
Cookie cookie = new Cookie("sessionToken", sessionToken.getToken());
cookie.setHttpOnly(true);
cookie.setPath("/"); // all endpoints shall return new session cookie
response.addCookie(cookie);
return ResponseEntity.ok().build();
UserDto responseUserDto = modelMapper.map(this.userRepository.findByName(loginRequestDto.getName()), UserDto.class);
log.info("User '{}' logged in successfully.", loginRequestDto.getName());
return ResponseEntity.ok().body(responseUserDto);
}

@PostMapping("/auth/verify-email/{token}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ protected void doFilterInternal(@NotNull HttpServletRequest request, @NotNull Ht
}

if (request.getRequestURI().startsWith("/auth/")) {
logger.info("Bypassing security filter for URI: " + request.getRequestURI());
logger.debug("Bypassing security filter for URI: " + request.getRequestURI());
chain.doFilter(request, response);
return;
}
Expand Down
8 changes: 7 additions & 1 deletion src/main/java/ch/floaty/infrastructure/UserSecurity.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package ch.floaty.infrastructure;

import ch.floaty.domain.model.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;

@Component
@Slf4j
public class UserSecurity {

public boolean hasUserIdOrAdmin(Long userId) {
Expand All @@ -19,6 +21,10 @@ public boolean hasUserIdOrAdmin(Long userId) {
boolean isAdmin = user.getAuthorities().stream()
.anyMatch(grantedAuthority -> grantedAuthority.getAuthority().equals("ADMIN"));
boolean hasUserId = user.getId().equals(userId);
return isAdmin || hasUserId;
boolean hasUserIdOrIsAdmin = isAdmin || hasUserId;
if (!hasUserIdOrIsAdmin) {
log.info("Unauthorized request: hasUserIdOrIsAdmin is false.");
}
return hasUserIdOrIsAdmin;
}
}
47 changes: 47 additions & 0 deletions src/main/resources/logback-spring.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>

<property name="LOGS" value="./logs" />

<appender name="Console"
class="ch.qos.logback.core.ConsoleAppender">
<layout class="ch.qos.logback.classic.PatternLayout">
<Pattern>
%white(%d{ISO8601}) %highlight(%-5level) [%blue(%t)] %yellow(%C{1}): %msg%n%throwable
</Pattern>
</layout>
</appender>

<appender name="RollingFile"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOGS}/spring-boot-logger.log</file>
<encoder
class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<Pattern>%d %p %C{1} [%t] %m%n</Pattern>
</encoder>

<rollingPolicy
class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- rollover daily and when the file reaches 10 MegaBytes -->
<fileNamePattern>${LOGS}/archived/spring-boot-logger-%d{yyyy-MM-dd}.%i.log
</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy
class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>10MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
</appender>

<!-- LOG everything at INFO level -->
<root level="info">
<appender-ref ref="RollingFile" />
<appender-ref ref="Console" />
</root>

<!-- LOG "com.baeldung*" at TRACE level -->
<logger name="com.baeldung" level="trace" additivity="false">
<appender-ref ref="RollingFile" />
<appender-ref ref="Console" />
</logger>

</configuration>
Loading