Skip to content

Commit

Permalink
Merge pull request #212 from peter-szrnka/211-gms-72-keycloak-sso-fea…
Browse files Browse the repository at this point in the history
…ture

GMS-72 keycloak sso feature
  • Loading branch information
peter-szrnka authored Apr 4, 2024
2 parents 6bd47bb + 634db47 commit a28b385
Show file tree
Hide file tree
Showing 145 changed files with 4,139 additions and 1,503 deletions.
1 change: 1 addition & 0 deletions batch-files/keycloak-sso/start-keycloak.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
docker run --name keycloak -p 7000:8080 -d -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak:24.0.1 start-dev
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package io.github.gms.auth;

import io.github.gms.auth.model.AuthenticationResponse;
import io.github.gms.common.dto.LoginVerificationRequestDto;

/**
* @author Peter Szrnka
Expand All @@ -11,5 +10,5 @@ public interface AuthenticationService {

AuthenticationResponse authenticate(String username, String credential);

AuthenticationResponse verify(LoginVerificationRequestDto dto);
void logout();
}
Original file line number Diff line number Diff line change
@@ -1,59 +1,46 @@
package io.github.gms.auth;

import dev.samstevens.totp.code.CodeVerifier;
import io.github.gms.auth.model.AuthenticationResponse;
import io.github.gms.auth.model.GmsUserDetails;
import io.github.gms.auth.service.TokenGeneratorService;
import io.github.gms.auth.types.AuthResponsePhase;
import io.github.gms.common.abstraction.AbstractAuthService;
import io.github.gms.common.dto.LoginVerificationRequestDto;
import io.github.gms.common.enums.JwtConfigType;
import io.github.gms.common.enums.SystemProperty;
import io.github.gms.common.converter.GenerateJwtRequestConverter;
import io.github.gms.functions.user.UserConverter;
import io.github.gms.common.service.JwtService;
import io.github.gms.functions.systemproperty.SystemPropertyService;
import io.github.gms.functions.user.UserService;
import io.github.gms.functions.user.UserConverter;
import io.github.gms.functions.user.UserLoginAttemptManagerService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Profile;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;

import java.util.Map;

import static io.github.gms.common.util.Constants.CONFIG_AUTH_TYPE_NOT_KEYCLOAK_SSO;

/**
* @author Peter Szrnka
* @since 1.0
*/
@Slf4j
@Service
public class AuthenticationServiceImpl extends AbstractAuthService implements AuthenticationService {
@RequiredArgsConstructor
@Profile(CONFIG_AUTH_TYPE_NOT_KEYCLOAK_SSO)
public class AuthenticationServiceImpl implements AuthenticationService {

private final TokenGeneratorService tokenGeneratorService;
private final SystemPropertyService systemPropertyService;
private final AuthenticationManager authenticationManager;
private final UserConverter converter;
private final CodeVerifier verifier;
private final UserService userService;

public AuthenticationServiceImpl(
AuthenticationManager authenticationManager,
JwtService jwtService,
SystemPropertyService systemPropertyService,
GenerateJwtRequestConverter generateJwtRequestConverter,
UserConverter converter,
UserAuthService userAuthService,
CodeVerifier verifier,
UserService userService) {
super(jwtService, systemPropertyService, generateJwtRequestConverter, userAuthService);
this.authenticationManager = authenticationManager;
this.converter = converter;
this.verifier = verifier;
this.userService = userService;
}
private final UserLoginAttemptManagerService userLoginAttemptManagerService;

@Override
public AuthenticationResponse authenticate(String username, String credential) {
try {
if (userService.isBlocked(username)) { // User locked in DB
if (userLoginAttemptManagerService.isBlocked(username)) { // User locked in DB
return AuthenticationResponse.builder()
.phase(AuthResponsePhase.BLOCKED)
.build();
Expand All @@ -76,8 +63,8 @@ public AuthenticationResponse authenticate(String username, String credential) {
.build();
}

Map<JwtConfigType, String> authenticationDetails = getAuthenticationDetails(user);
userService.resetLoginAttempt(username);
Map<JwtConfigType, String> authenticationDetails = tokenGeneratorService.getAuthenticationDetails(user);
userLoginAttemptManagerService.resetLoginAttempt(username);

return AuthenticationResponse.builder()
.currentUser(converter.toUserInfoDto(user, false))
Expand All @@ -86,51 +73,18 @@ public AuthenticationResponse authenticate(String username, String credential) {
.phase(AuthResponsePhase.COMPLETED)
.build();
} catch (Exception ex) {
userService.updateLoginAttempt(username);
userLoginAttemptManagerService.updateLoginAttempt(username);
log.warn("Login failed", ex);
return new AuthenticationResponse();
}
}

@Override
public AuthenticationResponse verify(LoginVerificationRequestDto dto) {
try {
if (userService.isBlocked(dto.getUsername())) {
return AuthenticationResponse.builder()
.phase(AuthResponsePhase.BLOCKED)
.build();
}

GmsUserDetails userDetails = (GmsUserDetails) userAuthService.loadUserByUsername(dto.getUsername());

if (Boolean.FALSE.equals(userDetails.getAccountNonLocked())) { // User locked in LDAP
return AuthenticationResponse.builder()
.phase(AuthResponsePhase.BLOCKED)
.build();
}

if (!verifier.isValidCode(userDetails.getMfaSecret(), dto.getVerificationCode())) {
userService.updateLoginAttempt(dto.getUsername());
return AuthenticationResponse.builder().phase(AuthResponsePhase.FAILED).build();
}

Map<JwtConfigType, String> authenticationDetails = getAuthenticationDetails(userDetails);

// Verify codes
return AuthenticationResponse.builder()
.currentUser(converter.toUserInfoDto(userDetails, false))
.phase(AuthResponsePhase.COMPLETED)
.token(authenticationDetails.get(JwtConfigType.ACCESS_JWT))
.refreshToken(authenticationDetails.get(JwtConfigType.REFRESH_JWT))
.build();
} catch (Exception e) {
return AuthenticationResponse.builder()
.phase(AuthResponsePhase.FAILED)
.build();
}
public void logout() {
log.info("User logged out");
}

private boolean isMfaEnabled(GmsUserDetails userDetails) {
private boolean isMfaEnabled(GmsUserDetails userDetails) {
return systemPropertyService.getBoolean(SystemProperty.ENABLE_GLOBAL_MFA) ||
(systemPropertyService.getBoolean(SystemProperty.ENABLE_MFA) && userDetails.isMfaEnabled());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,18 @@

import io.github.gms.auth.model.AuthorizationResponse;
import io.github.gms.auth.model.GmsUserDetails;
import io.github.gms.common.abstraction.AbstractAuthService;
import io.github.gms.auth.service.TokenGeneratorService;
import io.github.gms.common.enums.MdcParameter;
import io.github.gms.common.enums.SystemProperty;
import io.github.gms.common.converter.GenerateJwtRequestConverter;
import io.github.gms.common.service.JwtService;
import io.github.gms.functions.systemproperty.SystemPropertyService;
import io.jsonwebtoken.Claims;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.context.annotation.Profile;
import org.springframework.data.util.Pair;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
Expand All @@ -24,22 +25,22 @@
import java.util.stream.Stream;

import static io.github.gms.common.util.Constants.ACCESS_JWT_TOKEN;
import static io.github.gms.common.util.Constants.CONFIG_AUTH_TYPE_NOT_KEYCLOAK_SSO;

/**
* @author Peter Szrnka
* @since 1.0
*/
@Slf4j
@Service
public class AuthorizationServiceImpl extends AbstractAuthService implements AuthorizationService {
@RequiredArgsConstructor
@Profile(value = { CONFIG_AUTH_TYPE_NOT_KEYCLOAK_SSO })
public class AuthorizationServiceImpl implements AuthorizationService {

AuthorizationServiceImpl(
JwtService jwtService,
SystemPropertyService systemPropertyService,
GenerateJwtRequestConverter generateJwtRequestConverter,
UserAuthService userAuthService) {
super(jwtService, systemPropertyService, generateJwtRequestConverter, userAuthService);
}
private final JwtService jwtService;
private final TokenGeneratorService tokenGeneratorService;
private final SystemPropertyService systemPropertyService;
private final UserAuthService userAuthService;

@Override
public AuthorizationResponse authorize(HttpServletRequest request) {
Expand Down Expand Up @@ -86,7 +87,7 @@ public AuthorizationResponse authorize(HttpServletRequest request) {

return AuthorizationResponse.builder()
.authentication(authentication)
.jwtPair(getAuthenticationDetails(userDetails))
.jwtPair(tokenGeneratorService.getAuthenticationDetails(userDetails))
.build();
} catch (Exception e) {
log.warn("Authorization failed: {}", e.getMessage());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.github.gms.auth;

import io.github.gms.auth.model.AuthenticationResponse;
import io.github.gms.common.dto.LoginVerificationRequestDto;

/**
* @author Peter Szrnka
* @since 1.0
*/
public interface VerificationService {

AuthenticationResponse verify(LoginVerificationRequestDto dto);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package io.github.gms.auth;

import dev.samstevens.totp.code.CodeVerifier;
import io.github.gms.auth.model.AuthenticationResponse;
import io.github.gms.auth.model.GmsUserDetails;
import io.github.gms.auth.service.TokenGeneratorService;
import io.github.gms.auth.types.AuthResponsePhase;
import io.github.gms.common.dto.LoginVerificationRequestDto;
import io.github.gms.common.enums.JwtConfigType;
import io.github.gms.functions.user.UserConverter;
import io.github.gms.functions.user.UserLoginAttemptManagerService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;

import java.util.Map;

import static io.github.gms.common.util.Constants.CONFIG_AUTH_TYPE_NOT_KEYCLOAK_SSO;

/**
* @author Peter Szrnka
* @since 1.0
*/
@Service
@RequiredArgsConstructor
@Profile(CONFIG_AUTH_TYPE_NOT_KEYCLOAK_SSO)
public class VerificationServiceImpl implements VerificationService {

private final UserAuthService userAuthService;
private final UserConverter converter;
private final CodeVerifier verifier;
private final UserLoginAttemptManagerService userLoginAttemptManagerService;
private final TokenGeneratorService tokenGeneratorService;

@Override
public AuthenticationResponse verify(LoginVerificationRequestDto dto) {
try {
if (userLoginAttemptManagerService.isBlocked(dto.getUsername())) {
return AuthenticationResponse.builder()
.phase(AuthResponsePhase.BLOCKED)
.build();
}

GmsUserDetails userDetails = (GmsUserDetails) userAuthService.loadUserByUsername(dto.getUsername());

if (Boolean.FALSE.equals(userDetails.getAccountNonLocked())) { // User locked in LDAP
return AuthenticationResponse.builder()
.phase(AuthResponsePhase.BLOCKED)
.build();
}

if (!verifier.isValidCode(userDetails.getMfaSecret(), dto.getVerificationCode())) {
userLoginAttemptManagerService.updateLoginAttempt(dto.getUsername());
return AuthenticationResponse.builder().phase(AuthResponsePhase.FAILED).build();
}

Map<JwtConfigType, String> authenticationDetails = tokenGeneratorService.getAuthenticationDetails(userDetails);

// Verify codes
return AuthenticationResponse.builder()
.currentUser(converter.toUserInfoDto(userDetails, false))
.phase(AuthResponsePhase.COMPLETED)
.token(authenticationDetails.get(JwtConfigType.ACCESS_JWT))
.refreshToken(authenticationDetails.get(JwtConfigType.REFRESH_JWT))
.build();
} catch (Exception e) {
return AuthenticationResponse.builder()
.phase(AuthResponsePhase.FAILED)
.build();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package io.github.gms.auth.config;

import io.github.gms.common.filter.SecureHeaderInitializerFilter;
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer;
import org.springframework.security.config.annotation.web.configurers.HttpBasicConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.List;

/**
* @author Peter Szrnka
* @since 1.0
*/
public abstract class AbstractSecurityConfig {

private static final String[] FILTER_URL = new String[]{"/", "/system/status", "/healthcheck", "/setup/**",
"/login", "/authenticate", "/verify", "/logoutUser", "/api/**", "/info/me", "/actuator/**",
"/gms-app/**", "/favicon.ico", "/assets/**", "/index.html**", "/*.js**", "/*.css**", "/*.json**",
"/manifest.webmanifest", "/reset_password"};

@Bean
public SecurityFilterChain filterChain(HttpSecurity http,
AuthenticationEntryPoint authenticationEntryPoint,
SecureHeaderInitializerFilter secureHeaderInitializerFilter) throws Exception {
http
.cors(cors -> Customizer.withDefaults())
.csrf(AbstractHttpConfigurer::disable)
.exceptionHandling(e -> e.authenticationEntryPoint(authenticationEntryPoint))
.sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(authorizeHttpRequest ->
authorizeHttpRequest.requestMatchers(FILTER_URL).permitAll()
.requestMatchers(FILTER_URL).permitAll()
.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll().anyRequest()
.authenticated()
);

http.addFilterBefore(secureHeaderInitializerFilter, UsernamePasswordAuthenticationFilter.class);
http.formLogin(FormLoginConfigurer<HttpSecurity>::disable);
http.httpBasic(HttpBasicConfigurer<HttpSecurity>::disable);

return http.build();
}

@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(List.of("http://localhost:4200"));
configuration.setAllowedMethods(List.of("*"));
configuration.setAllowedHeaders(List.of("*"));
configuration.setAllowCredentials(true);

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
Loading

0 comments on commit a28b385

Please sign in to comment.