Skip to content

Commit

Permalink
Storing users in database + Cleanups
Browse files Browse the repository at this point in the history
  • Loading branch information
Aquerr committed Jan 3, 2025
1 parent 4d65cd7 commit 0865d4b
Show file tree
Hide file tree
Showing 137 changed files with 2,628 additions and 328 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package pl.bartlomiejstepien.armaserverwebgui.application.auth;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import pl.bartlomiejstepien.armaserverwebgui.application.config.security.JwtService;
import pl.bartlomiejstepien.armaserverwebgui.domain.user.UserService;
import reactor.core.publisher.Mono;

@Slf4j
@Service
@RequiredArgsConstructor
public class AuthService
{
private final PasswordEncoder passwordEncoder;
private final UserService userService;
private final JwtService jwtService;

public Mono<JwtToken> authenticate(String username, String password, String ipAddress)
{
log.info("Login attempt for {} from {}", username, ipAddress);
return userService.getUserWithPassword(username)
.filter(user -> passwordEncoder.matches(password, user.getPassword()))
.switchIfEmpty(Mono.error(new BadCredentialsException("Invalid username or password")))
.flatMap(user -> this.userService.getUser(username))
.map(user -> new JwtToken(this.jwtService.createJwt(user), user.getAuthorities()));
}

public Mono<Void> logout(String jwt)
{
return this.jwtService.invalidate(jwt);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package pl.bartlomiejstepien.armaserverwebgui.application.auth;

import pl.bartlomiejstepien.armaserverwebgui.application.security.AswgAuthority;

import java.util.Set;

public record JwtToken(String jwt, Set<AswgAuthority> authorities)
{

}
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ public class ASWGConfig
private static final String SERVER_DIRECTORY_PATH_PROPERTY = "aswg.server-directory-path";
private static final String MODS_DIRECTORY_PATH_PROPERTY = "aswg.mods-directory-path";

private static final String USERNAME_PROPERTY = "aswg.username";
private static final String PASSWORD_PROPERTY = "aswg.password";
private static final String USERNAME_PROPERTY = "aswg.default-user.username";
private static final String PASSWORD_PROPERTY = "aswg.default-user.password";
private static final String RESET_DEFAULT_USER = "aswg.default-user.reset";
private static final String STEAMCMD_PATH = "aswg.steamcmd.path";
private static final String STEAMCMD_USERNAME = "aswg.steamcmd.username";
private static final String STEAMCMD_PASSWORD = "aswg.steamcmd.password";
Expand All @@ -44,10 +45,12 @@ public class ASWGConfig
private static final String DISCORD_WEBHOOK_URL = "aswg.discord.webhook.url";


@Value("${aswg.username}")
@Value("${aswg.default-user.username}")
private String username;
@Value("${aswg.password}")
@Value("${aswg.default-user.password}")
private String password;
@Value("${aswg.default-user.reset}")
private boolean resetDefaultUser;
@Value("${aswg.server-port}")
private int serverPort;
@Value("${aswg.server-directory-path:}")
Expand Down Expand Up @@ -115,8 +118,10 @@ private Properties prepareProperties()
{
Properties configurationProperties = new Properties();
configurationProperties.setProperty(SERVER_DIRECTORY_PATH_PROPERTY, this.serverDirectoryPath);
configurationProperties.setProperty(MODS_DIRECTORY_PATH_PROPERTY, this.modsDirectoryPath);
configurationProperties.setProperty(USERNAME_PROPERTY, this.username);
configurationProperties.setProperty(PASSWORD_PROPERTY, this.password);
configurationProperties.setProperty(RESET_DEFAULT_USER, String.valueOf(this.resetDefaultUser));
configurationProperties.setProperty(STEAMCMD_PATH, this.steamCmdPath);
configurationProperties.setProperty(SERVER_PORT, String.valueOf(this.serverPort));
configurationProperties.setProperty(STEAM_API_KEY, this.steamApiKey);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import org.springframework.context.annotation.Configuration;
import pl.bartlomiejstepien.armaserverwebgui.domain.server.mission.VanillaMissionsImporter;
import pl.bartlomiejstepien.armaserverwebgui.domain.server.mission.converter.MissionConverter;
import pl.bartlomiejstepien.armaserverwebgui.repository.MissionRepository;
import pl.bartlomiejstepien.armaserverwebgui.interfaces.repository.MissionRepository;

@Configuration(proxyBeanMethods = false)
public class VanillaMissionsImporterConfig
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package pl.bartlomiejstepien.armaserverwebgui.application.config.security;

import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import pl.bartlomiejstepien.armaserverwebgui.domain.user.UserService;
import pl.bartlomiejstepien.armaserverwebgui.domain.user.dto.AswgUser;
import reactor.core.publisher.Mono;

import java.util.List;

@RequiredArgsConstructor
public class JwtReactiveAuthenticationManager implements ReactiveAuthenticationManager
{
private final UserService userService;
private final JwtService jwtService;

@Override
public Mono<Authentication> authenticate(Authentication authentication)
{
return Mono.just(authentication)
.map(authentication1 -> jwtService.validateJwt(String.valueOf(authentication1.getCredentials())))
.onErrorResume(Exception.class, err -> Mono.error(new BadCredentialsException("Bad auth token!")))
.flatMap(jws -> userService.getUser(jws.getPayload().getSubject()))
.mapNotNull(this::toAuthentication);
}

//TODO: Create custom Authentication implementation or store AswgUser in UsernamePasswordAuthenticationToken
private Authentication toAuthentication(AswgUser aswgUser)
{
return UsernamePasswordAuthenticationToken.authenticated(
aswgUser,
aswgUser.getUsername(),
prepareAuthorities(aswgUser)
);
}

private List<SimpleGrantedAuthority> prepareAuthorities(AswgUser aswgUser)
{
return aswgUser.getAuthorities().stream()
.map(authority -> new SimpleGrantedAuthority(authority.getCode()))
.toList();
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package pl.bartlomiejstepien.armaserverwebgui.application.config.security;

import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
Expand All @@ -10,18 +11,17 @@
import java.util.Objects;

@Component
@RequiredArgsConstructor
public class JwtServerAuthenticationConverter implements ServerAuthenticationConverter
{
private static final String BEARER = "Bearer ";
private final JwtService jwtService;

@Override
public Mono<Authentication> convert(ServerWebExchange exchange)
{
return Mono.justOrEmpty(exchange)
.flatMap(serverWebExchange -> Mono.justOrEmpty(serverWebExchange.getRequest().getHeaders().getFirst("Authorization")))
.mapNotNull(jwtService::extractJwt)
.filter(Objects::nonNull)
.filter(jwt -> jwt.startsWith(BEARER))
.map(jwt -> jwt.substring(BEARER.length()))
.map(jwt -> new UsernamePasswordAuthenticationToken(jwt, jwt));
.map(jwt -> UsernamePasswordAuthenticationToken.unauthenticated(jwt, jwt));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,85 @@
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ServerWebExchange;
import pl.bartlomiejstepien.armaserverwebgui.application.security.jwt.model.InvalidJwtTokenEntity;
import pl.bartlomiejstepien.armaserverwebgui.domain.user.dto.AswgUser;
import pl.bartlomiejstepien.armaserverwebgui.interfaces.jwt.InvalidJwtTokenRepository;
import reactor.core.publisher.Mono;

import javax.crypto.SecretKey;
import java.sql.Date;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import static java.util.Optional.ofNullable;

@Service
@Slf4j
@RequiredArgsConstructor
public class JwtService
{
private static final String AUTHORIZATION_HEADER = "Authorization";
private static final String BEARER = "Bearer ";

private static final SecretKey KEY = Jwts.SIG.HS256.key().build();
private static final Map<String, BlackListedJwt> BLACK_LISTED_JWTS = new HashMap<>();
private static final ScheduledExecutorService BLACK_LISTED_JWTS_CLEARER_EXECUTOR_SERVICE = Executors.newSingleThreadScheduledExecutor();

@Value("${aswg.security.jwt.issuer}")
private String jwtIssuer;

@Value("${aswg.security.jwt.expiration-time}")
private Duration jwtExpirationTime;

public String createJwt(String username)
static {
BLACK_LISTED_JWTS_CLEARER_EXECUTOR_SERVICE.scheduleAtFixedRate(JwtService::clearBlacklistedJwts, 1, 1, TimeUnit.HOURS);
}

private static void clearBlacklistedJwts()
{
for (final BlackListedJwt blackListedJwt : List.copyOf(BLACK_LISTED_JWTS.values()))
{
if (ZonedDateTime.now().isAfter(blackListedJwt.expirationTime()))
{
BLACK_LISTED_JWTS.remove(blackListedJwt.jwt());
}
}
}

private final InvalidJwtTokenRepository invalidJwtTokenRepository;

@EventListener
public void onApplicationReady(ApplicationReadyEvent event)
{
invalidJwtTokenRepository.findAll()
.map(invalidJwtTokenEntity -> new BlackListedJwt(invalidJwtTokenEntity.getJwt(), invalidJwtTokenEntity.getExpirationDateTime()))
.toIterable()
.forEach(blackListedJwt -> BLACK_LISTED_JWTS.put(blackListedJwt.jwt(), blackListedJwt));
}

public String createJwt(AswgUser aswgUser)
{
return Jwts.builder()
.subject(username)
.subject(aswgUser.getUsername())
.signWith(KEY)
.issuer(jwtIssuer)
.compressWith(Jwts.ZIP.DEF)
.expiration(Date.from(Instant.now().plus(jwtExpirationTime)))
.issuedAt(Date.from(Instant.now()))
.compact();
Expand All @@ -51,4 +103,32 @@ public Jws<Claims> validateJwt(String jwt)
throw exception;
}
}

public String extractJwt(ServerWebExchange serverWebExchange)
{
return ofNullable(serverWebExchange.getRequest().getHeaders().getFirst(AUTHORIZATION_HEADER))
.filter(headerValue -> headerValue.startsWith(BEARER))
.map(headerValue -> headerValue.substring(BEARER.length()))
.orElse(null);
}

public Mono<Void> invalidate(String jwt)
{
return Mono.just(validateJwt(jwt))
.map(jws -> jws.getPayload().getExpiration().toInstant().atZone(ZoneId.systemDefault()))
.flatMap(expirationDateTime -> {
BLACK_LISTED_JWTS.put(jwt, new BlackListedJwt(jwt, expirationDateTime));

InvalidJwtTokenEntity invalidJwtTokenEntity = new InvalidJwtTokenEntity();
invalidJwtTokenEntity.setJwt(jwt);
invalidJwtTokenEntity.setInvalidatedDateTime(ZonedDateTime.now());
invalidJwtTokenEntity.setExpirationDateTime(expirationDateTime);
return invalidJwtTokenRepository.save(invalidJwtTokenEntity);
})
.doOnError((throwable -> log.error("Could not save invalid jwt '{}'", jwt, throwable)))
.onErrorResume(throwable -> Mono.empty())
.then();
}

private record BlackListedJwt(String jwt, ZonedDateTime expirationTime) { }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package pl.bartlomiejstepien.armaserverwebgui.application.config.security;

import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;

//TODO: Implement it...
@Order(-4)
@Component
public class RateLimitWebFilter implements WebFilter
{
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain)
{
return chain.filter(exchange);
}
}
Loading

0 comments on commit 0865d4b

Please sign in to comment.