Working on SAML 2

This commit is contained in:
DarioGii 2025-02-01 23:30:28 +00:00 committed by Dario Ghunney Ware
parent 704da399d4
commit 6478674d9b
25 changed files with 339 additions and 277 deletions

View File

@ -35,10 +35,7 @@ public class AppConfig {
} }
@Bean @Bean
@ConditionalOnProperty( @ConditionalOnProperty(name = "system.customHTMLFiles", havingValue = "true")
name = "system.customHTMLFiles",
havingValue = "true",
matchIfMissing = false)
public SpringTemplateEngine templateEngine(ResourceLoader resourceLoader) { public SpringTemplateEngine templateEngine(ResourceLoader resourceLoader) {
SpringTemplateEngine templateEngine = new SpringTemplateEngine(); SpringTemplateEngine templateEngine = new SpringTemplateEngine();
templateEngine.addTemplateResolver(new FileFallbackTemplateResolver(resourceLoader)); templateEngine.addTemplateResolver(new FileFallbackTemplateResolver(resourceLoader));
@ -129,8 +126,8 @@ public class AppConfig {
} }
@ConditionalOnMissingClass("stirling.software.SPDF.config.security.SecurityConfiguration") @ConditionalOnMissingClass("stirling.software.SPDF.config.security.SecurityConfiguration")
@Bean(name = "activSecurity") @Bean(name = "activeSecurity")
public boolean missingActivSecurity() { public boolean missingActiveSecurity() {
return false; return false;
} }

View File

@ -6,6 +6,7 @@ import java.security.interfaces.RSAPrivateKey;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
@ -14,6 +15,7 @@ import org.springframework.security.saml2.provider.service.authentication.Saml2A
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler; import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
import com.coveo.saml.SamlClient; import com.coveo.saml.SamlClient;
import com.coveo.saml.SamlException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
@ -49,9 +51,8 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
} else if (authentication instanceof OAuth2AuthenticationToken) { } else if (authentication instanceof OAuth2AuthenticationToken) {
// Handle OAuth2 logout redirection // Handle OAuth2 logout redirection
getRedirect_oauth2(request, response, authentication); getRedirect_oauth2(request, response, authentication);
} } else if (authentication instanceof UsernamePasswordAuthenticationToken) {
// Handle Username/Password logout // Handle Username/Password logout
else if (authentication instanceof UsernamePasswordAuthenticationToken) {
getRedirectStrategy().sendRedirect(request, response, LOGOUT_PATH); getRedirectStrategy().sendRedirect(request, response, LOGOUT_PATH);
} else { } else {
// Handle unknown authentication types // Handle unknown authentication types
@ -90,27 +91,7 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
certificates.add(certificate); certificates.add(certificate);
// Construct URLs required for SAML configuration // Construct URLs required for SAML configuration
String serverUrl = SamlClient samlClient = getSamlClient(registrationId, samlConf, certificates);
SPDFApplication.getStaticBaseUrl() + ":" + SPDFApplication.getStaticPort();
String relyingPartyIdentifier =
serverUrl + "/saml2/service-provider-metadata/" + registrationId;
String assertionConsumerServiceUrl = serverUrl + "/login/saml2/sso/" + registrationId;
String idpUrl = samlConf.getIdpSingleLogoutUrl();
String idpIssuer = samlConf.getIdpIssuer();
// Create SamlClient instance for SAML logout
SamlClient samlClient =
new SamlClient(
relyingPartyIdentifier,
assertionConsumerServiceUrl,
idpUrl,
idpIssuer,
certificates,
SamlClient.SamlIdpBinding.POST);
// Read private key for service provider // Read private key for service provider
Resource privateKeyResource = samlConf.getPrivateKey(); Resource privateKeyResource = samlConf.getPrivateKey();
@ -127,7 +108,6 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
} }
} }
// Redirect for OAuth2 authentication logout
private void getRedirect_oauth2( private void getRedirect_oauth2(
HttpServletRequest request, HttpServletResponse response, Authentication authentication) HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException { throws IOException {
@ -164,12 +144,45 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
response.sendRedirect(redirectUrl); response.sendRedirect(redirectUrl);
} }
default -> { default -> {
log.info("Redirecting to default logout URL: {}", redirectUrl); String logoutUrl = oauth.getLogoutUrl();
response.sendRedirect(redirectUrl);
if (StringUtils.isNotBlank(logoutUrl)) {
log.info("Redirecting to logout URL: {}", logoutUrl);
response.sendRedirect(logoutUrl);
} else {
log.info("Redirecting to default logout URL: {}", redirectUrl);
response.sendRedirect(redirectUrl);
}
} }
} }
} }
// Redirect for OAuth2 authentication logout
private static SamlClient getSamlClient(
String registrationId, SAML2 samlConf, List<X509Certificate> certificates)
throws SamlException {
String serverUrl =
SPDFApplication.getStaticBaseUrl() + ":" + SPDFApplication.getStaticPort();
String relyingPartyIdentifier =
serverUrl + "/saml2/service-provider-metadata/" + registrationId;
String assertionConsumerServiceUrl = serverUrl + "/login/saml2/sso/" + registrationId;
String idpUrl = samlConf.getIdpSingleLogoutUrl();
String idpIssuer = samlConf.getIdpIssuer();
// Create SamlClient instance for SAML logout
return new SamlClient(
relyingPartyIdentifier,
assertionConsumerServiceUrl,
idpUrl,
idpIssuer,
certificates,
SamlClient.SamlIdpBinding.POST);
}
/** /**
* Handles different error scenarios during logout. Will return a <code>String</code> containing * Handles different error scenarios during logout. Will return a <code>String</code> containing
* the error request parameter. * the error request parameter.

View File

@ -23,6 +23,10 @@ import org.springframework.security.saml2.provider.service.web.authentication.Op
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository; import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.security.web.context.DelegatingSecurityContextRepository;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.security.web.context.RequestAttributeSecurityContextRepository;
import org.springframework.security.web.context.SecurityContextRepository;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository; import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler; import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
import org.springframework.security.web.savedrequest.NullRequestCache; import org.springframework.security.web.savedrequest.NullRequestCache;
@ -51,11 +55,7 @@ public class SecurityConfiguration {
private final CustomUserDetailsService userDetailsService; private final CustomUserDetailsService userDetailsService;
private final UserService userService; private final UserService userService;
@Qualifier("loginEnabled")
private final boolean loginEnabledValue; private final boolean loginEnabledValue;
@Qualifier("runningEE")
private final boolean runningEE; private final boolean runningEE;
private final ApplicationProperties applicationProperties; private final ApplicationProperties applicationProperties;
@ -105,10 +105,11 @@ public class SecurityConfiguration {
} }
@Bean @Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { public SecurityFilterChain filterChain(HttpSecurity http, SecurityContextRepository securityContextRepository) throws Exception {
if (applicationProperties.getSecurity().getCsrfDisabled() || !loginEnabledValue) { if (applicationProperties.getSecurity().getCsrfDisabled() || !loginEnabledValue) {
http.csrf(csrf -> csrf.disable()); http.csrf(csrf -> csrf.disable());
} }
if (loginEnabledValue) { if (loginEnabledValue) {
http.addFilterBefore( http.addFilterBefore(
userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
@ -164,8 +165,7 @@ public class SecurityConfiguration {
.logoutSuccessHandler( .logoutSuccessHandler(
new CustomLogoutSuccessHandler(applicationProperties)) new CustomLogoutSuccessHandler(applicationProperties))
.clearAuthentication(true) .clearAuthentication(true)
.invalidateHttpSession( // Invalidate session .invalidateHttpSession(true)
true)
.deleteCookies("JSESSIONID", "remember-me")); .deleteCookies("JSESSIONID", "remember-me"));
http.rememberMe( http.rememberMe(
rememberMeConfigurer -> // Use the configurator directly rememberMeConfigurer -> // Use the configurator directly
@ -234,7 +234,7 @@ public class SecurityConfiguration {
. .
/* /*
This Custom handler is used to check if the OAUTH2 user trying to log in, already exists in the database. This Custom handler is used to check if the OAUTH2 user trying to log in, already exists in the database.
If user exists, login proceeds as usual. If user does not exist, then it is autocreated but only if 'OAUTH2AutoCreateUser' If user exists, login proceeds as usual. If user does not exist, then it is auto-created but only if 'OAUTH2AutoCreateUser'
is set as true, else login fails with an error message advising the same. is set as true, else login fails with an error message advising the same.
*/ */
successHandler( successHandler(
@ -258,14 +258,20 @@ public class SecurityConfiguration {
.permitAll()); .permitAll());
} }
// Handle SAML // Handle SAML
if (applicationProperties.getSecurity().isSaml2Active()) { if (applicationProperties.getSecurity().isSaml2Active() && runningEE) {
// && runningEE
// Configure the authentication provider // Configure the authentication provider
OpenSaml4AuthenticationProvider authenticationProvider = OpenSaml4AuthenticationProvider authenticationProvider =
new OpenSaml4AuthenticationProvider(); new OpenSaml4AuthenticationProvider();
authenticationProvider.setResponseAuthenticationConverter( authenticationProvider.setResponseAuthenticationConverter(
new CustomSaml2ResponseAuthenticationConverter(userService)); new CustomSaml2ResponseAuthenticationConverter(userService));
http.authenticationProvider(authenticationProvider) http.authenticationProvider(authenticationProvider)
.securityContext(security ->
security.securityContextRepository(
new DelegatingSecurityContextRepository(
new RequestAttributeSecurityContextRepository(),
new HttpSessionSecurityContextRepository())
)
)
.saml2Login( .saml2Login(
saml2 -> { saml2 -> {
try { try {
@ -284,12 +290,13 @@ public class SecurityConfiguration {
.authenticationRequestResolver( .authenticationRequestResolver(
saml2AuthenticationRequestResolver); saml2AuthenticationRequestResolver);
} catch (Exception e) { } catch (Exception e) {
log.error("Error configuring SAML2 login", e); log.error("Error configuring SAML 2 login", e);
throw new RuntimeException(e); throw new RuntimeException(e);
} }
}); });
} }
} else { } else {
log.info("SAML 2 login is not enabled. Using default.");
http.authorizeHttpRequests(authz -> authz.anyRequest().permitAll()); http.authorizeHttpRequests(authz -> authz.anyRequest().permitAll());
} }
return http.build(); return http.build();
@ -315,7 +322,7 @@ public class SecurityConfiguration {
} }
@Bean @Bean
public boolean activSecurity() { public boolean activeSecurity() {
return true; return true;
} }
} }

View File

@ -88,7 +88,7 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
// Use API key to authenticate. This requires you to have an authentication // Use API key to authenticate. This requires you to have an authentication
// provider for API keys. // provider for API keys.
Optional<User> user = userService.getUserByApiKey(apiKey); Optional<User> user = userService.getUserByApiKey(apiKey);
if (!user.isPresent()) { if (user.isEmpty()) {
response.setStatus(HttpStatus.UNAUTHORIZED.value()); response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().write("Invalid API Key."); response.getWriter().write("Invalid API Key.");
return; return;

View File

@ -373,18 +373,15 @@ public class UserService implements UserServiceInterface {
public void invalidateUserSessions(String username) { public void invalidateUserSessions(String username) {
String usernameP = ""; String usernameP = "";
for (Object principal : sessionRegistry.getAllPrincipals()) { for (Object principal : sessionRegistry.getAllPrincipals()) {
for (SessionInformation sessionsInformation : for (SessionInformation sessionsInformation :
sessionRegistry.getAllSessions(principal, false)) { sessionRegistry.getAllSessions(principal, false)) {
if (principal instanceof UserDetails) { if (principal instanceof UserDetails userDetails) {
UserDetails userDetails = (UserDetails) principal;
usernameP = userDetails.getUsername(); usernameP = userDetails.getUsername();
} else if (principal instanceof OAuth2User) { } else if (principal instanceof OAuth2User oAuth2User) {
OAuth2User oAuth2User = (OAuth2User) principal;
usernameP = oAuth2User.getName(); usernameP = oAuth2User.getName();
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal) { } else if (principal instanceof CustomSaml2AuthenticatedPrincipal saml2User) {
CustomSaml2AuthenticatedPrincipal saml2User =
(CustomSaml2AuthenticatedPrincipal) principal;
usernameP = saml2User.getName(); usernameP = saml2User.getName();
} else if (principal instanceof String) { } else if (principal instanceof String) {
usernameP = (String) principal; usernameP = (String) principal;
@ -398,6 +395,7 @@ public class UserService implements UserServiceInterface {
public String getCurrentUsername() { public String getCurrentUsername() {
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) { if (principal instanceof UserDetails) {
return ((UserDetails) principal).getUsername(); return ((UserDetails) principal).getUsername();
} else if (principal instanceof OAuth2User) { } else if (principal instanceof OAuth2User) {
@ -406,8 +404,6 @@ public class UserService implements UserServiceInterface {
applicationProperties.getSecurity().getOauth2().getUseAsUsername()); applicationProperties.getSecurity().getOauth2().getUseAsUsername());
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal) { } else if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
return ((CustomSaml2AuthenticatedPrincipal) principal).getName(); return ((CustomSaml2AuthenticatedPrincipal) principal).getName();
} else if (principal instanceof String) {
return (String) principal;
} else { } else {
return principal.toString(); return principal.toString();
} }

View File

@ -50,8 +50,11 @@ public class CustomOAuth2AuthenticationFailureHandler
if (error.getErrorCode().equals("Password must not be null")) { if (error.getErrorCode().equals("Password must not be null")) {
errorCode = "userAlreadyExistsWeb"; errorCode = "userAlreadyExistsWeb";
} }
log.error("OAuth2 Authentication error: " + errorCode);
log.error("OAuth2AuthenticationException", exception); log.error(
"OAuth2 Authentication error: {}",
errorCode != null ? errorCode : exception.getMessage(),
exception);
getRedirectStrategy().sendRedirect(request, response, "/login?errorOAuth=" + errorCode); getRedirectStrategy().sendRedirect(request, response, "/login?errorOAuth=" + errorCode);
} }
log.error("Unhandled authentication exception", exception); log.error("Unhandled authentication exception", exception);

View File

@ -47,23 +47,31 @@ public class CustomOAuth2UserService implements OAuth2UserService<OidcUserReques
OAUTH2 oauth2 = applicationProperties.getSecurity().getOauth2(); OAUTH2 oauth2 = applicationProperties.getSecurity().getOauth2();
UsernameAttribute usernameAttribute = UsernameAttribute usernameAttribute =
UsernameAttribute.valueOf(oauth2.getUseAsUsername().toUpperCase()); UsernameAttribute.valueOf(oauth2.getUseAsUsername().toUpperCase());
String username = usernameAttribute.getName(); String usernameAttributeKey = usernameAttribute.getName();
Optional<User> internalUser = userService.findByUsernameIgnoreCase(username); // todo: save user by OIDC ID instead of username
Optional<User> internalUser =
userService.findByUsernameIgnoreCase(user.getAttribute(usernameAttributeKey));
if (internalUser.isPresent()) { if (internalUser.isPresent()) {
if (loginAttemptService.isBlocked(username)) { String internalUsername = internalUser.get().getUsername();
if (loginAttemptService.isBlocked(internalUsername)) {
throw new LockedException( throw new LockedException(
"Your account has been locked due to too many failed login attempts."); "The account "
+ internalUsername
+ " has been locked due to too many failed login attempts.");
} }
if (userService.hasPassword(username)) { if (userService.hasPassword(usernameAttributeKey)) {
throw new IllegalArgumentException("Password must not be null"); throw new IllegalArgumentException("Password must not be null");
} }
} }
// Return a new OidcUser with adjusted attributes // Return a new OidcUser with adjusted attributes
return new DefaultOidcUser( return new DefaultOidcUser(
user.getAuthorities(), userRequest.getIdToken(), user.getUserInfo(), username); user.getAuthorities(),
userRequest.getIdToken(),
user.getUserInfo(),
usernameAttributeKey);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
log.error("Error loading OIDC user: {}", e.getMessage()); log.error("Error loading OIDC user: {}", e.getMessage());
throw new OAuth2AuthenticationException(new OAuth2Error(e.getMessage()), e); throw new OAuth2AuthenticationException(new OAuth2Error(e.getMessage()), e);

View File

@ -94,7 +94,7 @@ public class OAuth2Configuration {
.clientId(keycloak.getClientId()) .clientId(keycloak.getClientId())
.clientSecret(keycloak.getClientSecret()) .clientSecret(keycloak.getClientSecret())
.scope(keycloak.getScopes()) .scope(keycloak.getScopes())
.userNameAttributeName(keycloak.getUseAsUsername().name()) .userNameAttributeName(keycloak.getUseAsUsername().getName())
.clientName(keycloak.getClientName()) .clientName(keycloak.getClientName())
.build()) .build())
: Optional.empty(); : Optional.empty();
@ -125,7 +125,7 @@ public class OAuth2Configuration {
.authorizationUri(google.getAuthorizationUri()) .authorizationUri(google.getAuthorizationUri())
.tokenUri(google.getTokenUri()) .tokenUri(google.getTokenUri())
.userInfoUri(google.getUserInfoUri()) .userInfoUri(google.getUserInfoUri())
.userNameAttributeName(google.getUseAsUsername().name()) .userNameAttributeName(google.getUseAsUsername().getName())
.clientName(google.getClientName()) .clientName(google.getClientName())
.redirectUri(REDIRECT_URI_PATH + google.getName()) .redirectUri(REDIRECT_URI_PATH + google.getName())
.authorizationGrantType(AUTHORIZATION_CODE) .authorizationGrantType(AUTHORIZATION_CODE)
@ -158,7 +158,7 @@ public class OAuth2Configuration {
.authorizationUri(github.getAuthorizationUri()) .authorizationUri(github.getAuthorizationUri())
.tokenUri(github.getTokenUri()) .tokenUri(github.getTokenUri())
.userInfoUri(github.getUserInfoUri()) .userInfoUri(github.getUserInfoUri())
.userNameAttributeName(github.getUseAsUsername().name()) .userNameAttributeName(github.getUseAsUsername().getName())
.clientName(github.getClientName()) .clientName(github.getClientName())
.redirectUri(REDIRECT_URI_PATH + github.getName()) .redirectUri(REDIRECT_URI_PATH + github.getName())
.authorizationGrantType(AUTHORIZATION_CODE) .authorizationGrantType(AUTHORIZATION_CODE)
@ -186,6 +186,7 @@ public class OAuth2Configuration {
oauth.getClientSecret(), oauth.getClientSecret(),
oauth.getScopes(), oauth.getScopes(),
UsernameAttribute.valueOf(oauth.getUseAsUsername().toUpperCase()), UsernameAttribute.valueOf(oauth.getUseAsUsername().toUpperCase()),
oauth.getLogoutUrl(),
null, null,
null, null,
null); null);
@ -220,9 +221,7 @@ public class OAuth2Configuration {
*/ */
@Bean @Bean
@ConditionalOnProperty( @ConditionalOnProperty(value = "security.oauth2.enabled", havingValue = "true")
value = "security.oauth2.enabled",
havingValue = "true")
GrantedAuthoritiesMapper userAuthoritiesMapper() { GrantedAuthoritiesMapper userAuthoritiesMapper() {
return (authorities) -> { return (authorities) -> {
Set<GrantedAuthority> mappedAuthorities = new HashSet<>(); Set<GrantedAuthority> mappedAuthorities = new HashSet<>();

View File

@ -8,7 +8,6 @@ import org.springframework.security.saml2.core.Saml2Error;
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException; import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
@ -22,7 +21,9 @@ public class CustomSaml2AuthenticationFailureHandler extends SimpleUrlAuthentica
HttpServletRequest request, HttpServletRequest request,
HttpServletResponse response, HttpServletResponse response,
AuthenticationException exception) AuthenticationException exception)
throws IOException, ServletException { throws IOException {
log.error("Authentication error", exception);
if (exception instanceof Saml2AuthenticationException) { if (exception instanceof Saml2AuthenticationException) {
Saml2Error error = ((Saml2AuthenticationException) exception).getSaml2Error(); Saml2Error error = ((Saml2AuthenticationException) exception).getSaml2Error();
getRedirectStrategy() getRedirectStrategy()
@ -34,6 +35,5 @@ public class CustomSaml2AuthenticationFailureHandler extends SimpleUrlAuthentica
response, response,
"/login?errorOAuth=not_authentication_provider_found"); "/login?errorOAuth=not_authentication_provider_found");
} }
log.error("AuthenticationException: " + exception);
} }
} }

View File

@ -112,13 +112,11 @@ public class CustomSaml2AuthenticationSuccessHandler
userService.processSSOPostLogin(username, saml2.getAutoCreateUser()); userService.processSSOPostLogin(username, saml2.getAutoCreateUser());
log.debug("Successfully processed authentication for user: {}", username); log.debug("Successfully processed authentication for user: {}", username);
response.sendRedirect(contextPath + "/"); response.sendRedirect(contextPath + "/");
return;
} catch (IllegalArgumentException | SQLException | UnsupportedProviderException e) { } catch (IllegalArgumentException | SQLException | UnsupportedProviderException e) {
log.debug( log.debug(
"Invalid username detected for user: {}, redirecting to logout", "Invalid username detected for user: {}, redirecting to logout",
username); username);
response.sendRedirect(contextPath + "/logout?invalidUsername=true"); response.sendRedirect(contextPath + "/logout?invalidUsername=true");
return;
} }
} }
} else { } else {

View File

@ -21,7 +21,7 @@ import stirling.software.SPDF.model.User;
public class CustomSaml2ResponseAuthenticationConverter public class CustomSaml2ResponseAuthenticationConverter
implements Converter<ResponseToken, Saml2Authentication> { implements Converter<ResponseToken, Saml2Authentication> {
private UserService userService; private final UserService userService;
public CustomSaml2ResponseAuthenticationConverter(UserService userService) { public CustomSaml2ResponseAuthenticationConverter(UserService userService) {
this.userService = userService; this.userService = userService;
@ -61,10 +61,10 @@ public class CustomSaml2ResponseAuthenticationConverter
Map<String, List<Object>> attributes = extractAttributes(assertion); Map<String, List<Object>> attributes = extractAttributes(assertion);
// Debug log with actual values // Debug log with actual values
log.debug("Extracted SAML Attributes: " + attributes); log.debug("Extracted SAML Attributes: {}", attributes);
// Try to get username/identifier in order of preference // Try to get username/identifier in order of preference
String userIdentifier = null; String userIdentifier;
if (hasAttribute(attributes, "username")) { if (hasAttribute(attributes, "username")) {
userIdentifier = getFirstAttributeValue(attributes, "username"); userIdentifier = getFirstAttributeValue(attributes, "username");
} else if (hasAttribute(attributes, "emailaddress")) { } else if (hasAttribute(attributes, "emailaddress")) {
@ -84,10 +84,8 @@ public class CustomSaml2ResponseAuthenticationConverter
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority("ROLE_USER"); SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority("ROLE_USER");
if (userOpt.isPresent()) { if (userOpt.isPresent()) {
User user = userOpt.get(); User user = userOpt.get();
if (user != null) { simpleGrantedAuthority =
simpleGrantedAuthority = new SimpleGrantedAuthority(userService.findRole(user).getAuthority());
new SimpleGrantedAuthority(userService.findRole(user).getAuthority());
}
} }
List<String> sessionIndexes = new ArrayList<>(); List<String> sessionIndexes = new ArrayList<>();
@ -102,7 +100,7 @@ public class CustomSaml2ResponseAuthenticationConverter
return new Saml2Authentication( return new Saml2Authentication(
principal, principal,
responseToken.getToken().getSaml2Response(), responseToken.getToken().getSaml2Response(),
Collections.singletonList(simpleGrantedAuthority)); List.of(simpleGrantedAuthority));
} }
private boolean hasAttribute(Map<String, List<Object>> attributes, String name) { private boolean hasAttribute(Map<String, List<Object>> attributes, String name) {

View File

@ -11,10 +11,12 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.security.saml2.core.Saml2X509Credential; import org.springframework.security.saml2.core.Saml2X509Credential;
import org.springframework.security.saml2.core.Saml2X509Credential.Saml2X509CredentialType; import org.springframework.security.saml2.core.Saml2X509Credential.Saml2X509CredentialType;
import org.springframework.security.saml2.provider.service.authentication.AbstractSaml2AuthenticationRequest;
import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository; import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
import org.springframework.security.saml2.provider.service.web.HttpSessionSaml2AuthenticationRequestRepository;
import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver; import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
@ -39,7 +41,7 @@ public class SAML2Configuration {
@ConditionalOnProperty(name = "security.saml2.enabled", havingValue = "true") @ConditionalOnProperty(name = "security.saml2.enabled", havingValue = "true")
public RelyingPartyRegistrationRepository relyingPartyRegistrations() throws Exception { public RelyingPartyRegistrationRepository relyingPartyRegistrations() throws Exception {
SAML2 samlConf = applicationProperties.getSecurity().getSaml2(); SAML2 samlConf = applicationProperties.getSecurity().getSaml2();
X509Certificate idpCert = CertificateUtils.readCertificate(samlConf.getidpCert()); X509Certificate idpCert = CertificateUtils.readCertificate(samlConf.getIdpCert());
Saml2X509Credential verificationCredential = Saml2X509Credential.verification(idpCert); Saml2X509Credential verificationCredential = Saml2X509Credential.verification(idpCert);
Resource privateKeyResource = samlConf.getPrivateKey(); Resource privateKeyResource = samlConf.getPrivateKey();
Resource certificateResource = samlConf.getSpCert(); Resource certificateResource = samlConf.getSpCert();
@ -51,15 +53,26 @@ public class SAML2Configuration {
RelyingPartyRegistration rp = RelyingPartyRegistration rp =
RelyingPartyRegistration.withRegistrationId(samlConf.getRegistrationId()) RelyingPartyRegistration.withRegistrationId(samlConf.getRegistrationId())
.signingX509Credentials(c -> c.add(signingCredential)) .signingX509Credentials(c -> c.add(signingCredential))
.entityId(samlConf.getIdpIssuer())
.singleLogoutServiceBinding(Saml2MessageBinding.POST)
.singleLogoutServiceLocation(samlConf.getIdpSingleLogoutUrl())
.singleLogoutServiceResponseLocation("http://localhost:8080/login")
.assertionConsumerServiceBinding(Saml2MessageBinding.POST)
.assertionConsumerServiceLocation(
"{baseUrl}/login/saml2/sso/{registrationId}")
.assertingPartyMetadata( .assertingPartyMetadata(
metadata -> metadata ->
metadata.entityId(samlConf.getIdpIssuer()) metadata.entityId(samlConf.getIdpIssuer())
.singleSignOnServiceLocation(
samlConf.getIdpSingleLoginUrl())
.verificationX509Credentials( .verificationX509Credentials(
c -> c.add(verificationCredential)) c -> c.add(verificationCredential))
.singleSignOnServiceBinding( .singleSignOnServiceBinding(
Saml2MessageBinding.POST) Saml2MessageBinding.POST)
.singleSignOnServiceLocation(
samlConf.getIdpSingleLoginUrl())
.singleLogoutServiceBinding(
Saml2MessageBinding.POST)
.singleLogoutServiceLocation(
samlConf.getIdpSingleLogoutUrl())
.wantAuthnRequestsSigned(true)) .wantAuthnRequestsSigned(true))
.build(); .build();
return new InMemoryRelyingPartyRegistrationRepository(rp); return new InMemoryRelyingPartyRegistrationRepository(rp);
@ -71,58 +84,93 @@ public class SAML2Configuration {
RelyingPartyRegistrationRepository relyingPartyRegistrationRepository) { RelyingPartyRegistrationRepository relyingPartyRegistrationRepository) {
OpenSaml4AuthenticationRequestResolver resolver = OpenSaml4AuthenticationRequestResolver resolver =
new OpenSaml4AuthenticationRequestResolver(relyingPartyRegistrationRepository); new OpenSaml4AuthenticationRequestResolver(relyingPartyRegistrationRepository);
resolver.setAuthnRequestCustomizer( resolver.setAuthnRequestCustomizer(
customizer -> { customizer -> {
log.debug("Customizing SAML Authentication request");
AuthnRequest authnRequest = customizer.getAuthnRequest();
log.debug("AuthnRequest ID: {}", authnRequest.getID());
if (authnRequest.getID() == null) {
authnRequest.setID("ARQ" + UUID.randomUUID().toString());
}
log.debug("AuthnRequest new ID after set: {}", authnRequest.getID());
log.debug("AuthnRequest IssueInstant: {}", authnRequest.getIssueInstant());
log.debug(
"AuthnRequest Issuer: {}",
authnRequest.getIssuer() != null
? authnRequest.getIssuer().getValue()
: "null");
HttpServletRequest request = customizer.getRequest(); HttpServletRequest request = customizer.getRequest();
// Log HTTP request details AuthnRequest authnRequest = customizer.getAuthnRequest();
log.debug("HTTP Request Method: {}", request.getMethod()); HttpSessionSaml2AuthenticationRequestRepository requestRepository =
log.debug("Request URI: {}", request.getRequestURI()); new HttpSessionSaml2AuthenticationRequestRepository();
log.debug("Request URL: {}", request.getRequestURL().toString()); AbstractSaml2AuthenticationRequest saml2AuthenticationRequest =
log.debug("Query String: {}", request.getQueryString()); requestRepository.loadAuthenticationRequest(request);
log.debug("Remote Address: {}", request.getRemoteAddr());
// Log headers if (saml2AuthenticationRequest != null) {
Collections.list(request.getHeaderNames()) String sessionId = request.getSession(false).getId();
.forEach(
headerName -> {
log.debug(
"Header - {}: {}",
headerName,
request.getHeader(headerName));
});
// Log SAML specific parameters
log.debug("SAML Request Parameters:");
log.debug("SAMLRequest: {}", request.getParameter("SAMLRequest"));
log.debug("RelayState: {}", request.getParameter("RelayState"));
// Log session debugrmation if exists
if (request.getSession(false) != null) {
log.debug("Session ID: {}", request.getSession().getId());
}
// Log any assertions consumer service details if present
if (authnRequest.getAssertionConsumerServiceURL() != null) {
log.debug( log.debug(
"AssertionConsumerServiceURL: {}", "Retrieving SAML 2 authentication request ID from the current HTTP session {}",
authnRequest.getAssertionConsumerServiceURL()); sessionId);
}
// Log NameID policy if present String authenticationRequestId = saml2AuthenticationRequest.getId();
if (authnRequest.getNameIDPolicy() != null) {
log.debug( if (!authenticationRequestId.isBlank()) {
"NameIDPolicy Format: {}", authnRequest.setID(authenticationRequestId);
authnRequest.getNameIDPolicy().getFormat()); } else {
log.warn(
"No authentication request found for HTTP session {}. Generating new ID",
sessionId);
authnRequest.setID("ARQ" + UUID.randomUUID().toString().substring(1));
}
} else {
log.debug("Generating new authentication request ID");
authnRequest.setID("ARQ" + UUID.randomUUID().toString().substring(1));
} }
logAuthnRequestDetails(authnRequest);
logHttpRequestDetails(request);
}); });
return resolver; return resolver;
} }
private static void logAuthnRequestDetails(AuthnRequest authnRequest) {
String message =
"""
AuthnRequest:
ID: {}
Issuer: {}
IssueInstant: {}
AssertionConsumerService (ACS) URL: {}
""";
log.debug(
message,
authnRequest.getID(),
authnRequest.getIssuer() != null ? authnRequest.getIssuer().getValue() : null,
authnRequest.getIssueInstant(),
authnRequest.getAssertionConsumerServiceURL());
if (authnRequest.getNameIDPolicy() != null) {
log.debug("NameIDPolicy Format: {}", authnRequest.getNameIDPolicy().getFormat());
}
}
private static void logHttpRequestDetails(HttpServletRequest request) {
log.debug("HTTP Headers: ");
Collections.list(request.getHeaderNames())
.forEach(
headerName ->
log.debug("{}: {}", headerName, request.getHeader(headerName)));
String message =
"""
HTTP Request Method: {}
Session ID: {}
Request Path: {}
Query String: {}
Remote Address: {}
SAML Request Parameters:
SAMLRequest: {}
RelayState: {}
""";
log.debug(
message,
request.getMethod(),
request.getSession().getId(),
request.getRequestURI(),
request.getQueryString(),
request.getRemoteAddr(),
request.getParameter("SAMLRequest"),
request.getParameter("RelayState"));
}
} }

View File

@ -126,7 +126,7 @@ public class UserController {
return new RedirectView("/change-creds?messageType=notAuthenticated", true); return new RedirectView("/change-creds?messageType=notAuthenticated", true);
} }
Optional<User> userOpt = userService.findByUsernameIgnoreCase(principal.getName()); Optional<User> userOpt = userService.findByUsernameIgnoreCase(principal.getName());
if (userOpt == null || userOpt.isEmpty()) { if (userOpt.isEmpty()) {
return new RedirectView("/change-creds?messageType=userNotFound", true); return new RedirectView("/change-creds?messageType=userNotFound", true);
} }
User user = userOpt.get(); User user = userOpt.get();
@ -154,7 +154,7 @@ public class UserController {
return new RedirectView("/account?messageType=notAuthenticated", true); return new RedirectView("/account?messageType=notAuthenticated", true);
} }
Optional<User> userOpt = userService.findByUsernameIgnoreCase(principal.getName()); Optional<User> userOpt = userService.findByUsernameIgnoreCase(principal.getName());
if (userOpt == null || userOpt.isEmpty()) { if (userOpt.isEmpty()) {
return new RedirectView("/account?messageType=userNotFound", true); return new RedirectView("/account?messageType=userNotFound", true);
} }
User user = userOpt.get(); User user = userOpt.get();
@ -176,7 +176,7 @@ public class UserController {
for (Map.Entry<String, String[]> entry : paramMap.entrySet()) { for (Map.Entry<String, String[]> entry : paramMap.entrySet()) {
updates.put(entry.getKey(), entry.getValue()[0]); updates.put(entry.getKey(), entry.getValue()[0]);
} }
log.debug("Processed updates: " + updates); log.debug("Processed updates: {}", updates);
// Assuming you have a method in userService to update the settings for a user // Assuming you have a method in userService to update the settings for a user
userService.updateUserSettings(principal.getName(), updates); userService.updateUserSettings(principal.getName(), updates);
// Redirect to a page of your choice after updating // Redirect to a page of your choice after updating
@ -199,7 +199,7 @@ public class UserController {
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username); Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
if (userOpt.isPresent()) { if (userOpt.isPresent()) {
User user = userOpt.get(); User user = userOpt.get();
if (user != null && user.getUsername().equalsIgnoreCase(username)) { if (user.getUsername().equalsIgnoreCase(username)) {
return new RedirectView("/addUsers?messageType=usernameExists", true); return new RedirectView("/addUsers?messageType=usernameExists", true);
} }
} }
@ -276,7 +276,7 @@ public class UserController {
Authentication authentication) Authentication authentication)
throws SQLException, UnsupportedProviderException { throws SQLException, UnsupportedProviderException {
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username); Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
if (!userOpt.isPresent()) { if (userOpt.isEmpty()) {
return new RedirectView("/addUsers?messageType=userNotFound", true); return new RedirectView("/addUsers?messageType=userNotFound", true);
} }
if (!userService.usernameExistsIgnoreCase(username)) { if (!userService.usernameExistsIgnoreCase(username)) {
@ -295,7 +295,7 @@ public class UserController {
List<Object> principals = sessionRegistry.getAllPrincipals(); List<Object> principals = sessionRegistry.getAllPrincipals();
String userNameP = ""; String userNameP = "";
for (Object principal : principals) { for (Object principal : principals) {
List<SessionInformation> sessionsInformations = List<SessionInformation> sessionsInformation =
sessionRegistry.getAllSessions(principal, false); sessionRegistry.getAllSessions(principal, false);
if (principal instanceof UserDetails) { if (principal instanceof UserDetails) {
userNameP = ((UserDetails) principal).getUsername(); userNameP = ((UserDetails) principal).getUsername();
@ -307,8 +307,8 @@ public class UserController {
userNameP = (String) principal; userNameP = (String) principal;
} }
if (userNameP.equalsIgnoreCase(username)) { if (userNameP.equalsIgnoreCase(username)) {
for (SessionInformation sessionsInformation : sessionsInformations) { for (SessionInformation sessionInfo : sessionsInformation) {
sessionRegistry.expireSession(sessionsInformation.getSessionId()); sessionRegistry.expireSession(sessionInfo.getSessionId());
} }
} }
} }

View File

@ -4,7 +4,12 @@ import static stirling.software.SPDF.utils.validation.Validator.validateProvider
import java.time.Instant; import java.time.Instant;
import java.time.temporal.ChronoUnit; import java.time.temporal.ChronoUnit;
import java.util.*; import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
@ -26,11 +31,15 @@ import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal; import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry; import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
import stirling.software.SPDF.model.*; import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.ApplicationProperties.Security; import stirling.software.SPDF.model.ApplicationProperties.Security;
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2; import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2.Client; import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2.Client;
import stirling.software.SPDF.model.ApplicationProperties.Security.SAML2; import stirling.software.SPDF.model.ApplicationProperties.Security.SAML2;
import stirling.software.SPDF.model.Authority;
import stirling.software.SPDF.model.Role;
import stirling.software.SPDF.model.SessionEntity;
import stirling.software.SPDF.model.User;
import stirling.software.SPDF.model.provider.GitHubProvider; import stirling.software.SPDF.model.provider.GitHubProvider;
import stirling.software.SPDF.model.provider.GoogleProvider; import stirling.software.SPDF.model.provider.GoogleProvider;
import stirling.software.SPDF.model.provider.KeycloakProvider; import stirling.software.SPDF.model.provider.KeycloakProvider;
@ -74,7 +83,7 @@ public class AccountWebController {
String firstChar = String.valueOf(oauth.getProvider().charAt(0)); String firstChar = String.valueOf(oauth.getProvider().charAt(0));
String clientName = String clientName =
oauth.getProvider().replaceFirst(firstChar, firstChar.toUpperCase()); oauth.getProvider().replaceFirst(firstChar, firstChar.toUpperCase());
providerList.put(OAUTH_2_AUTHORIZATION + "oidc", clientName); providerList.put(OAUTH_2_AUTHORIZATION + oauth.getProvider(), clientName);
} }
Client client = oauth.getClient(); Client client = oauth.getClient();
@ -108,8 +117,16 @@ public class AccountWebController {
SAML2 saml2 = securityProps.getSaml2(); SAML2 saml2 = securityProps.getSaml2();
if (securityProps.isSaml2Active() if (securityProps.isSaml2Active()
&& applicationProperties.getSystem().getEnableAlphaFunctionality()) { && applicationProperties.getSystem().getEnableAlphaFunctionality()
providerList.put("/saml2/authenticate/" + saml2.getRegistrationId(), "SAML 2"); && applicationProperties.getEnterpriseEdition().isEnabled()) {
String samlIdp = saml2.getProvider();
String saml2AuthenticationPath = "/saml2/authenticate/" + saml2.getRegistrationId();
if (applicationProperties.getEnterpriseEdition().isSsoAutoLogin()) {
return "redirect:login" + saml2AuthenticationPath;
} else {
providerList.put(saml2AuthenticationPath, samlIdp + " (SAML 2)");
}
} }
// Remove any null keys/values from the providerList // Remove any null keys/values from the providerList
@ -134,7 +151,9 @@ public class AccountWebController {
model.addAttribute("error", error); model.addAttribute("error", error);
} }
String errorOAuth = request.getParameter("errorOAuth"); String errorOAuth = request.getParameter("errorOAuth");
if (errorOAuth != null) { if (errorOAuth != null) {
switch (errorOAuth) { switch (errorOAuth) {
case "oAuth2AutoCreateDisabled" -> errorOAuth = "login.oAuth2AutoCreateDisabled"; case "oAuth2AutoCreateDisabled" -> errorOAuth = "login.oAuth2AutoCreateDisabled";
@ -142,19 +161,23 @@ public class AccountWebController {
case "userAlreadyExistsWeb" -> errorOAuth = "userAlreadyExistsWebMessage"; case "userAlreadyExistsWeb" -> errorOAuth = "userAlreadyExistsWebMessage";
case "oAuth2AuthenticationErrorWeb" -> errorOAuth = "login.oauth2InvalidUserType"; case "oAuth2AuthenticationErrorWeb" -> errorOAuth = "login.oauth2InvalidUserType";
case "invalid_token_response" -> errorOAuth = "login.oauth2InvalidTokenResponse"; case "invalid_token_response" -> errorOAuth = "login.oauth2InvalidTokenResponse";
case "authorization_request_not_found" -> errorOAuth = "login.oauth2RequestNotFound"; case "authorization_request_not_found" ->
errorOAuth = "login.oauth2RequestNotFound";
case "access_denied" -> errorOAuth = "login.oauth2AccessDenied"; case "access_denied" -> errorOAuth = "login.oauth2AccessDenied";
case "invalid_user_info_response" -> errorOAuth = "login.oauth2InvalidUserInfoResponse"; case "invalid_user_info_response" ->
errorOAuth = "login.oauth2InvalidUserInfoResponse";
case "invalid_request" -> errorOAuth = "login.oauth2invalidRequest"; case "invalid_request" -> errorOAuth = "login.oauth2invalidRequest";
case "invalid_id_token" -> errorOAuth = "login.oauth2InvalidIdToken"; case "invalid_id_token" -> errorOAuth = "login.oauth2InvalidIdToken";
case "oAuth2AdminBlockedUser" -> errorOAuth = "login.oAuth2AdminBlockedUser"; case "oAuth2AdminBlockedUser" -> errorOAuth = "login.oAuth2AdminBlockedUser";
case "userIsDisabled" -> errorOAuth = "login.userIsDisabled"; case "userIsDisabled" -> errorOAuth = "login.userIsDisabled";
case "invalid_destination" -> errorOAuth = "login.invalid_destination"; case "invalid_destination" -> errorOAuth = "login.invalid_destination";
case "relying_party_registration_not_found" -> errorOAuth = "login.relyingPartyRegistrationNotFound"; case "relying_party_registration_not_found" ->
errorOAuth = "login.relyingPartyRegistrationNotFound";
// Valid InResponseTo was not available from the validation context, unable to // Valid InResponseTo was not available from the validation context, unable to
// evaluate // evaluate
case "invalid_in_response_to" -> errorOAuth = "login.invalid_in_response_to"; case "invalid_in_response_to" -> errorOAuth = "login.invalid_in_response_to";
case "not_authentication_provider_found" -> errorOAuth = "login.not_authentication_provider_found"; case "not_authentication_provider_found" ->
errorOAuth = "login.not_authentication_provider_found";
} }
model.addAttribute("errorOAuth", errorOAuth); model.addAttribute("errorOAuth", errorOAuth);
@ -211,13 +234,11 @@ public class AccountWebController {
.plus(maxInactiveInterval, ChronoUnit.SECONDS); .plus(maxInactiveInterval, ChronoUnit.SECONDS);
if (now.isAfter(expirationTime)) { if (now.isAfter(expirationTime)) {
sessionPersistentRegistry.expireSession(sessionEntity.getSessionId()); sessionPersistentRegistry.expireSession(sessionEntity.getSessionId());
hasActiveSession = false;
} else { } else {
hasActiveSession = !sessionEntity.isExpired(); hasActiveSession = !sessionEntity.isExpired();
} }
lastRequest = sessionEntity.getLastRequest(); lastRequest = sessionEntity.getLastRequest();
} else { } else {
hasActiveSession = false;
// No session, set default last request time // No session, set default last request time
lastRequest = new Date(0); lastRequest = new Date(0);
} }
@ -254,53 +275,41 @@ public class AccountWebController {
}) })
.collect(Collectors.toList()); .collect(Collectors.toList());
String messageType = request.getParameter("messageType"); String messageType = request.getParameter("messageType");
String deleteMessage = null;
String deleteMessage;
if (messageType != null) { if (messageType != null) {
switch (messageType) { deleteMessage =
case "deleteCurrentUser": switch (messageType) {
deleteMessage = "deleteCurrentUserMessage"; case "deleteCurrentUser" -> "deleteCurrentUserMessage";
break; case "deleteUsernameExists" -> "deleteUsernameExistsMessage";
case "deleteUsernameExists": default -> null;
deleteMessage = "deleteUsernameExistsMessage"; };
break;
default:
break;
}
model.addAttribute("deleteMessage", deleteMessage); model.addAttribute("deleteMessage", deleteMessage);
String addMessage = null;
switch (messageType) { String addMessage;
case "usernameExists": addMessage =
addMessage = "usernameExistsMessage"; switch (messageType) {
break; case "usernameExists" -> "usernameExistsMessage";
case "invalidUsername": case "invalidUsername" -> "invalidUsernameMessage";
addMessage = "invalidUsernameMessage"; case "invalidPassword" -> "invalidPasswordMessage";
break; default -> null;
case "invalidPassword": };
addMessage = "invalidPasswordMessage";
break;
default:
break;
}
model.addAttribute("addMessage", addMessage); model.addAttribute("addMessage", addMessage);
} }
String changeMessage = null;
String changeMessage;
if (messageType != null) { if (messageType != null) {
switch (messageType) { changeMessage =
case "userNotFound": switch (messageType) {
changeMessage = "userNotFoundMessage"; case "userNotFound" -> "userNotFoundMessage";
break; case "downgradeCurrentUser" -> "downgradeCurrentUserMessage";
case "downgradeCurrentUser": case "disabledCurrentUser" -> "disabledCurrentUserMessage";
changeMessage = "downgradeCurrentUserMessage"; default -> messageType;
break; };
case "disabledCurrentUser":
changeMessage = "disabledCurrentUserMessage";
break;
default:
changeMessage = messageType;
break;
}
model.addAttribute("changeMessage", changeMessage); model.addAttribute("changeMessage", changeMessage);
} }
model.addAttribute("users", sortedUsers); model.addAttribute("users", sortedUsers);
model.addAttribute("currentUsername", authentication.getName()); model.addAttribute("currentUsername", authentication.getName());
model.addAttribute("roleDetails", roleDetails); model.addAttribute("roleDetails", roleDetails);
@ -321,39 +330,35 @@ public class AccountWebController {
if (authentication != null && authentication.isAuthenticated()) { if (authentication != null && authentication.isAuthenticated()) {
Object principal = authentication.getPrincipal(); Object principal = authentication.getPrincipal();
String username = null; String username = null;
// Retrieve username and other attributes and add login attributes to the model
if (principal instanceof UserDetails userDetails) { if (principal instanceof UserDetails userDetails) {
// Retrieve username and other attributes
username = userDetails.getUsername(); username = userDetails.getUsername();
// Add oAuth2 Login attributes to the model
model.addAttribute("oAuth2Login", false); model.addAttribute("oAuth2Login", false);
} }
if (principal instanceof OAuth2User userDetails) { if (principal instanceof OAuth2User userDetails) {
// Retrieve username and other attributes
username = userDetails.getName(); username = userDetails.getName();
// Add oAuth2 Login attributes to the model
model.addAttribute("oAuth2Login", true); model.addAttribute("oAuth2Login", true);
} }
if (principal instanceof CustomSaml2AuthenticatedPrincipal userDetails) { if (principal instanceof CustomSaml2AuthenticatedPrincipal userDetails) {
// Retrieve username and other attributes
username = userDetails.getName(); username = userDetails.getName();
// Add oAuth2 Login attributes to the model model.addAttribute("saml2Login", true);
model.addAttribute("oAuth2Login", true);
} }
if (username != null) { if (username != null) {
// Fetch user details from the database, assuming findByUsername method exists // Fetch user details from the database
Optional<User> user = userRepository.findByUsernameIgnoreCaseWithSettings(username); Optional<User> user = userRepository.findByUsernameIgnoreCaseWithSettings(username);
if (!user.isPresent()) { if (user.isEmpty()) {
return "redirect:/error"; return "redirect:/error";
} }
// Convert settings map to JSON string // Convert settings map to JSON string
ObjectMapper objectMapper = new ObjectMapper(); ObjectMapper objectMapper = new ObjectMapper();
String settingsJson; String settingsJson;
try { try {
settingsJson = objectMapper.writeValueAsString(user.get().getSettings()); settingsJson = objectMapper.writeValueAsString(user.get().getSettings());
} catch (JsonProcessingException e) { } catch (JsonProcessingException e) {
// Handle JSON conversion error log.error("Error converting settings map", e);
log.error("exception", e);
return "redirect:/error"; return "redirect:/error";
} }
@ -367,7 +372,7 @@ public class AccountWebController {
case "invalidUsername" -> messageType = "invalidUsernameMessage"; case "invalidUsername" -> messageType = "invalidUsernameMessage";
} }
} }
// Add attributes to the model
model.addAttribute("username", username); model.addAttribute("username", username);
model.addAttribute("messageType", messageType); model.addAttribute("messageType", messageType);
model.addAttribute("role", user.get().getRolesAsString()); model.addAttribute("role", user.get().getRolesAsString());
@ -390,19 +395,12 @@ public class AccountWebController {
} }
if (authentication != null && authentication.isAuthenticated()) { if (authentication != null && authentication.isAuthenticated()) {
Object principal = authentication.getPrincipal(); Object principal = authentication.getPrincipal();
if (principal instanceof UserDetails) { if (principal instanceof UserDetails userDetails) {
// Cast the principal object to UserDetails
UserDetails userDetails = (UserDetails) principal;
// Retrieve username and other attributes
String username = userDetails.getUsername(); String username = userDetails.getUsername();
// Fetch user details from the database // Fetch user details from the database
Optional<User> user = Optional<User> user = userRepository.findByUsernameIgnoreCase(username);
userRepository if (user.isEmpty()) {
.findByUsernameIgnoreCase( // Assuming findByUsername method exists // Handle error appropriately, example redirection in case of error
username);
if (!user.isPresent()) {
// Handle error appropriately
// Example redirection in case of error
return "redirect:/error"; return "redirect:/error";
} }
String messageType = request.getParameter("messageType"); String messageType = request.getParameter("messageType");
@ -425,7 +423,7 @@ public class AccountWebController {
} }
model.addAttribute("messageType", messageType); model.addAttribute("messageType", messageType);
} }
// Add attributes to the model
model.addAttribute("username", username); model.addAttribute("username", username);
} }
} else { } else {

View File

@ -14,7 +14,6 @@ import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
@ -110,7 +109,7 @@ public class ApplicationProperties {
private int loginAttemptCount; private int loginAttemptCount;
private long loginResetTimeMinutes; private long loginResetTimeMinutes;
private String loginMethod = "all"; private String loginMethod = "all";
private String customGlobalAPIKey; private String customGlobalAPIKey; // todo: expose?
public Boolean isAltLogin() { public Boolean isAltLogin() {
return saml2.getEnabled() || oauth2.getEnabled(); return saml2.getEnabled() || oauth2.getEnabled();
@ -139,13 +138,13 @@ public class ApplicationProperties {
|| loginMethod.equalsIgnoreCase(LoginMethods.ALL.toString())); || loginMethod.equalsIgnoreCase(LoginMethods.ALL.toString()));
} }
public boolean isOauth2Activ() { public boolean isOauth2Active() {
return (oauth2 != null return (oauth2 != null
&& oauth2.getEnabled() && oauth2.getEnabled()
&& !loginMethod.equalsIgnoreCase(LoginMethods.NORMAL.toString())); && !loginMethod.equalsIgnoreCase(LoginMethods.NORMAL.toString()));
} }
public boolean isSaml2Activ() { public boolean isSaml2Active() {
return (saml2 != null return (saml2 != null
&& saml2.getEnabled() && saml2.getEnabled()
&& !loginMethod.equalsIgnoreCase(LoginMethods.NORMAL.toString())); && !loginMethod.equalsIgnoreCase(LoginMethods.NORMAL.toString()));
@ -161,6 +160,7 @@ public class ApplicationProperties {
@Setter @Setter
@ToString @ToString
public static class SAML2 { public static class SAML2 {
private String provider;
private Boolean enabled = false; private Boolean enabled = false;
private Boolean autoCreateUser = false; private Boolean autoCreateUser = false;
private Boolean blockRegistration = false; private Boolean blockRegistration = false;
@ -198,7 +198,7 @@ public class ApplicationProperties {
} }
} }
public Resource getidpCert() { public Resource getIdpCert() {
if (idpCert == null) return null; if (idpCert == null) return null;
if (idpCert.startsWith("classpath:")) { if (idpCert.startsWith("classpath:")) {
return new ClassPathResource(idpCert.substring("classpath:".length())); return new ClassPathResource(idpCert.substring("classpath:".length()));
@ -228,12 +228,11 @@ public class ApplicationProperties {
private Collection<String> scopes = new ArrayList<>(); private Collection<String> scopes = new ArrayList<>();
private String provider; private String provider;
private Client client = new Client(); private Client client = new Client();
private String logoutUrl;
public void setScopes(String scopes) { public void setScopes(String scopes) {
List<String> scopesList = List<String> scopesList =
Arrays.stream(scopes.split(",")) Arrays.stream(scopes.split(",")).map(String::trim).toList();
.map(String::trim)
.toList();
this.scopes.addAll(scopesList); this.scopes.addAll(scopesList);
} }
@ -266,7 +265,9 @@ public class ApplicationProperties {
case "keycloak" -> getKeycloak(); case "keycloak" -> getKeycloak();
default -> default ->
throw new UnsupportedProviderException( throw new UnsupportedProviderException(
"Logout from the provider " + registrationId + " is not supported. " "Logout from the provider "
+ registrationId
+ " is not supported. "
+ "Report it at https://github.com/Stirling-Tools/Stirling-PDF/issues"); + "Report it at https://github.com/Stirling-Tools/Stirling-PDF/issues");
}; };
} }

View File

@ -4,14 +4,17 @@ import lombok.Getter;
@Getter @Getter
public enum UsernameAttribute { public enum UsernameAttribute {
NAME("name"),
EMAIL("email"), EMAIL("email"),
GIVEN_NAME("given_name"),
PREFERRED_NAME("preferred_name"),
PREFERRED_USERNAME("preferred_username"),
LOGIN("login"), LOGIN("login"),
PROFILE("profile"),
NAME("name"),
USERNAME("username"),
NICKNAME("nickname"),
GIVEN_NAME("given_name"),
MIDDLE_NAME("middle_name"),
FAMILY_NAME("family_name"), FAMILY_NAME("family_name"),
NICKNAME("nickname"); PREFERRED_NAME("preferred_name"),
PREFERRED_USERNAME("preferred_username");
private final String name; private final String name;

View File

@ -28,6 +28,7 @@ public class GitHubProvider extends Provider {
clientSecret, clientSecret,
scopes, scopes,
useAsUsername != null ? useAsUsername : UsernameAttribute.LOGIN, useAsUsername != null ? useAsUsername : UsernameAttribute.LOGIN,
null,
AUTHORIZATION_URI, AUTHORIZATION_URI,
TOKEN_URI, TOKEN_URI,
USER_INFO_URI); USER_INFO_URI);

View File

@ -29,6 +29,7 @@ public class GoogleProvider extends Provider {
clientSecret, clientSecret,
scopes, scopes,
useAsUsername, useAsUsername,
null,
AUTHORIZATION_URI, AUTHORIZATION_URI,
TOKEN_URI, TOKEN_URI,
USER_INFO_URI); USER_INFO_URI);

View File

@ -28,6 +28,7 @@ public class KeycloakProvider extends Provider {
useAsUsername, useAsUsername,
null, null,
null, null,
null,
null); null);
} }

View File

@ -16,6 +16,8 @@ import stirling.software.SPDF.model.exception.UnsupportedUsernameAttribute;
@NoArgsConstructor @NoArgsConstructor
public class Provider { public class Provider {
public static final String EXCEPTION_MESSAGE = "The attribute %s is not supported for %s.";
private String issuer; private String issuer;
private String name; private String name;
private String clientName; private String clientName;
@ -23,6 +25,7 @@ public class Provider {
private String clientSecret; private String clientSecret;
private Collection<String> scopes; private Collection<String> scopes;
private UsernameAttribute useAsUsername; private UsernameAttribute useAsUsername;
private String logoutUrl;
private String authorizationUri; private String authorizationUri;
private String tokenUri; private String tokenUri;
private String userInfoUri; private String userInfoUri;
@ -35,6 +38,7 @@ public class Provider {
String clientSecret, String clientSecret,
Collection<String> scopes, Collection<String> scopes,
UsernameAttribute useAsUsername, UsernameAttribute useAsUsername,
String logoutUrl,
String authorizationUri, String authorizationUri,
String tokenUri, String tokenUri,
String userInfoUri) { String userInfoUri) {
@ -46,6 +50,7 @@ public class Provider {
this.scopes = scopes == null ? new ArrayList<>() : scopes; this.scopes = scopes == null ? new ArrayList<>() : scopes;
this.useAsUsername = this.useAsUsername =
useAsUsername != null ? validateUsernameAttribute(useAsUsername) : EMAIL; useAsUsername != null ? validateUsernameAttribute(useAsUsername) : EMAIL;
this.logoutUrl = logoutUrl;
this.authorizationUri = authorizationUri; this.authorizationUri = authorizationUri;
this.tokenUri = tokenUri; this.tokenUri = tokenUri;
this.userInfoUri = userInfoUri; this.userInfoUri = userInfoUri;
@ -83,41 +88,29 @@ public class Provider {
} }
default -> default ->
throw new UnsupportedUsernameAttribute( throw new UnsupportedUsernameAttribute(
"The attribute " String.format(EXCEPTION_MESSAGE, usernameAttribute, clientName));
+ usernameAttribute
+ "is not supported for "
+ clientName
+ ".");
} }
} }
private UsernameAttribute validateGoogleUsernameAttribute(UsernameAttribute usernameAttribute) { private UsernameAttribute validateGoogleUsernameAttribute(UsernameAttribute usernameAttribute) {
switch (usernameAttribute) { switch (usernameAttribute) {
case EMAIL, NAME, GIVEN_NAME, PREFERRED_NAME -> { case EMAIL, NAME, GIVEN_NAME, FAMILY_NAME -> {
return usernameAttribute; return usernameAttribute;
} }
default -> default ->
throw new UnsupportedUsernameAttribute( throw new UnsupportedUsernameAttribute(
"The attribute " String.format(EXCEPTION_MESSAGE, usernameAttribute, clientName));
+ usernameAttribute
+ "is not supported for "
+ clientName
+ ".");
} }
} }
private UsernameAttribute validateGitHubUsernameAttribute(UsernameAttribute usernameAttribute) { private UsernameAttribute validateGitHubUsernameAttribute(UsernameAttribute usernameAttribute) {
switch (usernameAttribute) { switch (usernameAttribute) {
case EMAIL, NAME, LOGIN -> { case LOGIN, EMAIL, NAME -> {
return usernameAttribute; return usernameAttribute;
} }
default -> default ->
throw new UnsupportedUsernameAttribute( throw new UnsupportedUsernameAttribute(
"The attribute " String.format(EXCEPTION_MESSAGE, usernameAttribute, clientName));
+ usernameAttribute
+ "is not supported for "
+ clientName
+ ".");
} }
} }

View File

@ -16,7 +16,7 @@ security:
csrfDisabled: false # set to 'true' to disable CSRF protection (not recommended for production) csrfDisabled: false # set to 'true' to disable CSRF protection (not recommended for production)
loginAttemptCount: 5 # lock user account after 5 tries; when using e.g. Fail2Ban you can deactivate the function with -1 loginAttemptCount: 5 # lock user account after 5 tries; when using e.g. Fail2Ban you can deactivate the function with -1
loginResetTimeMinutes: 120 # lock account for 2 hours after x attempts loginResetTimeMinutes: 120 # lock account for 2 hours after x attempts
loginMethod: saml2 # Accepts values like 'all' and 'normal'(only Login with Username/Password), 'oauth2'(only Login with OAuth2) or 'saml2'(only Login with SAML2) loginMethod: all # Accepts values like 'all' and 'normal'(only Login with Username/Password), 'oauth2'(only Login with OAuth2) or 'saml2'(only Login with SAML2)
initialLogin: initialLogin:
username: '' # initial username for the first login username: '' # initial username for the first login
password: '' # initial password for the first login password: '' # initial password for the first login
@ -28,42 +28,44 @@ security:
clientId: '' # client ID for Keycloak OAuth2 clientId: '' # client ID for Keycloak OAuth2
clientSecret: '' # client secret for Keycloak OAuth2 clientSecret: '' # client secret for Keycloak OAuth2
scopes: openid, profile, email # scopes for Keycloak OAuth2 scopes: openid, profile, email # scopes for Keycloak OAuth2
useAsUsername: preferred_username # field to use as the username for Keycloak OAuth2 useAsUsername: preferred_username # field to use as the username for Keycloak OAuth2. Available options are: [email | preferred_name]
google: google:
clientId: '' # client ID for Google OAuth2 clientId: '' # client ID for Google OAuth2
clientSecret: '' # client secret for Google OAuth2 clientSecret: '' # client secret for Google OAuth2
scopes: email, profile # scopes for Google OAuth2 scopes: email, profile # scopes for Google OAuth2
useAsUsername: email # field to use as the username for Google OAuth2 useAsUsername: email # field to use as the username for Google OAuth2. Available options are: [email | name | given_name | family_name]
github: github:
clientId: '' # client ID for GitHub OAuth2 clientId: '' # client ID for GitHub OAuth2
clientSecret: '' # client secret for GitHub OAuth2 clientSecret: '' # client secret for GitHub OAuth2
scopes: read:user # scope for GitHub OAuth2 scopes: read:user # scope for GitHub OAuth2
useAsUsername: login # field to use as the username for GitHub OAuth2 useAsUsername: login # field to use as the username for GitHub OAuth2. Available options are: [email | login | name]
issuer: '' # set to any provider that supports OpenID Connect Discovery (/.well-known/openid-configuration) endpoint issuer: https://trial-6373896.okta.com/home/okta_flow_sso/0oaok4lk1nVvNBnqK697/alnbibn6b0OPFATt20g7 # set to any provider that supports OpenID Connect Discovery (/.well-known/openid-configuration) endpoint
clientId: '' # client ID from your provider clientId: 0oaok4lk4eNm6PtFD697 # client ID from your provider
clientSecret: '' # client secret from your provider clientSecret: lmwlmxFZSJ0miOoRpUAKf2jg8tVPPXhUxgL2VB-b4uJfhnk4sI02YodKWRX8fLSq # client secret from your provider
logoutUrl: ''
autoCreateUser: true # set to 'true' to allow auto-creation of non-existing users autoCreateUser: true # set to 'true' to allow auto-creation of non-existing users
blockRegistration: false # set to 'true' to deny login with SSO without prior registration by an admin blockRegistration: false # set to 'true' to deny login with SSO without prior registration by an admin
useAsUsername: email # default is 'email'; custom fields can be used as the username useAsUsername: username # default is 'email'; custom fields can be used as the username
scopes: openid, profile, email # specify the scopes for which the application will request permissions scopes: okta.users.read, okta.users.read.self, okta.users.manage.self, okta.groups.read # specify the scopes for which the application will request permissions
provider: google # set this to your OAuth provider's name, e.g., 'google' or 'keycloak' provider: google # set this to your OAuth provider's name, e.g., 'google' or 'keycloak'
saml2: saml2:
enabled: false # Only enabled for paid enterprise clients (enterpriseEdition.enabled must be true) enabled: true # Only enabled for paid enterprise clients (enterpriseEdition.enabled must be true)
provider: okta
autoCreateUser: true # set to 'true' to allow auto-creation of non-existing users autoCreateUser: true # set to 'true' to allow auto-creation of non-existing users
blockRegistration: false # set to 'true' to deny login with SSO without prior registration by an admin blockRegistration: false # set to 'true' to deny login with SSO without prior registration by an admin
registrationId: stirling registrationId: stirlingpdf-dario-saml
idpMetadataUri: https://dev-XXXXXXXX.okta.com/app/externalKey/sso/saml/metadata idpMetadataUri: https://trial-6373896.okta.com/app/exkomkf71reALy12X697/sso/saml/metadata # todo: remove
idpSingleLogoutUrl: https://dev-XXXXXXXX.okta.com/app/dev-XXXXXXXX_stirlingpdf_1/externalKey/slo/saml idpSingleLoginUrl: https://trial-6373896.okta.com/app/trial-6373896_stirlingpdfsaml2_1/exkoot0g5ipqOF3Bo697/sso/saml # todo: remove
idpSingleLoginUrl: https://dev-XXXXXXXX.okta.com/app/dev-XXXXXXXX_stirlingpdf_1/externalKey/sso/saml idpSingleLogoutUrl: https://trial-6373896.okta.com/app/trial-6373896_stirlingpdfsaml2_1/exkoot0g5ipqOF3Bo697/slo/saml # todo: remove
idpIssuer: http://www.okta.com/externalKey idpIssuer: http://www.okta.com/exkoot0g5ipqOF3Bo697
idpCert: classpath:okta.crt idpCert: classpath:okta.cert
privateKey: classpath:saml-private-key.key privateKey: classpath:private_key.key
spCert: classpath:saml-public-cert.crt spCert: classpath:certificate.crt
enterpriseEdition: enterpriseEdition:
enabled: false # set to 'true' to enable enterprise edition enabled: true # set to 'true' to enable enterprise edition
key: 00000000-0000-0000-0000-000000000000 key: 00000000-0000-0000-0000-000000000000
SSOAutoLogin: false # Enable to auto login to first provided SSO SSOAutoLogin: true # Enable to auto login to first provided SSO
CustomMetadata: CustomMetadata:
autoUpdateMetadata: false # set to 'true' to automatically update metadata with below values autoUpdateMetadata: false # set to 'true' to automatically update metadata with below values
author: username # supports text such as 'John Doe' or types such as username to autopopulate with user's username author: username # supports text such as 'John Doe' or types such as username to autopopulate with user's username
@ -80,7 +82,7 @@ legal:
system: system:
defaultLocale: en-US # set the default language (e.g. 'de-DE', 'fr-FR', etc) defaultLocale: en-US # set the default language (e.g. 'de-DE', 'fr-FR', etc)
googlevisibility: false # 'true' to allow Google visibility (via robots.txt), 'false' to disallow googlevisibility: false # 'true' to allow Google visibility (via robots.txt), 'false' to disallow
enableAlphaFunctionality: false # set to enable functionality which might need more testing before it fully goes live (this feature might make no changes) enableAlphaFunctionality: true # set to enable functionality which might need more testing before it fully goes live (this feature might make no changes)
showUpdate: false # see when a new update is available showUpdate: false # see when a new update is available
showUpdateOnlyAdmin: false # only admins can see when a new update is available, depending on showUpdate it must be set to 'true' showUpdateOnlyAdmin: false # only admins can see when a new update is available, depending on showUpdate it must be set to 'true'
customHTMLFiles: false # enable to have files placed in /customFiles/templates override the existing template HTML files customHTMLFiles: false # enable to have files placed in /customFiles/templates override the existing template HTML files

View File

@ -34,7 +34,7 @@
</th:block> </th:block>
<!-- Change Username Form --> <!-- Change Username Form -->
<th:block th:if="${!oAuth2Login}"> <th:block th:if="not ${oAuth2Login} or not ${saml2Login}">
<h4 th:text="#{account.changeUsername}">Change Username?</h4> <h4 th:text="#{account.changeUsername}">Change Username?</h4>
<form id="formsavechangeusername" class="bg-card mt-4 mb-4" th:action="@{'/api/v1/user/change-username'}" method="post"> <form id="formsavechangeusername" class="bg-card mt-4 mb-4" th:action="@{'/api/v1/user/change-username'}" method="post">
<div class="mb-3"> <div class="mb-3">
@ -53,7 +53,7 @@
</th:block> </th:block>
<!-- Change Password Form --> <!-- Change Password Form -->
<th:block th:if="${!oAuth2Login}"> <th:block th:if="not ${oAuth2Login} or not ${saml2Login}">
<h4 th:text="#{account.changePassword}">Change Password?</h4> <h4 th:text="#{account.changePassword}">Change Password?</h4>
<form id="formsavechangepassword" class="bg-card mt-4 mb-4" th:action="@{'/api/v1/user/change-password'}" method="post"> <form id="formsavechangepassword" class="bg-card mt-4 mb-4" th:action="@{'/api/v1/user/change-password'}" method="post">
<div class="mb-3"> <div class="mb-3">

View File

@ -253,11 +253,11 @@
<label for="cacheInputs" th:text="#{settings.cacheInputs.name}"></label> <label for="cacheInputs" th:text="#{settings.cacheInputs.name}"></label>
</div> </div>
<a th:if="${@loginEnabled and @activSecurity}" th:href="@{'/account'}" class="btn btn-sm btn-outline-primary" <a th:if="${@loginEnabled and @activeSecurity}" th:href="@{'/account'}" class="btn btn-sm btn-outline-primary"
role="button" th:text="#{settings.accountSettings}" target="_blank">Account Settings</a> role="button" th:text="#{settings.accountSettings}" target="_blank">Account Settings</a>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<a th:if="${@loginEnabled and @activSecurity}" class="btn btn-danger" role="button" <a th:if="${@loginEnabled and @activeSecurity}" class="btn btn-danger" role="button"
th:text="#{settings.signOut}" th:href="@{'/logout'}">Sign Out</a> th:text="#{settings.signOut}" th:href="@{'/logout'}">Sign Out</a>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" th:text="#{close}"></button> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal" th:text="#{close}"></button>
</div> </div>

View File

@ -98,7 +98,7 @@
<img class="my-4" th:src="@{'/favicon.svg'}" alt="favicon" width="144" height="144"> <img class="my-4" th:src="@{'/favicon.svg'}" alt="favicon" width="144" height="144">
<h1 class="h1 mb-3 fw-normal" th:text="${@appName}">Stirling-PDF</h1> <h1 class="h1 mb-3 fw-normal" th:text="${@appName}">Stirling-PDF</h1>
<div th:if="${altLogin} and (${loginMethod} == 'all' or ${loginMethod} == 'oauth2')"> <div th:if="${altLogin} and (${loginMethod} == 'all' or ${loginMethod} == 'oauth2' or ${loginMethod} == 'saml2')">
<a href="#" class="w-100 btn btn-lg btn-primary" data-bs-toggle="modal" data-bs-target="#loginsModal" th:text="#{login.ssoSignIn}">Login Via SSO</a> <a href="#" class="w-100 btn btn-lg btn-primary" data-bs-toggle="modal" data-bs-target="#loginsModal" th:text="#{login.ssoSignIn}">Login Via SSO</a>
<br> <br>
<br> <br>

View File

@ -29,7 +29,6 @@ class ValidatorTest {
when(provider.getClientId()).thenReturn("clientId"); when(provider.getClientId()).thenReturn("clientId");
when(provider.getClientSecret()).thenReturn("clientSecret"); when(provider.getClientSecret()).thenReturn("clientSecret");
when(provider.getScopes()).thenReturn(List.of("read:user")); when(provider.getScopes()).thenReturn(List.of("read:user"));
when(provider.getUseAsUsername()).thenReturn(UsernameAttribute.EMAIL);
assertTrue(Validator.validateProvider(provider)); assertTrue(Validator.validateProvider(provider));
} }
@ -44,15 +43,11 @@ class ValidatorTest {
Provider generic = null; Provider generic = null;
var google = new GoogleProvider(null, "clientSecret", List.of("scope"), UsernameAttribute.EMAIL); var google = new GoogleProvider(null, "clientSecret", List.of("scope"), UsernameAttribute.EMAIL);
var github = new GitHubProvider("clientId", "", List.of("scope"), UsernameAttribute.LOGIN); var github = new GitHubProvider("clientId", "", List.of("scope"), UsernameAttribute.LOGIN);
var keycloak = new KeycloakProvider("issuer", "clientId", "clientSecret", List.of("scope"), UsernameAttribute.EMAIL);
keycloak.setUseAsUsername(null);
return Stream.of( return Stream.of(
Arguments.of(generic), Arguments.of(generic),
Arguments.of(google), Arguments.of(google),
Arguments.of(github), Arguments.of(github)
Arguments.of(keycloak)
); );
} }