SSO Refactoring (#2818)

# Description of Changes

* Refactoring of SSO code around OAuth & SAML 2
* Enabling auto-login with SAML 2 via the new `SSOAutoLogin` property
* Correcting typos & general cleanup

---

## Checklist

### General

- [x] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [x] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md)
(if applicable)
- [x] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md)
(if applicable)
- [x] I have performed a self-review of my own code
- [x] My changes generate no new warnings

### Documentation

- [x] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [x] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [x] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#6-testing)
for more details.
This commit is contained in:
Dario Ghunney Ware 2025-02-24 22:18:34 +00:00
parent 19c19be59e
commit 206db46464
85 changed files with 1462 additions and 1144 deletions

View File

@ -20,6 +20,7 @@ import stirling.software.SPDF.utils.GeneralUtils;
@Service
@Slf4j
public class KeygenLicenseVerifier {
// todo: place in config files?
private static final String ACCOUNT_ID = "e5430f69-e834-4ae4-befd-b602aae5f372";
private static final String BASE_URL = "https://api.keygen.sh/v1/accounts";
private static final ObjectMapper objectMapper = new ObjectMapper();
@ -68,7 +69,7 @@ public class KeygenLicenseVerifier {
return false;
} catch (Exception e) {
log.error("Error verifying license: " + e.getMessage());
log.error("Error verifying license: {}", e.getMessage());
return false;
}
}
@ -95,10 +96,9 @@ public class KeygenLicenseVerifier {
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
log.debug(" validateLicenseResponse body: " + response.body());
log.debug("ValidateLicenseResponse body: {}", response.body());
JsonNode jsonResponse = objectMapper.readTree(response.body());
if (response.statusCode() == 200) {
JsonNode metaNode = jsonResponse.path("meta");
boolean isValid = metaNode.path("valid").asBoolean();
@ -120,7 +120,7 @@ public class KeygenLicenseVerifier {
log.info(applicationProperties.toString());
} else {
log.error("Error validating license. Status code: " + response.statusCode());
log.error("Error validating license. Status code: {}", response.statusCode());
}
return jsonResponse;
}

View File

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

View File

@ -20,7 +20,7 @@ public class CleanUrlInterceptor implements HandlerInterceptor {
"endpoints",
"logout",
"error",
"erroroauth",
"errorOAuth",
"file",
"messageType",
"infoMessage");

View File

@ -3,7 +3,7 @@ package stirling.software.SPDF.config.interfaces;
import java.sql.SQLException;
import java.util.List;
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
import stirling.software.SPDF.model.exception.UnsupportedProviderException;
import stirling.software.SPDF.utils.FileInfo;
public interface DatabaseInterface {

View File

@ -69,7 +69,7 @@ public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationF
}
if (exception instanceof BadCredentialsException
|| exception instanceof UsernameNotFoundException) {
getRedirectStrategy().sendRedirect(request, response, "/login?error=badcredentials");
getRedirectStrategy().sendRedirect(request, response, "/login?error=badCredentials");
return;
}
if (exception instanceof InternalAuthenticationServiceException

View File

@ -14,8 +14,8 @@ import org.springframework.security.saml2.provider.service.authentication.Saml2A
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
import com.coveo.saml.SamlClient;
import com.coveo.saml.SamlException;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@ -28,62 +28,43 @@ import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrin
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
import stirling.software.SPDF.model.ApplicationProperties.Security.SAML2;
import stirling.software.SPDF.model.Provider;
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
import stirling.software.SPDF.model.provider.KeycloakProvider;
import stirling.software.SPDF.utils.UrlUtils;
@Slf4j
@AllArgsConstructor
public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
public static final String LOGOUT_PATH = "/login?logout=true";
private final ApplicationProperties applicationProperties;
@Override
public void onLogoutSuccess(
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
throws IOException {
if (!response.isCommitted()) {
// Handle user logout due to disabled account
if (request.getParameter("userIsDisabled") != null) {
response.sendRedirect(
request.getContextPath() + "/login?erroroauth=userIsDisabled");
return;
}
// Handle OAuth2 authentication error
if (request.getParameter("oauth2AuthenticationErrorWeb") != null) {
response.sendRedirect(
request.getContextPath() + "/login?erroroauth=userAlreadyExistsWeb");
return;
}
if (authentication != null) {
// Handle SAML2 logout redirection
if (authentication instanceof Saml2Authentication) {
// Handle SAML2 logout redirection
getRedirect_saml2(request, response, authentication);
return;
}
// Handle OAuth2 logout redirection
else if (authentication instanceof OAuth2AuthenticationToken) {
} else if (authentication instanceof OAuth2AuthenticationToken) {
// Handle OAuth2 logout redirection
getRedirect_oauth2(request, response, authentication);
return;
}
// Handle Username/Password logout
else if (authentication instanceof UsernamePasswordAuthenticationToken) {
getRedirectStrategy().sendRedirect(request, response, "/login?logout=true");
return;
}
// Handle unknown authentication types
else {
} else if (authentication instanceof UsernamePasswordAuthenticationToken) {
// Handle Username/Password logout
getRedirectStrategy().sendRedirect(request, response, LOGOUT_PATH);
} else {
// Handle unknown authentication types
log.error(
"authentication class unknown: "
+ authentication.getClass().getSimpleName());
getRedirectStrategy().sendRedirect(request, response, "/login?logout=true");
return;
"Authentication class unknown: {}",
authentication.getClass().getSimpleName());
getRedirectStrategy().sendRedirect(request, response, LOGOUT_PATH);
}
} else {
// Redirect to login page after logout
getRedirectStrategy().sendRedirect(request, response, "/login?logout=true");
return;
String path = checkForErrors(request);
getRedirectStrategy().sendRedirect(request, response, path);
}
}
}
@ -100,7 +81,7 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
CustomSaml2AuthenticatedPrincipal principal =
(CustomSaml2AuthenticatedPrincipal) samlAuthentication.getPrincipal();
String nameIdValue = principal.getName();
String nameIdValue = principal.name();
try {
// Read certificate from the resource
@ -111,27 +92,7 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
certificates.add(certificate);
// Construct URLs required for SAML configuration
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
SamlClient samlClient =
new SamlClient(
relyingPartyIdentifier,
assertionConsumerServiceUrl,
idpUrl,
idpIssuer,
certificates,
SamlClient.SamlIdpBinding.POST);
SamlClient samlClient = getSamlClient(registrationId, samlConf, certificates);
// Read private key for service provider
Resource privateKeyResource = samlConf.getPrivateKey();
@ -143,8 +104,12 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
// Redirect to identity provider for logout
samlClient.redirectToIdentityProvider(response, null, nameIdValue);
} catch (Exception e) {
log.error(nameIdValue, e);
getRedirectStrategy().sendRedirect(request, response, "/login?logout=true");
log.error(
"Error retrieving logout URL from Provider {} for user {}",
samlConf.getProvider(),
nameIdValue,
e);
getRedirectStrategy().sendRedirect(request, response, LOGOUT_PATH);
}
}
@ -152,87 +117,107 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
private void getRedirect_oauth2(
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException {
String param = "logout=true";
String registrationId = null;
String issuer = null;
String clientId = null;
String registrationId;
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2();
String path = checkForErrors(request);
if (authentication instanceof OAuth2AuthenticationToken) {
OAuth2AuthenticationToken oauthToken = (OAuth2AuthenticationToken) authentication;
if (authentication instanceof OAuth2AuthenticationToken oauthToken) {
registrationId = oauthToken.getAuthorizedClientRegistrationId();
try {
// Get OAuth2 provider details from configuration
Provider provider = oauth.getClient().get(registrationId);
issuer = provider.getIssuer();
clientId = provider.getClientId();
} catch (UnsupportedProviderException e) {
log.error(e.getMessage());
}
} else {
registrationId = oauth.getProvider() != null ? oauth.getProvider() : "";
issuer = oauth.getIssuer();
clientId = oauth.getClientId();
}
String errorMessage = "";
// Handle different error scenarios during logout
if (request.getParameter("oauth2AuthenticationErrorWeb") != null) {
param = "erroroauth=oauth2AuthenticationErrorWeb";
} else if ((errorMessage = request.getParameter("error")) != null) {
param = "error=" + sanitizeInput(errorMessage);
} else if ((errorMessage = request.getParameter("erroroauth")) != null) {
param = "erroroauth=" + sanitizeInput(errorMessage);
} else if (request.getParameter("oauth2AutoCreateDisabled") != null) {
param = "error=oauth2AutoCreateDisabled";
} else if (request.getParameter("oauth2_admin_blocked_user") != null) {
param = "erroroauth=oauth2_admin_blocked_user";
} else if (request.getParameter("userIsDisabled") != null) {
param = "erroroauth=userIsDisabled";
} else if (request.getParameter("badcredentials") != null) {
param = "error=badcredentials";
}
String redirect_url = UrlUtils.getOrigin(request) + "/login?" + param;
String redirectUrl = UrlUtils.getOrigin(request) + "/login?" + path;
// Redirect based on OAuth2 provider
switch (registrationId.toLowerCase()) {
case "keycloak":
// Add Keycloak specific logout URL if needed
case "keycloak" -> {
KeycloakProvider keycloak = oauth.getClient().getKeycloak();
String logoutUrl =
issuer
keycloak.getIssuer()
+ "/protocol/openid-connect/logout"
+ "?client_id="
+ clientId
+ keycloak.getClientId()
+ "&post_logout_redirect_uri="
+ response.encodeRedirectURL(redirect_url);
log.info("Redirecting to Keycloak logout URL: " + logoutUrl);
+ response.encodeRedirectURL(redirectUrl);
log.info("Redirecting to Keycloak logout URL: {}", logoutUrl);
response.sendRedirect(logoutUrl);
break;
case "github":
// Add GitHub specific logout URL if needed
String githubLogoutUrl = "https://github.com/logout";
log.info("Redirecting to GitHub logout URL: " + githubLogoutUrl);
response.sendRedirect(githubLogoutUrl);
break;
case "google":
// Add Google specific logout URL if needed
// String googleLogoutUrl =
// "https://accounts.google.com/Logout?continue=https://appengine.google.com/_ah/logout?continue="
// + response.encodeRedirectURL(redirect_url);
log.info("Google does not have a specific logout URL");
// log.info("Redirecting to Google logout URL: " + googleLogoutUrl);
// response.sendRedirect(googleLogoutUrl);
// break;
default:
String defaultRedirectUrl = request.getContextPath() + "/login?" + param;
log.info("Redirecting to default logout URL: " + defaultRedirectUrl);
response.sendRedirect(defaultRedirectUrl);
break;
}
case "github", "google" -> {
log.info(
"No redirect URL for {} available. Redirecting to default logout URL: {}",
registrationId,
redirectUrl);
response.sendRedirect(redirectUrl);
}
default -> {
log.info("Redirecting to default logout URL: {}", redirectUrl);
response.sendRedirect(redirectUrl);
}
}
}
// Sanitize input to avoid potential security vulnerabilities
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 idpSLOUrl = samlConf.getIdpSingleLogoutUrl();
String idpIssuer = samlConf.getIdpIssuer();
// Create SamlClient instance for SAML logout
return new SamlClient(
relyingPartyIdentifier,
assertionConsumerServiceUrl,
idpSLOUrl,
idpIssuer,
certificates,
SamlClient.SamlIdpBinding.POST);
}
/**
* Handles different error scenarios during logout. Will return a <code>String</code> containing
* the error request parameter.
*
* @param request the user's <code>HttpServletRequest</code> request.
* @return a <code>String</code> containing the error request parameter.
*/
private String checkForErrors(HttpServletRequest request) {
String errorMessage;
String path = "logout=true";
if (request.getParameter("oAuth2AuthenticationErrorWeb") != null) {
path = "errorOAuth=userAlreadyExistsWeb";
} else if ((errorMessage = request.getParameter("errorOAuth")) != null) {
path = "errorOAuth=" + sanitizeInput(errorMessage);
} else if (request.getParameter("oAuth2AutoCreateDisabled") != null) {
path = "errorOAuth=oAuth2AutoCreateDisabled";
} else if (request.getParameter("oAuth2AdminBlockedUser") != null) {
path = "errorOAuth=oAuth2AdminBlockedUser";
} else if (request.getParameter("userIsDisabled") != null) {
path = "errorOAuth=userIsDisabled";
} else if ((errorMessage = request.getParameter("error")) != null) {
path = "errorOAuth=" + sanitizeInput(errorMessage);
} else if (request.getParameter("badCredentials") != null) {
path = "errorOAuth=badCredentials";
}
return path;
}
/**
* Sanitize input to avoid potential security vulnerabilities. Will return a sanitised <code>
* String</code>.
*
* @return a sanitised <code>String</code>
*/
private String sanitizeInput(String input) {
return input.replaceAll("[^a-zA-Z0-9 ]", "");
}

View File

@ -12,7 +12,7 @@ import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.interfaces.DatabaseInterface;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.Role;
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
import stirling.software.SPDF.model.exception.UnsupportedProviderException;
@Slf4j
@Component

View File

@ -1,6 +1,6 @@
package stirling.software.SPDF.config.security;
import java.util.*;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
@ -51,11 +51,7 @@ public class SecurityConfiguration {
private final CustomUserDetailsService userDetailsService;
private final UserService userService;
@Qualifier("loginEnabled")
private final boolean loginEnabledValue;
@Qualifier("runningEE")
private final boolean runningEE;
private final ApplicationProperties applicationProperties;
@ -109,6 +105,7 @@ public class SecurityConfiguration {
if (applicationProperties.getSecurity().getCsrfDisabled() || !loginEnabledValue) {
http.csrf(csrf -> csrf.disable());
}
if (loginEnabledValue) {
http.addFilterBefore(
userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
@ -164,8 +161,7 @@ public class SecurityConfiguration {
.logoutSuccessHandler(
new CustomLogoutSuccessHandler(applicationProperties))
.clearAuthentication(true)
.invalidateHttpSession( // Invalidate session
true)
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID", "remember-me"));
http.rememberMe(
rememberMeConfigurer -> // Use the configurator directly
@ -227,14 +223,14 @@ public class SecurityConfiguration {
.permitAll());
}
// Handle OAUTH2 Logins
if (applicationProperties.getSecurity().isOauth2Activ()) {
if (applicationProperties.getSecurity().isOauth2Active()) {
http.oauth2Login(
oauth2 ->
oauth2.loginPage("/oauth2")
.
/*
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.
*/
successHandler(
@ -258,8 +254,7 @@ public class SecurityConfiguration {
.permitAll());
}
// Handle SAML
if (applicationProperties.getSecurity().isSaml2Activ()) {
// && runningEE
if (applicationProperties.getSecurity().isSaml2Active() && runningEE) {
// Configure the authentication provider
OpenSaml4AuthenticationProvider authenticationProvider =
new OpenSaml4AuthenticationProvider();
@ -284,12 +279,13 @@ public class SecurityConfiguration {
.authenticationRequestResolver(
saml2AuthenticationRequestResolver);
} catch (Exception e) {
log.error("Error configuring SAML2 login", e);
log.error("Error configuring SAML 2 login", e);
throw new RuntimeException(e);
}
});
}
} else {
log.info("SAML 2 login is not enabled. Using default.");
http.authorizeHttpRequests(authz -> authz.anyRequest().permitAll());
}
return http.build();
@ -315,7 +311,7 @@ public class SecurityConfiguration {
}
@Bean
public boolean activSecurity() {
public boolean activeSecurity() {
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
// provider for API keys.
Optional<User> user = userService.getUserByApiKey(apiKey);
if (!user.isPresent()) {
if (user.isEmpty()) {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().write("Invalid API Key.");
return;
@ -150,7 +150,7 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
OAUTH2 oAuth = securityProp.getOauth2();
blockRegistration = oAuth != null && oAuth.getBlockRegistration();
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
username = ((CustomSaml2AuthenticatedPrincipal) principal).getName();
username = ((CustomSaml2AuthenticatedPrincipal) principal).name();
loginMethod = LoginMethod.SAML2USER;
SAML2 saml2 = securityProp.getSaml2();
blockRegistration = saml2 != null && saml2.getBlockRegistration();
@ -177,7 +177,7 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
if (blockRegistration && !isUserExists) {
log.warn("Blocked registration for OAuth2/SAML user: {}", username);
response.sendRedirect(
request.getContextPath() + "/logout?oauth2_admin_blocked_user=true");
request.getContextPath() + "/logout?oAuth2AdminBlockedUser=true");
return;
}
@ -193,7 +193,7 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
// Redirect to logout if credentials are invalid
if (!isUserExists && notSsoLogin) {
response.sendRedirect(request.getContextPath() + "/logout?badcredentials=true");
response.sendRedirect(request.getContextPath() + "/logout?badCredentials=true");
return;
}
if (isUserDisabled) {

View File

@ -27,7 +27,7 @@ import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrin
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface;
import stirling.software.SPDF.model.*;
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
import stirling.software.SPDF.model.exception.UnsupportedProviderException;
import stirling.software.SPDF.repository.AuthorityRepository;
import stirling.software.SPDF.repository.UserRepository;
@ -78,20 +78,18 @@ public class UserService implements UserServiceInterface {
}
// Handle OAUTH2 login and user auto creation.
public boolean processSSOPostLogin(String username, boolean autoCreateUser)
public void processSSOPostLogin(String username, boolean autoCreateUser)
throws IllegalArgumentException, SQLException, UnsupportedProviderException {
if (!isUsernameValid(username)) {
return false;
return;
}
Optional<User> existingUser = findByUsernameIgnoreCase(username);
if (existingUser.isPresent()) {
return true;
return;
}
if (autoCreateUser) {
saveUser(username, AuthenticationType.SSO);
return true;
}
return false;
}
public Authentication getAuthentication(String apiKey) {
@ -373,19 +371,16 @@ public class UserService implements UserServiceInterface {
public void invalidateUserSessions(String username) {
String usernameP = "";
for (Object principal : sessionRegistry.getAllPrincipals()) {
for (SessionInformation sessionsInformation :
sessionRegistry.getAllSessions(principal, false)) {
if (principal instanceof UserDetails) {
UserDetails userDetails = (UserDetails) principal;
if (principal instanceof UserDetails userDetails) {
usernameP = userDetails.getUsername();
} else if (principal instanceof OAuth2User) {
OAuth2User oAuth2User = (OAuth2User) principal;
} else if (principal instanceof OAuth2User oAuth2User) {
usernameP = oAuth2User.getName();
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
CustomSaml2AuthenticatedPrincipal saml2User =
(CustomSaml2AuthenticatedPrincipal) principal;
usernameP = saml2User.getName();
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal saml2User) {
usernameP = saml2User.name();
} else if (principal instanceof String) {
usernameP = (String) principal;
}
@ -398,6 +393,7 @@ public class UserService implements UserServiceInterface {
public String getCurrentUsername() {
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
return ((UserDetails) principal).getUsername();
} else if (principal instanceof OAuth2User) {
@ -405,42 +401,48 @@ public class UserService implements UserServiceInterface {
.getAttribute(
applicationProperties.getSecurity().getOauth2().getUseAsUsername());
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
return ((CustomSaml2AuthenticatedPrincipal) principal).getName();
} else if (principal instanceof String) {
return (String) principal;
return ((CustomSaml2AuthenticatedPrincipal) principal).name();
} else {
return principal.toString();
}
}
@Transactional
public void syncCustomApiUser(String customApiKey)
throws SQLException, UnsupportedProviderException {
if (customApiKey == null || customApiKey.trim().length() == 0) {
public void syncCustomApiUser(String customApiKey) {
if (customApiKey == null || customApiKey.trim().isBlank()) {
return;
}
String username = "CUSTOM_API_USER";
Optional<User> existingUser = findByUsernameIgnoreCase(username);
if (!existingUser.isPresent()) {
// Create new user with API role
User user = new User();
user.setUsername(username);
user.setPassword(UUID.randomUUID().toString());
user.setEnabled(true);
user.setFirstLogin(false);
user.setAuthenticationType(AuthenticationType.WEB);
user.setApiKey(customApiKey);
user.addAuthority(new Authority(Role.INTERNAL_API_USER.getRoleId(), user));
userRepository.save(user);
existingUser.ifPresentOrElse(
user -> {
// Update API key if it has changed
User updatedUser = existingUser.get();
if (!customApiKey.equals(updatedUser.getApiKey())) {
updatedUser.setApiKey(customApiKey);
userRepository.save(updatedUser);
}
},
() -> {
// Create new user with API role
User user = new User();
user.setUsername(username);
user.setPassword(UUID.randomUUID().toString());
user.setEnabled(true);
user.setFirstLogin(false);
user.setAuthenticationType(AuthenticationType.WEB);
user.setApiKey(customApiKey);
user.addAuthority(new Authority(Role.INTERNAL_API_USER.getRoleId(), user));
userRepository.save(user);
});
try {
databaseService.exportDatabase();
} else {
// Update API key if it has changed
User user = existingUser.get();
if (!customApiKey.equals(user.getApiKey())) {
user.setApiKey(customApiKey);
userRepository.save(user);
databaseService.exportDatabase();
}
} catch (SQLException | UnsupportedProviderException e) {
log.error("Error exporting database after synchronising custom API user", e);
}
}

View File

@ -14,7 +14,7 @@ import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.InstallationPathConfig;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
import stirling.software.SPDF.model.exception.UnsupportedProviderException;
@Slf4j
@Getter

View File

@ -8,7 +8,7 @@ import org.springframework.stereotype.Component;
import stirling.software.SPDF.config.interfaces.DatabaseInterface;
import stirling.software.SPDF.controller.api.H2SQLCondition;
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
import stirling.software.SPDF.model.exception.UnsupportedProviderException;
@Component
@Conditional(H2SQLCondition.class)

View File

@ -29,7 +29,7 @@ public class CustomOAuth2AuthenticationFailureHandler
if (exception instanceof BadCredentialsException) {
log.error("BadCredentialsException", exception);
getRedirectStrategy().sendRedirect(request, response, "/login?error=badcredentials");
getRedirectStrategy().sendRedirect(request, response, "/login?error=badCredentials");
return;
}
if (exception instanceof DisabledException) {
@ -50,10 +50,12 @@ public class CustomOAuth2AuthenticationFailureHandler
if (error.getErrorCode().equals("Password must not be null")) {
errorCode = "userAlreadyExistsWeb";
}
log.error("OAuth2 Authentication error: " + errorCode);
log.error("OAuth2AuthenticationException", exception);
getRedirectStrategy().sendRedirect(request, response, "/login?erroroauth=" + errorCode);
return;
log.error(
"OAuth2 Authentication error: {}",
errorCode != null ? errorCode : exception.getMessage(),
exception);
getRedirectStrategy().sendRedirect(request, response, "/login?errorOAuth=" + errorCode);
}
log.error("Unhandled authentication exception", exception);
super.onAuthenticationFailure(request, response, exception);

View File

@ -20,19 +20,18 @@ import stirling.software.SPDF.config.security.UserService;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
import stirling.software.SPDF.model.AuthenticationType;
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
import stirling.software.SPDF.model.exception.UnsupportedProviderException;
import stirling.software.SPDF.utils.RequestUriUtils;
public class CustomOAuth2AuthenticationSuccessHandler
extends SavedRequestAwareAuthenticationSuccessHandler {
private LoginAttemptService loginAttemptService;
private ApplicationProperties applicationProperties;
private UserService userService;
private final LoginAttemptService loginAttemptService;
private final ApplicationProperties applicationProperties;
private final UserService userService;
public CustomOAuth2AuthenticationSuccessHandler(
final LoginAttemptService loginAttemptService,
LoginAttemptService loginAttemptService,
ApplicationProperties applicationProperties,
UserService userService) {
this.applicationProperties = applicationProperties;
@ -48,11 +47,9 @@ public class CustomOAuth2AuthenticationSuccessHandler
Object principal = authentication.getPrincipal();
String username = "";
if (principal instanceof OAuth2User) {
OAuth2User oauthUser = (OAuth2User) principal;
if (principal instanceof OAuth2User oauthUser) {
username = oauthUser.getName();
} else if (principal instanceof UserDetails) {
UserDetails oauthUser = (UserDetails) principal;
} else if (principal instanceof UserDetails oauthUser) {
username = oauthUser.getUsername();
}
@ -78,6 +75,7 @@ public class CustomOAuth2AuthenticationSuccessHandler
throw new LockedException(
"Your account has been locked due to too many failed login attempts.");
}
if (userService.isUserDisabled(username)) {
getRedirectStrategy()
.sendRedirect(request, response, "/logout?userIsDisabled=true");
@ -87,13 +85,14 @@ public class CustomOAuth2AuthenticationSuccessHandler
&& userService.hasPassword(username)
&& !userService.isAuthenticationTypeByUsername(username, AuthenticationType.SSO)
&& oAuth.getAutoCreateUser()) {
response.sendRedirect(contextPath + "/logout?oauth2AuthenticationErrorWeb=true");
response.sendRedirect(contextPath + "/logout?oAuth2AuthenticationErrorWeb=true");
return;
}
try {
if (oAuth.getBlockRegistration()
&& !userService.usernameExistsIgnoreCase(username)) {
response.sendRedirect(contextPath + "/logout?oauth2_admin_blocked_user=true");
response.sendRedirect(contextPath + "/logout?oAuth2AdminBlockedUser=true");
return;
}
if (principal instanceof OAuth2User) {

View File

@ -17,19 +17,19 @@ import stirling.software.SPDF.config.security.LoginAttemptService;
import stirling.software.SPDF.config.security.UserService;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2.Client;
import stirling.software.SPDF.model.User;
import stirling.software.SPDF.model.UsernameAttribute;
@Slf4j
public class CustomOAuth2UserService implements OAuth2UserService<OidcUserRequest, OidcUser> {
private final OidcUserService delegate = new OidcUserService();
private UserService userService;
private final UserService userService;
private LoginAttemptService loginAttemptService;
private final LoginAttemptService loginAttemptService;
private ApplicationProperties applicationProperties;
private final ApplicationProperties applicationProperties;
public CustomOAuth2UserService(
ApplicationProperties applicationProperties,
@ -42,34 +42,26 @@ public class CustomOAuth2UserService implements OAuth2UserService<OidcUserReques
@Override
public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
OAUTH2 oauth2 = applicationProperties.getSecurity().getOauth2();
String usernameAttribute = oauth2.getUseAsUsername();
if (usernameAttribute == null || usernameAttribute.trim().isEmpty()) {
Client client = oauth2.getClient();
if (client != null && client.getKeycloak() != null) {
usernameAttribute = client.getKeycloak().getUseAsUsername();
} else {
usernameAttribute = "email";
}
}
try {
OidcUser user = delegate.loadUser(userRequest);
String username = user.getUserInfo().getClaimAsString(usernameAttribute);
OAUTH2 oauth2 = applicationProperties.getSecurity().getOauth2();
UsernameAttribute usernameAttribute =
UsernameAttribute.valueOf(oauth2.getUseAsUsername().toUpperCase());
String usernameAttributeKey = usernameAttribute.getName();
// Check if the username claim is null or empty
if (username == null || username.trim().isEmpty()) {
throw new IllegalArgumentException(
"Claim '" + usernameAttribute + "' cannot be null or empty");
}
// todo: save user by OIDC ID instead of username
Optional<User> internalUser =
userService.findByUsernameIgnoreCase(user.getAttribute(usernameAttributeKey));
Optional<User> duser = userService.findByUsernameIgnoreCase(username);
if (duser.isPresent()) {
if (loginAttemptService.isBlocked(username)) {
if (internalUser.isPresent()) {
String internalUsername = internalUser.get().getUsername();
if (loginAttemptService.isBlocked(internalUsername)) {
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");
}
}
@ -79,7 +71,7 @@ public class CustomOAuth2UserService implements OAuth2UserService<OidcUserReques
user.getAuthorities(),
userRequest.getIdToken(),
user.getUserInfo(),
usernameAttribute);
usernameAttributeKey);
} catch (IllegalArgumentException e) {
log.error("Error loading OIDC user: {}", e.getMessage());
throw new OAuth2AuthenticationException(new OAuth2Error(e.getMessage()), e);

View File

@ -1,5 +1,8 @@
package stirling.software.SPDF.config.security.oauth2;
import static org.springframework.security.oauth2.core.AuthorizationGrantType.AUTHORIZATION_CODE;
import static stirling.software.SPDF.utils.validation.Validator.*;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
@ -26,18 +29,20 @@ import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2.Client;
import stirling.software.SPDF.model.User;
import stirling.software.SPDF.model.provider.GithubProvider;
import stirling.software.SPDF.model.UsernameAttribute;
import stirling.software.SPDF.model.exception.NoProviderFoundException;
import stirling.software.SPDF.model.provider.GitHubProvider;
import stirling.software.SPDF.model.provider.GoogleProvider;
import stirling.software.SPDF.model.provider.KeycloakProvider;
import stirling.software.SPDF.model.provider.Provider;
@Configuration
@Slf4j
@ConditionalOnProperty(
value = "security.oauth2.enabled",
havingValue = "true",
matchIfMissing = false)
@Configuration
@ConditionalOnProperty(value = "security.oauth2.enabled", havingValue = "true")
public class OAuth2Configuration {
public static final String REDIRECT_URI_PATH = "{baseUrl}/login/oauth2/code/";
private final ApplicationProperties applicationProperties;
@Lazy private final UserService userService;
@ -48,139 +53,175 @@ public class OAuth2Configuration {
}
@Bean
@ConditionalOnProperty(
value = "security.oauth2.enabled",
havingValue = "true",
matchIfMissing = false)
public ClientRegistrationRepository clientRegistrationRepository() {
@ConditionalOnProperty(value = "security.oauth2.enabled", havingValue = "true")
public ClientRegistrationRepository clientRegistrationRepository()
throws NoProviderFoundException {
List<ClientRegistration> registrations = new ArrayList<>();
githubClientRegistration().ifPresent(registrations::add);
oidcClientRegistration().ifPresent(registrations::add);
googleClientRegistration().ifPresent(registrations::add);
keycloakClientRegistration().ifPresent(registrations::add);
if (registrations.isEmpty()) {
log.error("At least one OAuth2 provider must be configured");
System.exit(1);
log.error("No OAuth2 provider registered");
throw new NoProviderFoundException("At least one OAuth2 provider must be configured.");
}
return new InMemoryClientRegistrationRepository(registrations);
}
private Optional<ClientRegistration> googleClientRegistration() {
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2();
if (oauth == null || !oauth.getEnabled()) {
return Optional.empty();
}
Client client = oauth.getClient();
if (client == null) {
return Optional.empty();
}
GoogleProvider google = client.getGoogle();
return google != null && google.isSettingsValid()
? Optional.of(
ClientRegistration.withRegistrationId(google.getName())
.clientId(google.getClientId())
.clientSecret(google.getClientSecret())
.scope(google.getScopes())
.authorizationUri(google.getAuthorizationuri())
.tokenUri(google.getTokenuri())
.userInfoUri(google.getUserinfouri())
.userNameAttributeName(google.getUseAsUsername())
.clientName(google.getClientName())
.redirectUri("{baseUrl}/login/oauth2/code/" + google.getName())
.authorizationGrantType(
org.springframework.security.oauth2.core
.AuthorizationGrantType.AUTHORIZATION_CODE)
.build())
: Optional.empty();
}
private Optional<ClientRegistration> keycloakClientRegistration() {
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2();
if (oauth == null || !oauth.getEnabled()) {
OAUTH2 oauth2 = applicationProperties.getSecurity().getOauth2();
if (isOAuth2Enabled(oauth2) || isClientInitialised(oauth2)) {
return Optional.empty();
}
Client client = oauth.getClient();
if (client == null) {
return Optional.empty();
}
KeycloakProvider keycloak = client.getKeycloak();
return keycloak != null && keycloak.isSettingsValid()
Client client = oauth2.getClient();
KeycloakProvider keycloakClient = client.getKeycloak();
Provider keycloak =
new KeycloakProvider(
keycloakClient.getIssuer(),
keycloakClient.getClientId(),
keycloakClient.getClientSecret(),
keycloakClient.getScopes(),
keycloakClient.getUseAsUsername());
return validateProvider(keycloak)
? Optional.of(
ClientRegistrations.fromIssuerLocation(keycloak.getIssuer())
.registrationId(keycloak.getName())
.clientId(keycloak.getClientId())
.clientSecret(keycloak.getClientSecret())
.scope(keycloak.getScopes())
.userNameAttributeName(keycloak.getUseAsUsername())
.userNameAttributeName(keycloak.getUseAsUsername().getName())
.clientName(keycloak.getClientName())
.build())
: Optional.empty();
}
private Optional<ClientRegistration> googleClientRegistration() {
OAUTH2 oAuth2 = applicationProperties.getSecurity().getOauth2();
if (isOAuth2Enabled(oAuth2) || isClientInitialised(oAuth2)) {
return Optional.empty();
}
Client client = oAuth2.getClient();
GoogleProvider googleClient = client.getGoogle();
Provider google =
new GoogleProvider(
googleClient.getClientId(),
googleClient.getClientSecret(),
googleClient.getScopes(),
googleClient.getUseAsUsername());
return validateProvider(google)
? Optional.of(
ClientRegistration.withRegistrationId(google.getName())
.clientId(google.getClientId())
.clientSecret(google.getClientSecret())
.scope(google.getScopes())
.authorizationUri(google.getAuthorizationUri())
.tokenUri(google.getTokenUri())
.userInfoUri(google.getUserInfoUri())
.userNameAttributeName(google.getUseAsUsername().getName())
.clientName(google.getClientName())
.redirectUri(REDIRECT_URI_PATH + google.getName())
.authorizationGrantType(AUTHORIZATION_CODE)
.build())
: Optional.empty();
}
private Optional<ClientRegistration> githubClientRegistration() {
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2();
if (oauth == null || !oauth.getEnabled()) {
OAUTH2 oAuth2 = applicationProperties.getSecurity().getOauth2();
if (isOAuth2Enabled(oAuth2)) {
return Optional.empty();
}
Client client = oauth.getClient();
if (client == null) {
return Optional.empty();
}
GithubProvider github = client.getGithub();
return github != null && github.isSettingsValid()
Client client = oAuth2.getClient();
GitHubProvider githubClient = client.getGithub();
Provider github =
new GitHubProvider(
githubClient.getClientId(),
githubClient.getClientSecret(),
githubClient.getScopes(),
githubClient.getUseAsUsername());
return validateProvider(github)
? Optional.of(
ClientRegistration.withRegistrationId(github.getName())
.clientId(github.getClientId())
.clientSecret(github.getClientSecret())
.scope(github.getScopes())
.authorizationUri(github.getAuthorizationuri())
.tokenUri(github.getTokenuri())
.userInfoUri(github.getUserinfouri())
.userNameAttributeName(github.getUseAsUsername())
.authorizationUri(github.getAuthorizationUri())
.tokenUri(github.getTokenUri())
.userInfoUri(github.getUserInfoUri())
.userNameAttributeName(github.getUseAsUsername().getName())
.clientName(github.getClientName())
.redirectUri("{baseUrl}/login/oauth2/code/" + github.getName())
.authorizationGrantType(
org.springframework.security.oauth2.core
.AuthorizationGrantType.AUTHORIZATION_CODE)
.redirectUri(REDIRECT_URI_PATH + github.getName())
.authorizationGrantType(AUTHORIZATION_CODE)
.build())
: Optional.empty();
}
private Optional<ClientRegistration> oidcClientRegistration() {
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2();
if (oauth == null
|| oauth.getIssuer() == null
|| oauth.getIssuer().isEmpty()
|| oauth.getClientId() == null
|| oauth.getClientId().isEmpty()
|| oauth.getClientSecret() == null
|| oauth.getClientSecret().isEmpty()
|| oauth.getScopes() == null
|| oauth.getScopes().isEmpty()
|| oauth.getUseAsUsername() == null
|| oauth.getUseAsUsername().isEmpty()) {
if (isOAuth2Enabled(oauth) || isClientInitialised(oauth)) {
return Optional.empty();
}
return Optional.of(
ClientRegistrations.fromIssuerLocation(oauth.getIssuer())
.registrationId("oidc")
.clientId(oauth.getClientId())
.clientSecret(oauth.getClientSecret())
.scope(oauth.getScopes())
.userNameAttributeName(oauth.getUseAsUsername())
.clientName("OIDC")
.build());
String name = oauth.getProvider();
String firstChar = String.valueOf(name.charAt(0));
String clientName = name.replaceFirst(firstChar, firstChar.toUpperCase());
Provider oidcProvider =
new Provider(
oauth.getIssuer(),
name,
clientName,
oauth.getClientId(),
oauth.getClientSecret(),
oauth.getScopes(),
UsernameAttribute.valueOf(oauth.getUseAsUsername().toUpperCase()),
oauth.getLogoutUrl(),
null,
null,
null);
return !isStringEmpty(oidcProvider.getIssuer()) || validateProvider(oidcProvider)
? Optional.of(
ClientRegistrations.fromIssuerLocation(oauth.getIssuer())
.registrationId(name)
.clientId(oidcProvider.getClientId())
.clientSecret(oidcProvider.getClientSecret())
.scope(oidcProvider.getScopes())
.userNameAttributeName(oidcProvider.getUseAsUsername().getName())
.clientName(clientName)
.redirectUri(REDIRECT_URI_PATH + name)
.authorizationGrantType(AUTHORIZATION_CODE)
.build())
: Optional.empty();
}
private boolean isOAuth2Enabled(OAUTH2 oAuth2) {
return oAuth2 == null || !oAuth2.getEnabled();
}
private boolean isClientInitialised(OAUTH2 oauth2) {
Client client = oauth2.getClient();
return client == null;
}
/*
This following function is to grant Authorities to the OAUTH2 user from the values stored in the database.
This is required for the internal; 'hasRole()' function to give out the correct role.
*/
@Bean
@ConditionalOnProperty(
value = "security.oauth2.enabled",
havingValue = "true",
matchIfMissing = false)
@ConditionalOnProperty(value = "security.oauth2.enabled", havingValue = "true")
GrantedAuthoritiesMapper userAuthoritiesMapper() {
return (authorities) -> {
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
@ -200,11 +241,9 @@ public class OAuth2Configuration {
(String) oauth2Auth.getAttributes().get(useAsUsername));
if (userOpt.isPresent()) {
User user = userOpt.get();
if (user != null) {
mappedAuthorities.add(
new SimpleGrantedAuthority(
userService.findRole(user).getAuthority()));
}
mappedAuthorities.add(
new SimpleGrantedAuthority(
userService.findRole(user).getAuthority()));
}
}
});

View File

@ -13,8 +13,10 @@ import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.bouncycastle.util.io.pem.PemObject;
import org.bouncycastle.util.io.pem.PemReader;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.core.io.Resource;
@ConditionalOnProperty(name = "security.saml2.enabled", havingValue = "true")
public class CertificateUtils {
public static X509Certificate readCertificate(Resource certificateResource) throws Exception {

View File

@ -4,27 +4,13 @@ import java.io.Serializable;
import java.util.List;
import java.util.Map;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal;
public class CustomSaml2AuthenticatedPrincipal
@ConditionalOnProperty(name = "security.saml2.enabled", havingValue = "true")
public record CustomSaml2AuthenticatedPrincipal(String name, Map<String, List<Object>> attributes, String nameId, List<String> sessionIndexes)
implements Saml2AuthenticatedPrincipal, Serializable {
private final String name;
private final Map<String, List<Object>> attributes;
private final String nameId;
private final List<String> sessionIndexes;
public CustomSaml2AuthenticatedPrincipal(
String name,
Map<String, List<Object>> attributes,
String nameId,
List<String> sessionIndexes) {
this.name = name;
this.attributes = attributes;
this.nameId = nameId;
this.sessionIndexes = sessionIndexes;
}
@Override
public String getName() {
return this.name;
@ -35,11 +21,4 @@ public class CustomSaml2AuthenticatedPrincipal
return this.attributes;
}
public String getNameId() {
return this.nameId;
}
public List<String> getSessionIndexes() {
return this.sessionIndexes;
}
}

View File

@ -2,19 +2,20 @@ package stirling.software.SPDF.config.security.saml2;
import java.io.IOException;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.security.authentication.ProviderNotFoundException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.saml2.core.Saml2Error;
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@ConditionalOnProperty(name = "security.saml2.enabled", havingValue = "true")
public class CustomSaml2AuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Override
@ -22,18 +23,19 @@ public class CustomSaml2AuthenticationFailureHandler extends SimpleUrlAuthentica
HttpServletRequest request,
HttpServletResponse response,
AuthenticationException exception)
throws IOException, ServletException {
throws IOException {
log.error("Authentication error", exception);
if (exception instanceof Saml2AuthenticationException) {
Saml2Error error = ((Saml2AuthenticationException) exception).getSaml2Error();
getRedirectStrategy()
.sendRedirect(request, response, "/login?erroroauth=" + error.getErrorCode());
.sendRedirect(request, response, "/login?errorOAuth=" + error.getErrorCode());
} else if (exception instanceof ProviderNotFoundException) {
getRedirectStrategy()
.sendRedirect(
request,
response,
"/login?erroroauth=not_authentication_provider_found");
"/login?errorOAuth=not_authentication_provider_found");
}
log.error("AuthenticationException: " + exception);
}
}

View File

@ -21,7 +21,7 @@ import stirling.software.SPDF.config.security.UserService;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.ApplicationProperties.Security.SAML2;
import stirling.software.SPDF.model.AuthenticationType;
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
import stirling.software.SPDF.model.exception.UnsupportedProviderException;
import stirling.software.SPDF.utils.RequestUriUtils;
@AllArgsConstructor
@ -42,7 +42,7 @@ public class CustomSaml2AuthenticationSuccessHandler
log.debug("Starting SAML2 authentication success handling");
if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
String username = ((CustomSaml2AuthenticatedPrincipal) principal).getName();
String username = ((CustomSaml2AuthenticatedPrincipal) principal).name();
log.debug("Authenticated principal found for user: {}", username);
HttpSession session = request.getSession(false);
@ -97,7 +97,7 @@ public class CustomSaml2AuthenticationSuccessHandler
"User {} exists with password but is not SSO user, redirecting to logout",
username);
response.sendRedirect(
contextPath + "/logout?oauth2AuthenticationErrorWeb=true");
contextPath + "/logout?oAuth2AuthenticationErrorWeb=true");
return;
}
@ -105,20 +105,18 @@ public class CustomSaml2AuthenticationSuccessHandler
if (saml2.getBlockRegistration() && !userExists) {
log.debug("Registration blocked for new user: {}", username);
response.sendRedirect(
contextPath + "/login?erroroauth=oauth2_admin_blocked_user");
contextPath + "/login?errorOAuth=oAuth2AdminBlockedUser");
return;
}
log.debug("Processing SSO post-login for user: {}", username);
userService.processSSOPostLogin(username, saml2.getAutoCreateUser());
log.debug("Successfully processed authentication for user: {}", username);
response.sendRedirect(contextPath + "/");
return;
} catch (IllegalArgumentException | SQLException | UnsupportedProviderException e) {
log.debug(
"Invalid username detected for user: {}, redirecting to logout",
username);
response.sendRedirect(contextPath + "/logout?invalidUsername=true");
return;
}
}
} else {

View File

@ -7,6 +7,7 @@ import org.opensaml.saml.saml2.core.Assertion;
import org.opensaml.saml.saml2.core.Attribute;
import org.opensaml.saml.saml2.core.AttributeStatement;
import org.opensaml.saml.saml2.core.AuthnStatement;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider.ResponseToken;
@ -18,10 +19,11 @@ import stirling.software.SPDF.config.security.UserService;
import stirling.software.SPDF.model.User;
@Slf4j
@ConditionalOnProperty(name = "security.saml2.enabled", havingValue = "true")
public class CustomSaml2ResponseAuthenticationConverter
implements Converter<ResponseToken, Saml2Authentication> {
private UserService userService;
private final UserService userService;
public CustomSaml2ResponseAuthenticationConverter(UserService userService) {
this.userService = userService;
@ -61,10 +63,10 @@ public class CustomSaml2ResponseAuthenticationConverter
Map<String, List<Object>> attributes = extractAttributes(assertion);
// 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
String userIdentifier = null;
String userIdentifier;
if (hasAttribute(attributes, "username")) {
userIdentifier = getFirstAttributeValue(attributes, "username");
} else if (hasAttribute(attributes, "emailaddress")) {
@ -84,10 +86,8 @@ public class CustomSaml2ResponseAuthenticationConverter
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority("ROLE_USER");
if (userOpt.isPresent()) {
User user = userOpt.get();
if (user != null) {
simpleGrantedAuthority =
new SimpleGrantedAuthority(userService.findRole(user).getAuthority());
}
simpleGrantedAuthority =
new SimpleGrantedAuthority(userService.findRole(user).getAuthority());
}
List<String> sessionIndexes = new ArrayList<>();
@ -102,7 +102,7 @@ public class CustomSaml2ResponseAuthenticationConverter
return new Saml2Authentication(
principal,
responseToken.getToken().getSaml2Response(),
Collections.singletonList(simpleGrantedAuthority));
List.of(simpleGrantedAuthority));
}
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.security.saml2.core.Saml2X509Credential;
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.RelyingPartyRegistration;
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.web.HttpSessionSaml2AuthenticationRequestRepository;
import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver;
import jakarta.servlet.http.HttpServletRequest;
@ -26,27 +28,20 @@ import stirling.software.SPDF.model.ApplicationProperties.Security.SAML2;
@Configuration
@Slf4j
@ConditionalOnProperty(
value = "security.saml2.enabled",
havingValue = "true",
matchIfMissing = false)
@ConditionalOnProperty(value = "security.saml2.enabled", havingValue = "true")
public class SAML2Configuration {
private final ApplicationProperties applicationProperties;
public SAML2Configuration(ApplicationProperties applicationProperties) {
this.applicationProperties = applicationProperties;
}
@Bean
@ConditionalOnProperty(
name = "security.saml2.enabled",
havingValue = "true",
matchIfMissing = false)
@ConditionalOnProperty(name = "security.saml2.enabled", havingValue = "true")
public RelyingPartyRegistrationRepository relyingPartyRegistrations() throws Exception {
SAML2 samlConf = applicationProperties.getSecurity().getSaml2();
X509Certificate idpCert = CertificateUtils.readCertificate(samlConf.getidpCert());
X509Certificate idpCert = CertificateUtils.readCertificate(samlConf.getIdpCert());
Saml2X509Credential verificationCredential = Saml2X509Credential.verification(idpCert);
Resource privateKeyResource = samlConf.getPrivateKey();
Resource certificateResource = samlConf.getSpCert();
@ -58,81 +53,124 @@ public class SAML2Configuration {
RelyingPartyRegistration rp =
RelyingPartyRegistration.withRegistrationId(samlConf.getRegistrationId())
.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(
metadata ->
metadata.entityId(samlConf.getIdpIssuer())
.singleSignOnServiceLocation(
samlConf.getIdpSingleLoginUrl())
.verificationX509Credentials(
c -> c.add(verificationCredential))
.singleSignOnServiceBinding(
Saml2MessageBinding.POST)
.singleSignOnServiceLocation(
samlConf.getIdpSingleLoginUrl())
.singleLogoutServiceBinding(
Saml2MessageBinding.POST)
.singleLogoutServiceLocation(
samlConf.getIdpSingleLogoutUrl())
.wantAuthnRequestsSigned(true))
.build();
return new InMemoryRelyingPartyRegistrationRepository(rp);
}
@Bean
@ConditionalOnProperty(
name = "security.saml2.enabled",
havingValue = "true",
matchIfMissing = false)
@ConditionalOnProperty(name = "security.saml2.enabled", havingValue = "true")
public OpenSaml4AuthenticationRequestResolver authenticationRequestResolver(
RelyingPartyRegistrationRepository relyingPartyRegistrationRepository) {
OpenSaml4AuthenticationRequestResolver resolver =
new OpenSaml4AuthenticationRequestResolver(relyingPartyRegistrationRepository);
resolver.setAuthnRequestCustomizer(
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();
// Log HTTP request details
log.debug("HTTP Request Method: {}", request.getMethod());
log.debug("Request URI: {}", request.getRequestURI());
log.debug("Request URL: {}", request.getRequestURL().toString());
log.debug("Query String: {}", request.getQueryString());
log.debug("Remote Address: {}", request.getRemoteAddr());
// Log headers
Collections.list(request.getHeaderNames())
.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) {
AuthnRequest authnRequest = customizer.getAuthnRequest();
HttpSessionSaml2AuthenticationRequestRepository requestRepository =
new HttpSessionSaml2AuthenticationRequestRepository();
AbstractSaml2AuthenticationRequest saml2AuthenticationRequest =
requestRepository.loadAuthenticationRequest(request);
if (saml2AuthenticationRequest != null) {
String sessionId = request.getSession(false).getId();
log.debug(
"AssertionConsumerServiceURL: {}",
authnRequest.getAssertionConsumerServiceURL());
}
// Log NameID policy if present
if (authnRequest.getNameIDPolicy() != null) {
log.debug(
"NameIDPolicy Format: {}",
authnRequest.getNameIDPolicy().getFormat());
"Retrieving SAML 2 authentication request ID from the current HTTP session {}",
sessionId);
String authenticationRequestId = saml2AuthenticationRequest.getId();
if (!authenticationRequestId.isBlank()) {
authnRequest.setID(authenticationRequestId);
} 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;
}
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

@ -48,7 +48,7 @@ public class SessionPersistentRegistry implements SessionRegistry {
} else if (principal instanceof OAuth2User) {
principalName = ((OAuth2User) principal).getName();
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
principalName = ((CustomSaml2AuthenticatedPrincipal) principal).getName();
principalName = ((CustomSaml2AuthenticatedPrincipal) principal).name();
} else if (principal instanceof String) {
principalName = (String) principal;
}
@ -79,7 +79,7 @@ public class SessionPersistentRegistry implements SessionRegistry {
} else if (principal instanceof OAuth2User) {
principalName = ((OAuth2User) principal).getName();
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
principalName = ((CustomSaml2AuthenticatedPrincipal) principal).getName();
principalName = ((CustomSaml2AuthenticatedPrincipal) principal).name();
} else if (principal instanceof String) {
principalName = (String) principal;
}

View File

@ -36,7 +36,7 @@ import stirling.software.SPDF.model.AuthenticationType;
import stirling.software.SPDF.model.Role;
import stirling.software.SPDF.model.User;
import stirling.software.SPDF.model.api.user.UsernameAndPass;
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
import stirling.software.SPDF.model.exception.UnsupportedProviderException;
@Controller
@Tag(name = "User", description = "User APIs")
@ -126,7 +126,7 @@ public class UserController {
return new RedirectView("/change-creds?messageType=notAuthenticated", true);
}
Optional<User> userOpt = userService.findByUsernameIgnoreCase(principal.getName());
if (userOpt == null || userOpt.isEmpty()) {
if (userOpt.isEmpty()) {
return new RedirectView("/change-creds?messageType=userNotFound", true);
}
User user = userOpt.get();
@ -154,7 +154,7 @@ public class UserController {
return new RedirectView("/account?messageType=notAuthenticated", true);
}
Optional<User> userOpt = userService.findByUsernameIgnoreCase(principal.getName());
if (userOpt == null || userOpt.isEmpty()) {
if (userOpt.isEmpty()) {
return new RedirectView("/account?messageType=userNotFound", true);
}
User user = userOpt.get();
@ -176,7 +176,7 @@ public class UserController {
for (Map.Entry<String, String[]> entry : paramMap.entrySet()) {
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
userService.updateUserSettings(principal.getName(), updates);
// Redirect to a page of your choice after updating
@ -199,7 +199,7 @@ public class UserController {
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
if (userOpt.isPresent()) {
User user = userOpt.get();
if (user != null && user.getUsername().equalsIgnoreCase(username)) {
if (user.getUsername().equalsIgnoreCase(username)) {
return new RedirectView("/addUsers?messageType=usernameExists", true);
}
}
@ -276,7 +276,7 @@ public class UserController {
Authentication authentication)
throws SQLException, UnsupportedProviderException {
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
if (!userOpt.isPresent()) {
if (userOpt.isEmpty()) {
return new RedirectView("/addUsers?messageType=userNotFound", true);
}
if (!userService.usernameExistsIgnoreCase(username)) {
@ -295,20 +295,20 @@ public class UserController {
List<Object> principals = sessionRegistry.getAllPrincipals();
String userNameP = "";
for (Object principal : principals) {
List<SessionInformation> sessionsInformations =
List<SessionInformation> sessionsInformation =
sessionRegistry.getAllSessions(principal, false);
if (principal instanceof UserDetails) {
userNameP = ((UserDetails) principal).getUsername();
} else if (principal instanceof OAuth2User) {
userNameP = ((OAuth2User) principal).getName();
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
userNameP = ((CustomSaml2AuthenticatedPrincipal) principal).getName();
userNameP = ((CustomSaml2AuthenticatedPrincipal) principal).name();
} else if (principal instanceof String) {
userNameP = (String) principal;
}
if (userNameP.equalsIgnoreCase(username)) {
for (SessionInformation sessionsInformation : sessionsInformations) {
sessionRegistry.expireSession(sessionsInformation.getSessionId());
for (SessionInformation sessionInfo : sessionsInformation) {
sessionRegistry.expireSession(sessionInfo.getSessionId());
}
}
}

View File

@ -1,8 +1,15 @@
package stirling.software.SPDF.controller.web;
import static stirling.software.SPDF.utils.validation.Validator.validateProvider;
import java.time.Instant;
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 org.springframework.security.access.prepost.PreAuthorize;
@ -24,12 +31,16 @@ import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
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.OAUTH2;
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2.Client;
import stirling.software.SPDF.model.ApplicationProperties.Security.SAML2;
import stirling.software.SPDF.model.provider.GithubProvider;
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.GoogleProvider;
import stirling.software.SPDF.model.provider.KeycloakProvider;
import stirling.software.SPDF.repository.UserRepository;
@ -39,12 +50,12 @@ import stirling.software.SPDF.repository.UserRepository;
@Tag(name = "Account Security", description = "Account Security APIs")
public class AccountWebController {
public static final String OAUTH_2_AUTHORIZATION = "/oauth2/authorization/";
private final ApplicationProperties applicationProperties;
private final SessionPersistentRegistry sessionPersistentRegistry;
private final UserRepository // Assuming you have a repository for user operations
userRepository;
// Assuming you have a repository for user operations
private final UserRepository userRepository;
public AccountWebController(
ApplicationProperties applicationProperties,
@ -61,132 +72,127 @@ public class AccountWebController {
if (authentication != null && authentication.isAuthenticated()) {
return "redirect:/";
}
Map<String, String> providerList = new HashMap<>();
Security securityProps = applicationProperties.getSecurity();
OAUTH2 oauth = securityProps.getOauth2();
if (oauth != null) {
if (oauth.getEnabled()) {
if (oauth.isSettingsValid()) {
providerList.put("/oauth2/authorization/oidc", oauth.getProvider());
String firstChar = String.valueOf(oauth.getProvider().charAt(0));
String clientName =
oauth.getProvider().replaceFirst(firstChar, firstChar.toUpperCase());
providerList.put(OAUTH_2_AUTHORIZATION + oauth.getProvider(), clientName);
}
Client client = oauth.getClient();
if (client != null) {
GoogleProvider google = client.getGoogle();
if (google.isSettingsValid()) {
if (validateProvider(google)) {
providerList.put(
"/oauth2/authorization/" + google.getName(),
google.getClientName());
OAUTH_2_AUTHORIZATION + google.getName(), google.getClientName());
}
GithubProvider github = client.getGithub();
if (github.isSettingsValid()) {
GitHubProvider github = client.getGithub();
if (validateProvider(github)) {
providerList.put(
"/oauth2/authorization/" + github.getName(),
github.getClientName());
OAUTH_2_AUTHORIZATION + github.getName(), github.getClientName());
}
KeycloakProvider keycloak = client.getKeycloak();
if (keycloak.isSettingsValid()) {
if (validateProvider(keycloak)) {
providerList.put(
"/oauth2/authorization/" + keycloak.getName(),
OAUTH_2_AUTHORIZATION + keycloak.getName(),
keycloak.getClientName());
}
}
}
}
SAML2 saml2 = securityProps.getSaml2();
if (securityProps.isSaml2Activ()
&& applicationProperties.getSystem().getEnableAlphaFunctionality()) {
providerList.put("/saml2/authenticate/" + saml2.getRegistrationId(), "SAML 2");
if (securityProps.isSaml2Active()
&& applicationProperties.getSystem().getEnableAlphaFunctionality()
&& applicationProperties.getEnterpriseEdition().isEnabled()) {
String samlIdp = saml2.getProvider();
String saml2AuthenticationPath = "/saml2/authenticate/" + saml2.getRegistrationId();
if (applicationProperties.getEnterpriseEdition().isSsoAutoLogin()) {
return "redirect:"
+ request.getRequestURL()
+ saml2AuthenticationPath;
} else {
providerList.put(saml2AuthenticationPath, samlIdp + " (SAML 2)");
}
}
// Remove any null keys/values from the providerList
providerList
.entrySet()
.removeIf(entry -> entry.getKey() == null || entry.getValue() == null);
model.addAttribute("providerlist", providerList);
model.addAttribute("providerList", providerList);
model.addAttribute("loginMethod", securityProps.getLoginMethod());
boolean altLogin = providerList.size() > 0 ? securityProps.isAltLogin() : false;
boolean altLogin = !providerList.isEmpty() ? securityProps.isAltLogin() : false;
model.addAttribute("altLogin", altLogin);
model.addAttribute("currentPage", "login");
String error = request.getParameter("error");
if (error != null) {
switch (error) {
case "badcredentials":
error = "login.invalid";
break;
case "locked":
error = "login.locked";
break;
case "oauth2AuthenticationError":
error = "userAlreadyExistsOAuthMessage";
break;
default:
break;
case "badCredentials" -> error = "login.invalid";
case "locked" -> error = "login.locked";
case "oauth2AuthenticationError" -> error = "userAlreadyExistsOAuthMessage";
}
model.addAttribute("error", error);
}
String erroroauth = request.getParameter("erroroauth");
if (erroroauth != null) {
switch (erroroauth) {
case "oauth2AutoCreateDisabled":
erroroauth = "login.oauth2AutoCreateDisabled";
break;
case "invalidUsername":
erroroauth = "login.invalid";
break;
case "userAlreadyExistsWeb":
erroroauth = "userAlreadyExistsWebMessage";
break;
case "oauth2AuthenticationErrorWeb":
erroroauth = "login.oauth2InvalidUserType";
break;
case "invalid_token_response":
erroroauth = "login.oauth2InvalidTokenResponse";
break;
case "authorization_request_not_found":
erroroauth = "login.oauth2RequestNotFound";
break;
case "access_denied":
erroroauth = "login.oauth2AccessDenied";
break;
case "invalid_user_info_response":
erroroauth = "login.oauth2InvalidUserInfoResponse";
break;
case "invalid_request":
erroroauth = "login.oauth2invalidRequest";
break;
case "invalid_id_token":
erroroauth = "login.oauth2InvalidIdToken";
break;
case "oauth2_admin_blocked_user":
erroroauth = "login.oauth2AdminBlockedUser";
break;
case "userIsDisabled":
erroroauth = "login.userIsDisabled";
break;
case "invalid_destination":
erroroauth = "login.invalid_destination";
break;
case "relying_party_registration_not_found":
erroroauth = "login.relyingPartyRegistrationNotFound";
break;
String errorOAuth = request.getParameter("errorOAuth");
if (errorOAuth != null) {
switch (errorOAuth) {
case "oAuth2AutoCreateDisabled" -> errorOAuth = "login.oAuth2AutoCreateDisabled";
case "invalidUsername" -> errorOAuth = "login.invalid";
case "userAlreadyExistsWeb" -> errorOAuth = "userAlreadyExistsWebMessage";
case "oAuth2AuthenticationErrorWeb" -> errorOAuth = "login.oauth2InvalidUserType";
case "invalid_token_response" -> errorOAuth = "login.oauth2InvalidTokenResponse";
case "authorization_request_not_found" ->
errorOAuth = "login.oauth2RequestNotFound";
case "access_denied" -> errorOAuth = "login.oauth2AccessDenied";
case "invalid_user_info_response" ->
errorOAuth = "login.oauth2InvalidUserInfoResponse";
case "invalid_request" -> errorOAuth = "login.oauth2invalidRequest";
case "invalid_id_token" -> errorOAuth = "login.oauth2InvalidIdToken";
case "oAuth2AdminBlockedUser" -> errorOAuth = "login.oAuth2AdminBlockedUser";
case "userIsDisabled" -> errorOAuth = "login.userIsDisabled";
case "invalid_destination" -> errorOAuth = "login.invalid_destination";
case "relying_party_registration_not_found" ->
errorOAuth = "login.relyingPartyRegistrationNotFound";
// Valid InResponseTo was not available from the validation context, unable to
// evaluate
case "invalid_in_response_to":
erroroauth = "login.invalid_in_response_to";
break;
case "not_authentication_provider_found":
erroroauth = "login.not_authentication_provider_found";
break;
default:
break;
case "invalid_in_response_to" -> errorOAuth = "login.invalid_in_response_to";
case "not_authentication_provider_found" ->
errorOAuth = "login.not_authentication_provider_found";
}
model.addAttribute("erroroauth", erroroauth);
model.addAttribute("errorOAuth", errorOAuth);
}
if (request.getParameter("messageType") != null) {
model.addAttribute("messageType", "changedCredsMessage");
}
if (request.getParameter("logout") != null) {
model.addAttribute("logoutMessage", "You have been logged out.");
}
return "login";
}
@ -230,13 +236,11 @@ public class AccountWebController {
.plus(maxInactiveInterval, ChronoUnit.SECONDS);
if (now.isAfter(expirationTime)) {
sessionPersistentRegistry.expireSession(sessionEntity.getSessionId());
hasActiveSession = false;
} else {
hasActiveSession = !sessionEntity.isExpired();
}
lastRequest = sessionEntity.getLastRequest();
} else {
hasActiveSession = false;
// No session, set default last request time
lastRequest = new Date(0);
}
@ -273,53 +277,41 @@ public class AccountWebController {
})
.collect(Collectors.toList());
String messageType = request.getParameter("messageType");
String deleteMessage = null;
String deleteMessage;
if (messageType != null) {
switch (messageType) {
case "deleteCurrentUser":
deleteMessage = "deleteCurrentUserMessage";
break;
case "deleteUsernameExists":
deleteMessage = "deleteUsernameExistsMessage";
break;
default:
break;
}
deleteMessage =
switch (messageType) {
case "deleteCurrentUser" -> "deleteCurrentUserMessage";
case "deleteUsernameExists" -> "deleteUsernameExistsMessage";
default -> null;
};
model.addAttribute("deleteMessage", deleteMessage);
String addMessage = null;
switch (messageType) {
case "usernameExists":
addMessage = "usernameExistsMessage";
break;
case "invalidUsername":
addMessage = "invalidUsernameMessage";
break;
case "invalidPassword":
addMessage = "invalidPasswordMessage";
break;
default:
break;
}
String addMessage;
addMessage =
switch (messageType) {
case "usernameExists" -> "usernameExistsMessage";
case "invalidUsername" -> "invalidUsernameMessage";
case "invalidPassword" -> "invalidPasswordMessage";
default -> null;
};
model.addAttribute("addMessage", addMessage);
}
String changeMessage = null;
String changeMessage;
if (messageType != null) {
switch (messageType) {
case "userNotFound":
changeMessage = "userNotFoundMessage";
break;
case "downgradeCurrentUser":
changeMessage = "downgradeCurrentUserMessage";
break;
case "disabledCurrentUser":
changeMessage = "disabledCurrentUserMessage";
break;
default:
changeMessage = messageType;
break;
}
changeMessage =
switch (messageType) {
case "userNotFound" -> "userNotFoundMessage";
case "downgradeCurrentUser" -> "downgradeCurrentUserMessage";
case "disabledCurrentUser" -> "disabledCurrentUserMessage";
default -> messageType;
};
model.addAttribute("changeMessage", changeMessage);
}
model.addAttribute("users", sortedUsers);
model.addAttribute("currentUsername", authentication.getName());
model.addAttribute("roleDetails", roleDetails);
@ -340,78 +332,51 @@ public class AccountWebController {
if (authentication != null && authentication.isAuthenticated()) {
Object principal = authentication.getPrincipal();
String username = null;
if (principal instanceof UserDetails) {
// Cast the principal object to UserDetails
UserDetails userDetails = (UserDetails) principal;
// Retrieve username and other attributes
// Retrieve username and other attributes and add login attributes to the model
if (principal instanceof UserDetails userDetails) {
username = userDetails.getUsername();
// Add oAuth2 Login attributes to the model
model.addAttribute("oAuth2Login", false);
}
if (principal instanceof OAuth2User) {
// Cast the principal object to OAuth2User
OAuth2User userDetails = (OAuth2User) principal;
// Retrieve username and other attributes
username =
userDetails.getAttribute(
applicationProperties.getSecurity().getOauth2().getUseAsUsername());
// Add oAuth2 Login attributes to the model
if (principal instanceof OAuth2User userDetails) {
username = userDetails.getName();
model.addAttribute("oAuth2Login", true);
}
if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
// Cast the principal object to OAuth2User
CustomSaml2AuthenticatedPrincipal userDetails =
(CustomSaml2AuthenticatedPrincipal) principal;
// Retrieve username and other attributes
username = userDetails.getName();
// Add oAuth2 Login attributes to the model
model.addAttribute("oAuth2Login", true);
if (principal instanceof CustomSaml2AuthenticatedPrincipal userDetails) {
username = userDetails.name();
model.addAttribute("saml2Login", true);
}
if (username != null) {
// Fetch user details from the database
Optional<User> user =
userRepository
.findByUsernameIgnoreCaseWithSettings( // Assuming findByUsername
// method exists
username);
if (!user.isPresent()) {
Optional<User> user = userRepository.findByUsernameIgnoreCaseWithSettings(username);
if (user.isEmpty()) {
return "redirect:/error";
}
// Convert settings map to JSON string
ObjectMapper objectMapper = new ObjectMapper();
String settingsJson;
try {
settingsJson = objectMapper.writeValueAsString(user.get().getSettings());
} catch (JsonProcessingException e) {
// Handle JSON conversion error
log.error("exception", e);
log.error("Error converting settings map", e);
return "redirect:/error";
}
String messageType = request.getParameter("messageType");
if (messageType != null) {
switch (messageType) {
case "notAuthenticated":
messageType = "notAuthenticatedMessage";
break;
case "userNotFound":
messageType = "userNotFoundMessage";
break;
case "incorrectPassword":
messageType = "incorrectPasswordMessage";
break;
case "usernameExists":
messageType = "usernameExistsMessage";
break;
case "invalidUsername":
messageType = "invalidUsernameMessage";
break;
default:
break;
case "notAuthenticated" -> messageType = "notAuthenticatedMessage";
case "userNotFound" -> messageType = "userNotFoundMessage";
case "incorrectPassword" -> messageType = "incorrectPasswordMessage";
case "usernameExists" -> messageType = "usernameExistsMessage";
case "invalidUsername" -> messageType = "invalidUsernameMessage";
}
model.addAttribute("messageType", messageType);
}
// Add attributes to the model
model.addAttribute("username", username);
model.addAttribute("messageType", messageType);
model.addAttribute("role", user.get().getRolesAsString());
model.addAttribute("settings", settingsJson);
model.addAttribute("changeCredsFlag", user.get().isFirstLogin());
@ -432,19 +397,12 @@ public class AccountWebController {
}
if (authentication != null && authentication.isAuthenticated()) {
Object principal = authentication.getPrincipal();
if (principal instanceof UserDetails) {
// Cast the principal object to UserDetails
UserDetails userDetails = (UserDetails) principal;
// Retrieve username and other attributes
if (principal instanceof UserDetails userDetails) {
String username = userDetails.getUsername();
// Fetch user details from the database
Optional<User> user =
userRepository
.findByUsernameIgnoreCase( // Assuming findByUsername method exists
username);
if (!user.isPresent()) {
// Handle error appropriately
// Example redirection in case of error
Optional<User> user = userRepository.findByUsernameIgnoreCase(username);
if (user.isEmpty()) {
// Handle error appropriately, example redirection in case of error
return "redirect:/error";
}
String messageType = request.getParameter("messageType");
@ -467,7 +425,7 @@ public class AccountWebController {
}
model.addAttribute("messageType", messageType);
}
// Add attributes to the model
model.addAttribute("username", username);
}
} else {

View File

@ -1,5 +1,7 @@
package stirling.software.SPDF.model;
import static stirling.software.SPDF.utils.validation.Validator.*;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
@ -12,7 +14,6 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
@ -34,10 +35,11 @@ import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.InstallationPathConfig;
import stirling.software.SPDF.config.YamlPropertySourceFactory;
import stirling.software.SPDF.model.provider.GithubProvider;
import stirling.software.SPDF.model.exception.UnsupportedProviderException;
import stirling.software.SPDF.model.provider.GitHubProvider;
import stirling.software.SPDF.model.provider.GoogleProvider;
import stirling.software.SPDF.model.provider.KeycloakProvider;
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
import stirling.software.SPDF.model.provider.Provider;
@Configuration
@ConfigurationProperties(prefix = "")
@ -136,13 +138,13 @@ public class ApplicationProperties {
|| loginMethod.equalsIgnoreCase(LoginMethods.ALL.toString()));
}
public boolean isOauth2Activ() {
public boolean isOauth2Active() {
return (oauth2 != null
&& oauth2.getEnabled()
&& !loginMethod.equalsIgnoreCase(LoginMethods.NORMAL.toString()));
}
public boolean isSaml2Activ() {
public boolean isSaml2Active() {
return (saml2 != null
&& saml2.getEnabled()
&& !loginMethod.equalsIgnoreCase(LoginMethods.NORMAL.toString()));
@ -158,6 +160,7 @@ public class ApplicationProperties {
@Setter
@ToString
public static class SAML2 {
private String provider;
private Boolean enabled = false;
private Boolean autoCreateUser = false;
private Boolean blockRegistration = false;
@ -195,7 +198,7 @@ public class ApplicationProperties {
}
}
public Resource getidpCert() {
public Resource getIdpCert() {
if (idpCert == null) return null;
if (idpCert.startsWith("classpath:")) {
return new ClassPathResource(idpCert.substring("classpath:".length()));
@ -225,12 +228,11 @@ public class ApplicationProperties {
private Collection<String> scopes = new ArrayList<>();
private String provider;
private Client client = new Client();
private String logoutUrl;
public void setScopes(String scopes) {
List<String> scopesList =
Arrays.stream(scopes.split(","))
.map(String::trim)
.collect(Collectors.toList());
Arrays.stream(scopes.split(",")).map(String::trim).toList();
this.scopes.addAll(scopesList);
}
@ -243,32 +245,31 @@ public class ApplicationProperties {
}
public boolean isSettingsValid() {
return isValid(this.getIssuer(), "issuer")
&& isValid(this.getClientId(), "clientId")
&& isValid(this.getClientSecret(), "clientSecret")
&& isValid(this.getScopes(), "scopes")
&& isValid(this.getUseAsUsername(), "useAsUsername");
return !isStringEmpty(this.getIssuer())
&& !isStringEmpty(this.getClientId())
&& !isStringEmpty(this.getClientSecret())
&& !isCollectionEmpty(this.getScopes())
&& !isStringEmpty(this.getUseAsUsername());
}
@Data
public static class Client {
private GoogleProvider google = new GoogleProvider();
private GithubProvider github = new GithubProvider();
private GitHubProvider github = new GitHubProvider();
private KeycloakProvider keycloak = new KeycloakProvider();
public Provider get(String registrationId) throws UnsupportedProviderException {
switch (registrationId.toLowerCase()) {
case "google":
return getGoogle();
case "github":
return getGithub();
case "keycloak":
return getKeycloak();
default:
throw new UnsupportedProviderException(
"Logout from the provider is not supported? Report it at"
+ " https://github.com/Stirling-Tools/Stirling-PDF/issues");
}
return switch (registrationId.toLowerCase()) {
case "google" -> getGoogle();
case "github" -> getGithub();
case "keycloak" -> getKeycloak();
default ->
throw new UnsupportedProviderException(
"Logout from the provider "
+ registrationId
+ " is not supported. "
+ "Report it at https://github.com/Stirling-Tools/Stirling-PDF/issues");
};
}
}
}
@ -335,10 +336,10 @@ public class ApplicationProperties {
@Override
public String toString() {
return """
Driver {
driverName='%s'
}
"""
Driver {
driverName='%s'
}
"""
.formatted(driverName);
}
}

View File

@ -1,80 +0,0 @@
package stirling.software.SPDF.model;
import java.util.Collection;
public class Provider implements ProviderInterface {
private String name;
private String clientName;
public String getName() {
return name;
}
public String getClientName() {
return clientName;
}
protected boolean isValid(String value, String name) {
if (value != null && !value.trim().isEmpty()) {
return true;
}
return false;
}
protected boolean isValid(Collection<String> value, String name) {
if (value != null && !value.isEmpty()) {
return true;
}
return false;
}
@Override
public Collection<String> getScopes() {
throw new UnsupportedOperationException("Unimplemented method 'getScope'");
}
@Override
public void setScopes(String scopes) {
throw new UnsupportedOperationException("Unimplemented method 'setScope'");
}
@Override
public String getUseAsUsername() {
throw new UnsupportedOperationException("Unimplemented method 'getUseAsUsername'");
}
@Override
public void setUseAsUsername(String useAsUsername) {
throw new UnsupportedOperationException("Unimplemented method 'setUseAsUsername'");
}
@Override
public String getIssuer() {
throw new UnsupportedOperationException("Unimplemented method 'getIssuer'");
}
@Override
public void setIssuer(String issuer) {
throw new UnsupportedOperationException("Unimplemented method 'setIssuer'");
}
@Override
public String getClientSecret() {
throw new UnsupportedOperationException("Unimplemented method 'getClientSecret'");
}
@Override
public void setClientSecret(String clientSecret) {
throw new UnsupportedOperationException("Unimplemented method 'setClientSecret'");
}
@Override
public String getClientId() {
throw new UnsupportedOperationException("Unimplemented method 'getClientId'");
}
@Override
public void setClientId(String clientId) {
throw new UnsupportedOperationException("Unimplemented method 'setClientId'");
}
}

View File

@ -1,26 +0,0 @@
package stirling.software.SPDF.model;
import java.util.Collection;
public interface ProviderInterface {
public Collection<String> getScopes();
public void setScopes(String scopes);
public String getUseAsUsername();
public void setUseAsUsername(String useAsUsername);
public String getIssuer();
public void setIssuer(String issuer);
public String getClientSecret();
public void setClientSecret(String clientSecret);
public String getClientId();
public void setClientId(String clientId);
}

View File

@ -0,0 +1,24 @@
package stirling.software.SPDF.model;
import lombok.Getter;
@Getter
public enum UsernameAttribute {
EMAIL("email"),
LOGIN("login"),
PROFILE("profile"),
NAME("name"),
USERNAME("username"),
NICKNAME("nickname"),
GIVEN_NAME("given_name"),
MIDDLE_NAME("middle_name"),
FAMILY_NAME("family_name"),
PREFERRED_NAME("preferred_name"),
PREFERRED_USERNAME("preferred_username");
private final String name;
UsernameAttribute(final String name) {
this.name = name;
}
}

View File

@ -0,0 +1,11 @@
package stirling.software.SPDF.model.exception;
public class NoProviderFoundException extends Exception {
public NoProviderFoundException(String message) {
super(message);
}
public NoProviderFoundException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -1,4 +1,4 @@
package stirling.software.SPDF.model.provider;
package stirling.software.SPDF.model.exception;
public class UnsupportedProviderException extends Exception {
public UnsupportedProviderException(String message) {

View File

@ -0,0 +1,7 @@
package stirling.software.SPDF.model.exception;
public class UnsupportedUsernameAttribute extends RuntimeException {
public UnsupportedUsernameAttribute(String message) {
super(message);
}
}

View File

@ -0,0 +1,86 @@
package stirling.software.SPDF.model.provider;
import java.util.ArrayList;
import java.util.Collection;
import lombok.NoArgsConstructor;
import stirling.software.SPDF.model.UsernameAttribute;
@NoArgsConstructor
public class GitHubProvider extends Provider {
private static final String NAME = "github";
private static final String CLIENT_NAME = "GitHub";
private static final String AUTHORIZATION_URI = "https://github.com/login/oauth/authorize";
private static final String TOKEN_URI = "https://github.com/login/oauth/access_token";
private static final String USER_INFO_URI = "https://api.github.com/user";
public GitHubProvider(
String clientId,
String clientSecret,
Collection<String> scopes,
UsernameAttribute useAsUsername) {
super(
null,
NAME,
CLIENT_NAME,
clientId,
clientSecret,
scopes,
useAsUsername != null ? useAsUsername : UsernameAttribute.LOGIN,
null,
AUTHORIZATION_URI,
TOKEN_URI,
USER_INFO_URI);
}
@Override
public String getAuthorizationUri() {
return AUTHORIZATION_URI;
}
@Override
public String getTokenUri() {
return TOKEN_URI;
}
@Override
public String getUserInfoUri() {
return USER_INFO_URI;
}
@Override
public String getName() {
return NAME;
}
@Override
public String getClientName() {
return CLIENT_NAME;
}
@Override
public Collection<String> getScopes() {
Collection<String> scopes = super.getScopes();
if (scopes == null || scopes.isEmpty()) {
scopes = new ArrayList<>();
scopes.add("read:user");
}
return scopes;
}
@Override
public String toString() {
return "GitHub [clientId="
+ getClientId()
+ ", clientSecret="
+ (getClientSecret() != null && !getClientSecret().isEmpty() ? "*****" : "NULL")
+ ", scopes="
+ getScopes()
+ ", useAsUsername="
+ getUseAsUsername()
+ "]";
}
}

View File

@ -1,114 +0,0 @@
package stirling.software.SPDF.model.provider;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.stream.Collectors;
import stirling.software.SPDF.model.Provider;
public class GithubProvider extends Provider {
private static final String authorizationUri = "https://github.com/login/oauth/authorize";
private static final String tokenUri = "https://github.com/login/oauth/access_token";
private static final String userInfoUri = "https://api.github.com/user";
private String clientId;
private String clientSecret;
private Collection<String> scopes = new ArrayList<>();
private String useAsUsername = "login";
public String getAuthorizationuri() {
return authorizationUri;
}
public String getTokenuri() {
return tokenUri;
}
public String getUserinfouri() {
return userInfoUri;
}
@Override
public String getIssuer() {
return new String();
}
@Override
public void setIssuer(String issuer) {}
@Override
public String getClientId() {
return this.clientId;
}
@Override
public void setClientId(String clientId) {
this.clientId = clientId;
}
@Override
public String getClientSecret() {
return this.clientSecret;
}
@Override
public void setClientSecret(String clientSecret) {
this.clientSecret = clientSecret;
}
@Override
public Collection<String> getScopes() {
if (scopes == null || scopes.isEmpty()) {
scopes = new ArrayList<>();
scopes.add("read:user");
}
return scopes;
}
@Override
public void setScopes(String scopes) {
this.scopes =
Arrays.stream(scopes.split(",")).map(String::trim).collect(Collectors.toList());
}
@Override
public String getUseAsUsername() {
return this.useAsUsername;
}
@Override
public void setUseAsUsername(String useAsUsername) {
this.useAsUsername = useAsUsername;
}
@Override
public String toString() {
return "GitHub [clientId="
+ clientId
+ ", clientSecret="
+ (clientSecret != null && !clientSecret.isEmpty() ? "MASKED" : "NULL")
+ ", scopes="
+ scopes
+ ", useAsUsername="
+ useAsUsername
+ "]";
}
@Override
public String getName() {
return "github";
}
@Override
public String getClientName() {
return "GitHub";
}
public boolean isSettingsValid() {
return super.isValid(this.getClientId(), "clientId")
&& super.isValid(this.getClientSecret(), "clientSecret")
&& super.isValid(this.getScopes(), "scopes")
&& isValid(this.getUseAsUsername(), "useAsUsername");
}
}

View File

@ -1,116 +1,85 @@
package stirling.software.SPDF.model.provider;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.stream.Collectors;
import stirling.software.SPDF.model.Provider;
import lombok.NoArgsConstructor;
import stirling.software.SPDF.model.UsernameAttribute;
@NoArgsConstructor
public class GoogleProvider extends Provider {
private static final String authorizationUri = "https://accounts.google.com/o/oauth2/v2/auth";
private static final String tokenUri = "https://www.googleapis.com/oauth2/v4/token";
private static final String userInfoUri =
private static final String NAME = "google";
private static final String CLIENT_NAME = "Google";
private static final String AUTHORIZATION_URI = "https://accounts.google.com/o/oauth2/v2/auth";
private static final String TOKEN_URI = "https://www.googleapis.com/oauth2/v4/token";
private static final String USER_INFO_URI =
"https://www.googleapis.com/oauth2/v3/userinfo?alt=json";
private String clientId;
private String clientSecret;
private Collection<String> scopes = new ArrayList<>();
private String useAsUsername = "email";
public String getAuthorizationuri() {
return authorizationUri;
public GoogleProvider(
String clientId,
String clientSecret,
Collection<String> scopes,
UsernameAttribute useAsUsername) {
super(
null,
NAME,
CLIENT_NAME,
clientId,
clientSecret,
scopes,
useAsUsername,
null,
AUTHORIZATION_URI,
TOKEN_URI,
USER_INFO_URI);
}
public String getTokenuri() {
return tokenUri;
public String getAuthorizationUri() {
return AUTHORIZATION_URI;
}
public String getUserinfouri() {
return userInfoUri;
public String getTokenUri() {
return TOKEN_URI;
}
public String getUserinfoUri() {
return USER_INFO_URI;
}
@Override
public String getIssuer() {
return new String();
public String getName() {
return NAME;
}
@Override
public void setIssuer(String issuer) {}
@Override
public String getClientId() {
return this.clientId;
}
@Override
public void setClientId(String clientId) {
this.clientId = clientId;
}
@Override
public String getClientSecret() {
return this.clientSecret;
}
@Override
public void setClientSecret(String clientSecret) {
this.clientSecret = clientSecret;
public String getClientName() {
return CLIENT_NAME;
}
@Override
public Collection<String> getScopes() {
Collection<String> scopes = super.getScopes();
if (scopes == null || scopes.isEmpty()) {
scopes = new ArrayList<>();
scopes.add("https://www.googleapis.com/auth/userinfo.email");
scopes.add("https://www.googleapis.com/auth/userinfo.profile");
}
return scopes;
}
@Override
public void setScopes(String scopes) {
this.scopes =
Arrays.stream(scopes.split(",")).map(String::trim).collect(Collectors.toList());
}
@Override
public String getUseAsUsername() {
return this.useAsUsername;
}
@Override
public void setUseAsUsername(String useAsUsername) {
this.useAsUsername = useAsUsername;
}
@Override
public String toString() {
return "Google [clientId="
+ clientId
+ getClientId()
+ ", clientSecret="
+ (clientSecret != null && !clientSecret.isEmpty() ? "MASKED" : "NULL")
+ (getClientSecret() != null && !getClientSecret().isEmpty() ? "*****" : "NULL")
+ ", scopes="
+ scopes
+ getScopes()
+ ", useAsUsername="
+ useAsUsername
+ getUseAsUsername()
+ "]";
}
@Override
public String getName() {
return "google";
}
@Override
public String getClientName() {
return "Google";
}
public boolean isSettingsValid() {
return super.isValid(this.getClientId(), "clientId")
&& super.isValid(this.getClientSecret(), "clientSecret")
&& super.isValid(this.getScopes(), "scopes")
&& isValid(this.getUseAsUsername(), "useAsUsername");
}
}

View File

@ -1,106 +1,72 @@
package stirling.software.SPDF.model.provider;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.stream.Collectors;
import stirling.software.SPDF.model.Provider;
import lombok.NoArgsConstructor;
import stirling.software.SPDF.model.UsernameAttribute;
@NoArgsConstructor
public class KeycloakProvider extends Provider {
private String issuer;
private String clientId;
private String clientSecret;
private Collection<String> scopes = new ArrayList<>();
private String useAsUsername = "email";
private static final String NAME = "keycloak";
private static final String CLIENT_NAME = "Keycloak";
@Override
public String getIssuer() {
return this.issuer;
public KeycloakProvider(
String issuer,
String clientId,
String clientSecret,
Collection<String> scopes,
UsernameAttribute useAsUsername) {
super(
issuer,
NAME,
CLIENT_NAME,
clientId,
clientSecret,
scopes,
useAsUsername,
null,
null,
null,
null);
}
@Override
public void setIssuer(String issuer) {
this.issuer = issuer;
public String getName() {
return NAME;
}
@Override
public String getClientId() {
return this.clientId;
}
@Override
public void setClientId(String clientId) {
this.clientId = clientId;
}
@Override
public String getClientSecret() {
return this.clientSecret;
}
@Override
public void setClientSecret(String clientSecret) {
this.clientSecret = clientSecret;
public String getClientName() {
return CLIENT_NAME;
}
@Override
public Collection<String> getScopes() {
Collection<String> scopes = super.getScopes();
if (scopes == null || scopes.isEmpty()) {
scopes = new ArrayList<>();
scopes.add("profile");
scopes.add("email");
}
return scopes;
}
@Override
public void setScopes(String scopes) {
this.scopes =
Arrays.stream(scopes.split(",")).map(String::trim).collect(Collectors.toList());
}
@Override
public String getUseAsUsername() {
return this.useAsUsername;
}
@Override
public void setUseAsUsername(String useAsUsername) {
this.useAsUsername = useAsUsername;
}
@Override
public String toString() {
return "Keycloak [issuer="
+ issuer
+ getIssuer()
+ ", clientId="
+ clientId
+ getClientId()
+ ", clientSecret="
+ (clientSecret != null && !clientSecret.isEmpty() ? "MASKED" : "NULL")
+ (getClientSecret() != null && !getClientSecret().isBlank() ? "*****" : "NULL")
+ ", scopes="
+ scopes
+ getScopes()
+ ", useAsUsername="
+ useAsUsername
+ getUseAsUsername()
+ "]";
}
@Override
public String getName() {
return "keycloak";
}
@Override
public String getClientName() {
return "Keycloak";
}
public boolean isSettingsValid() {
return isValid(this.getIssuer(), "issuer")
&& isValid(this.getClientId(), "clientId")
&& isValid(this.getClientSecret(), "clientSecret")
&& isValid(this.getScopes(), "scopes")
&& isValid(this.getUseAsUsername(), "useAsUsername");
}
}

View File

@ -0,0 +1,134 @@
package stirling.software.SPDF.model.provider;
import static stirling.software.SPDF.model.UsernameAttribute.EMAIL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.stream.Collectors;
import lombok.Data;
import lombok.NoArgsConstructor;
import stirling.software.SPDF.model.UsernameAttribute;
import stirling.software.SPDF.model.exception.UnsupportedUsernameAttribute;
@Data
@NoArgsConstructor
public class Provider {
public static final String EXCEPTION_MESSAGE = "The attribute %s is not supported for %s.";
private String issuer;
private String name;
private String clientName;
private String clientId;
private String clientSecret;
private Collection<String> scopes;
private UsernameAttribute useAsUsername;
private String logoutUrl;
private String authorizationUri;
private String tokenUri;
private String userInfoUri;
public Provider(
String issuer,
String name,
String clientName,
String clientId,
String clientSecret,
Collection<String> scopes,
UsernameAttribute useAsUsername,
String logoutUrl,
String authorizationUri,
String tokenUri,
String userInfoUri) {
this.issuer = issuer;
this.name = name;
this.clientName = clientName;
this.clientId = clientId;
this.clientSecret = clientSecret;
this.scopes = scopes == null ? new ArrayList<>() : scopes;
this.useAsUsername =
useAsUsername != null ? validateUsernameAttribute(useAsUsername) : EMAIL;
this.logoutUrl = logoutUrl;
this.authorizationUri = authorizationUri;
this.tokenUri = tokenUri;
this.userInfoUri = userInfoUri;
}
public void setScopes(String scopes) {
if (scopes != null && !scopes.isBlank()) {
this.scopes =
Arrays.stream(scopes.split(",")).map(String::trim).collect(Collectors.toList());
}
}
private UsernameAttribute validateUsernameAttribute(UsernameAttribute usernameAttribute) {
switch (name) {
case "google" -> {
return validateGoogleUsernameAttribute(usernameAttribute);
}
case "github" -> {
return validateGitHubUsernameAttribute(usernameAttribute);
}
case "keycloak" -> {
return validateKeycloakUsernameAttribute(usernameAttribute);
}
default -> {
return usernameAttribute;
}
}
}
private UsernameAttribute validateKeycloakUsernameAttribute(
UsernameAttribute usernameAttribute) {
switch (usernameAttribute) {
case EMAIL, NAME, GIVEN_NAME, FAMILY_NAME, PREFERRED_USERNAME -> {
return usernameAttribute;
}
default ->
throw new UnsupportedUsernameAttribute(
String.format(EXCEPTION_MESSAGE, usernameAttribute, clientName));
}
}
private UsernameAttribute validateGoogleUsernameAttribute(UsernameAttribute usernameAttribute) {
switch (usernameAttribute) {
case EMAIL, NAME, GIVEN_NAME, FAMILY_NAME -> {
return usernameAttribute;
}
default ->
throw new UnsupportedUsernameAttribute(
String.format(EXCEPTION_MESSAGE, usernameAttribute, clientName));
}
}
private UsernameAttribute validateGitHubUsernameAttribute(UsernameAttribute usernameAttribute) {
switch (usernameAttribute) {
case LOGIN, EMAIL, NAME -> {
return usernameAttribute;
}
default ->
throw new UnsupportedUsernameAttribute(
String.format(EXCEPTION_MESSAGE, usernameAttribute, clientName));
}
}
@Override
public String toString() {
return "Provider [name="
+ getName()
+ ", clientName="
+ getClientName()
+ ", clientId="
+ getClientId()
+ ", clientSecret="
+ (getClientSecret() != null && !getClientSecret().isEmpty() ? "*****" : "NULL")
+ ", scopes="
+ getScopes()
+ ", useAsUsername="
+ getUseAsUsername()
+ "]";
}
}

View File

@ -7,8 +7,6 @@ import jakarta.servlet.http.HttpServletRequest;
public class UrlUtils {
private UrlUtils() {}
public static String getOrigin(HttpServletRequest request) {
String scheme = request.getScheme(); // http or https
String serverName = request.getServerName(); // localhost

View File

@ -0,0 +1,36 @@
package stirling.software.SPDF.utils.validation;
import java.util.Collection;
import stirling.software.SPDF.model.provider.Provider;
public class Validator {
public static boolean validateProvider(Provider provider) {
if (provider == null) {
return false;
}
if (isStringEmpty(provider.getClientId())) {
return false;
}
if (isStringEmpty(provider.getClientSecret())) {
return false;
}
if (isCollectionEmpty(provider.getScopes())) {
return false;
}
return true;
}
public static boolean isStringEmpty(String input) {
return input == null || input.isBlank();
}
public static boolean isCollectionEmpty(Collection<String> input) {
return input == null || input.isEmpty();
}
}

View File

@ -572,8 +572,8 @@ login.invalid=اسم المستخدم أو كلمة المرور غير صالح
login.locked=تم قفل حسابك.
login.signinTitle=الرجاء تسجيل الدخول
login.ssoSignIn=تسجيل الدخول عبر تسجيل الدخول الأحادي
login.oauth2AutoCreateDisabled=تم تعطيل الإنشاء التلقائي لمستخدم OAuth2
login.oauth2AdminBlockedUser=تم حظر تسجيل أو تسجيل دخول المستخدمين غير المسجلين حاليًا. يرجى الاتصال بالمسؤول.
login.oAuth2AutoCreateDisabled=تم تعطيل الإنشاء التلقائي لمستخدم OAuth2
login.oAuth2AdminBlockedUser=تم حظر تسجيل أو تسجيل دخول المستخدمين غير المسجلين حاليًا. يرجى الاتصال بالمسؤول.
login.oauth2RequestNotFound=لم يتم العثور على طلب التفويض
login.oauth2InvalidUserInfoResponse=استجابة معلومات المستخدم غير صالحة
login.oauth2invalidRequest=طلب غير صالح

View File

@ -572,8 +572,8 @@ login.invalid=Etibarsız istifadəçi adı və ya şifr.
login.locked=Sizin hesabınız kilidlənmişdir.
login.signinTitle=Zəhmət olmasa, daxil olun
login.ssoSignIn=Single Sign-on vasitəsilə daxil olun
login.oauth2AutoCreateDisabled=OAUTH2 Auto-Create İstifadəçisi Deaktivləşdirilmişdir
login.oauth2AdminBlockedUser=Qeydiyyatdan keçməmiş istifadəçilərin qeydiyyatı və daxil olması hal-hazırda bloklanmışdır. Zəhmət olmasa, administratorla əlaqə saxlayın.
login.oAuth2AutoCreateDisabled=OAUTH2 Auto-Create İstifadəçisi Deaktivləşdirilmişdir
login.oAuth2AdminBlockedUser=Qeydiyyatdan keçməmiş istifadəçilərin qeydiyyatı və daxil olması hal-hazırda bloklanmışdır. Zəhmət olmasa, administratorla əlaqə saxlayın.
login.oauth2RequestNotFound=Təsdiqlənmə sorğusu tapılmadı
login.oauth2InvalidUserInfoResponse=Yanlış İstifadəçi Məlumatı Cavabı
login.oauth2invalidRequest=Etibarsız Sorğu

View File

@ -572,8 +572,8 @@ login.invalid=Невалидно потребителско име или пар
login.locked=Вашият акаунт е заключен.
login.signinTitle=Моля впишете се
login.ssoSignIn=Влизане чрез еднократно влизане
login.oauth2AutoCreateDisabled=OAUTH2 Автоматично създаване на потребител е деактивирано
login.oauth2AdminBlockedUser=Регистрацията или влизането на нерегистрирани потребители в момента е блокирано. Моля, свържете се с администратора.
login.oAuth2AutoCreateDisabled=OAUTH2 Автоматично създаване на потребител е деактивирано
login.oAuth2AdminBlockedUser=Регистрацията или влизането на нерегистрирани потребители в момента е блокирано. Моля, свържете се с администратора.
login.oauth2RequestNotFound=Заявката за оторизация не е намерена
login.oauth2InvalidUserInfoResponse=Невалидна информация за потребителя
login.oauth2invalidRequest=Невалидна заявка

View File

@ -572,8 +572,8 @@ login.invalid=Nom d'usuari/contrasenya no vàlid
login.locked=Compte bloquejat
login.signinTitle=Autenticat
login.ssoSignIn=Inicia sessió mitjançant inici de sessió únic
login.oauth2AutoCreateDisabled=La creació automàtica d'usuaris OAUTH2 està desactivada
login.oauth2AdminBlockedUser=El registre o inici de sessió d'usuaris no registrats està actualment bloquejat. Si us plau, contacta amb l'administrador.
login.oAuth2AutoCreateDisabled=La creació automàtica d'usuaris OAUTH2 està desactivada
login.oAuth2AdminBlockedUser=El registre o inici de sessió d'usuaris no registrats està actualment bloquejat. Si us plau, contacta amb l'administrador.
login.oauth2RequestNotFound=Sol·licitud d'autorització no trobada
login.oauth2InvalidUserInfoResponse=Resposta d'informació d'usuari no vàlida
login.oauth2invalidRequest=Sol·licitud no vàlida

View File

@ -572,8 +572,8 @@ login.invalid=Neplatné uživatelské jméno nebo heslo.
login.locked=Váš účet byl uzamčen.
login.signinTitle=Prosím přihlaste se
login.ssoSignIn=Přihlásit se přes Single Sign-on
login.oauth2AutoCreateDisabled=Automatické vytváření OAUTH2 uživatelů je zakázáno
login.oauth2AdminBlockedUser=Registrace nebo přihlášení neregistrovaných uživatelů je momentálně blokováno. Kontaktujte prosím správce.
login.oAuth2AutoCreateDisabled=Automatické vytváření OAUTH2 uživatelů je zakázáno
login.oAuth2AdminBlockedUser=Registrace nebo přihlášení neregistrovaných uživatelů je momentálně blokováno. Kontaktujte prosím správce.
login.oauth2RequestNotFound=Požadavek na autorizaci nebyl nalezen
login.oauth2InvalidUserInfoResponse=Neplatná odpověď s informacemi o uživateli
login.oauth2invalidRequest=Neplatný požadavek

View File

@ -572,8 +572,8 @@ login.invalid=Ugyldigt brugernavn eller adgangskode.
login.locked=Din konto er blevet låst.
login.signinTitle=Log venligst ind
login.ssoSignIn=Log ind via Single Sign-on
login.oauth2AutoCreateDisabled=OAUTH2 Auto-Opret Bruger Deaktiveret
login.oauth2AdminBlockedUser=Registrering eller login af ikke-registrerede brugere er i øjeblikket blokeret. Kontakt venligst administratoren.
login.oAuth2AutoCreateDisabled=OAUTH2 Auto-Opret Bruger Deaktiveret
login.oAuth2AdminBlockedUser=Registrering eller login af ikke-registrerede brugere er i øjeblikket blokeret. Kontakt venligst administratoren.
login.oauth2RequestNotFound=Autorisationsanmodning ikke fundet
login.oauth2InvalidUserInfoResponse=Ugyldigt Brugerinfo Svar
login.oauth2invalidRequest=Ugyldig Anmodning

View File

@ -572,8 +572,8 @@ login.invalid=Benutzername oder Passwort ungültig.
login.locked=Ihr Konto wurde gesperrt.
login.signinTitle=Bitte melden Sie sich an.
login.ssoSignIn=Anmeldung per Single Sign-On
login.oauth2AutoCreateDisabled=OAUTH2 Benutzer automatisch erstellen deaktiviert
login.oauth2AdminBlockedUser=Die Registrierung bzw. das anmelden von nicht registrierten Benutzern ist derzeit gesperrt. Bitte wenden Sie sich an den Administrator.
login.oAuth2AutoCreateDisabled=OAUTH2 Benutzer automatisch erstellen deaktiviert
login.oAuth2AdminBlockedUser=Die Registrierung bzw. das anmelden von nicht registrierten Benutzern ist derzeit gesperrt. Bitte wenden Sie sich an den Administrator.
login.oauth2RequestNotFound=Autorisierungsanfrage nicht gefunden
login.oauth2InvalidUserInfoResponse=Ungültige Benutzerinformationsantwort
login.oauth2invalidRequest=ungültige Anfrage

View File

@ -572,8 +572,8 @@ login.invalid=Μη έγκυρο όνομα χρήστη ή κωδικός.
login.locked=Ο λογαριασμός σας έχει κλειδωθεί.
login.signinTitle=Παρακαλώ συνδεθείτε
login.ssoSignIn=Σύνδεση μέσω Single Sign-on
login.oauth2AutoCreateDisabled=Η αυτόματη δημιουργία χρήστη OAUTH2 είναι απενεργοποιημένη
login.oauth2AdminBlockedUser=Η εγγραφή ή σύνδεση μη εγγεγραμμένων χρηστών είναι προς το παρόν αποκλεισμένη. Παρακαλώ επικοινωνήστε με τον διαχειριστή.
login.oAuth2AutoCreateDisabled=Η αυτόματη δημιουργία χρήστη OAUTH2 είναι απενεργοποιημένη
login.oAuth2AdminBlockedUser=Η εγγραφή ή σύνδεση μη εγγεγραμμένων χρηστών είναι προς το παρόν αποκλεισμένη. Παρακαλώ επικοινωνήστε με τον διαχειριστή.
login.oauth2RequestNotFound=Το αίτημα εξουσιοδότησης δεν βρέθηκε
login.oauth2InvalidUserInfoResponse=Μη έγκυρη απόκριση πληροφοριών χρήστη
login.oauth2invalidRequest=Μη έγκυρο αίτημα

View File

@ -572,8 +572,8 @@ login.invalid=Invalid username or password.
login.locked=Your account has been locked.
login.signinTitle=Please sign in
login.ssoSignIn=Login via Single Sign-on
login.oauth2AutoCreateDisabled=OAUTH2 Auto-Create User Disabled
login.oauth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator.
login.oAuth2AutoCreateDisabled=OAUTH2 Auto-Create User Disabled
login.oAuth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator.
login.oauth2RequestNotFound=Authorization request not found
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
login.oauth2invalidRequest=Invalid Request

View File

@ -572,8 +572,8 @@ login.invalid=Invalid username or password.
login.locked=Your account has been locked.
login.signinTitle=Please sign in
login.ssoSignIn=Login via Single Sign-on
login.oauth2AutoCreateDisabled=OAUTH2 Auto-Create User Disabled
login.oauth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator.
login.oAuth2AutoCreateDisabled=OAUTH2 Auto-Create User Disabled
login.oAuth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator.
login.oauth2RequestNotFound=Authorization request not found
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
login.oauth2invalidRequest=Invalid Request

View File

@ -572,8 +572,8 @@ login.invalid=Nombre de usuario o contraseña erróneos.
login.locked=Su cuenta se ha bloqueado.
login.signinTitle=Por favor, inicie sesión
login.ssoSignIn=Iniciar sesión a través del inicio de sesión único
login.oauth2AutoCreateDisabled=Usuario de creación automática de OAUTH2 DESACTIVADO
login.oauth2AdminBlockedUser=El registro o inicio de sesión de usuarios no registrados está actualmente bloqueado. Por favor, contáctese con el administrador.
login.oAuth2AutoCreateDisabled=Usuario de creación automática de OAUTH2 DESACTIVADO
login.oAuth2AdminBlockedUser=El registro o inicio de sesión de usuarios no registrados está actualmente bloqueado. Por favor, contáctese con el administrador.
login.oauth2RequestNotFound=Solicitud de autorización no encontrada
login.oauth2InvalidUserInfoResponse=Respuesta de información de usuario no válida
login.oauth2invalidRequest=Solicitud no válida

View File

@ -572,8 +572,8 @@ login.invalid=Okerreko erabiltzaile izena edo pasahitza.
login.locked=Zure kontua blokeatu egin da.
login.signinTitle=Mesedez, hasi saioa
login.ssoSignIn=Hasi saioa Saioa hasteko modu bakarraren bidez
login.oauth2AutoCreateDisabled=OAUTH2 Sortu automatikoki erabiltzailea desgaituta dago
login.oauth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator.
login.oAuth2AutoCreateDisabled=OAUTH2 Sortu automatikoki erabiltzailea desgaituta dago
login.oAuth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator.
login.oauth2RequestNotFound=Authorization request not found
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
login.oauth2invalidRequest=Invalid Request

View File

@ -572,8 +572,8 @@ login.invalid=نام کاربری یا رمز عبور اشتباه است.
login.locked=حساب شما قفل شده است.
login.signinTitle=لطفاً وارد شوید
login.ssoSignIn=ورود از طریق Single Sign-on
login.oauth2AutoCreateDisabled=ایجاد خودکار کاربر با OAUTH2 غیرفعال است
login.oauth2AdminBlockedUser=ثبت‌نام یا ورود کاربران ثبت‌نشده در حال حاضر مسدود است. لطفاً با مدیر تماس بگیرید.
login.oAuth2AutoCreateDisabled=ایجاد خودکار کاربر با OAUTH2 غیرفعال است
login.oAuth2AdminBlockedUser=ثبت‌نام یا ورود کاربران ثبت‌نشده در حال حاضر مسدود است. لطفاً با مدیر تماس بگیرید.
login.oauth2RequestNotFound=درخواست احراز هویت پیدا نشد
login.oauth2InvalidUserInfoResponse=پاسخ اطلاعات کاربری نامعتبر است
login.oauth2invalidRequest=درخواست نامعتبر

View File

@ -572,8 +572,8 @@ login.invalid=Nom d'utilisateur ou mot de passe invalide.
login.locked=Votre compte a été verrouillé.
login.signinTitle=Veuillez vous connecter
login.ssoSignIn=Se connecter via l'authentification unique
login.oauth2AutoCreateDisabled=OAUTH2 Création automatique d'utilisateur désactivée
login.oauth2AdminBlockedUser=La création ou l'authentification d'utilisateurs non enregistrés est actuellement bloquée. Veuillez contacter l'administrateur.
login.oAuth2AutoCreateDisabled=OAUTH2 Création automatique d'utilisateur désactivée
login.oAuth2AdminBlockedUser=La création ou l'authentification d'utilisateurs non enregistrés est actuellement bloquée. Veuillez contacter l'administrateur.
login.oauth2RequestNotFound=Demande d'autorisation introuvable
login.oauth2InvalidUserInfoResponse=Réponse contenant les informations de l'utilisateur est invalide
login.oauth2invalidRequest=Requête invalide

View File

@ -572,8 +572,8 @@ login.invalid=अमान्य उपयोगकर्ता नाम या
login.locked=आपका खाता लॉक कर दिया गया है।
login.signinTitle=कृपया साइन इन करें
login.ssoSignIn=सिंगल साइन-ऑन के माध्यम से लॉगिन करें
login.oauth2AutoCreateDisabled=OAUTH2 स्वतः उपयोगकर्ता निर्माण अक्षम है
login.oauth2AdminBlockedUser=गैर-पंजीकृत उपयोगकर्ताओं का पंजीकरण या लॉगिन वर्तमान में अवरुद्ध है। कृपया व्यवस्थापक से संपर्क करें।
login.oAuth2AutoCreateDisabled=OAUTH2 स्वतः उपयोगकर्ता निर्माण अक्षम है
login.oAuth2AdminBlockedUser=गैर-पंजीकृत उपयोगकर्ताओं का पंजीकरण या लॉगिन वर्तमान में अवरुद्ध है। कृपया व्यवस्थापक से संपर्क करें।
login.oauth2RequestNotFound=प्राधिकरण अनुरोध नहीं मिला
login.oauth2InvalidUserInfoResponse=अमान्य उपयोगकर्ता जानकारी प्रतिक्रिया
login.oauth2invalidRequest=अमान्य अनुरोध

View File

@ -572,8 +572,8 @@ login.invalid=Neispravno korisničko ime ili zaporka.
login.locked=Vaš račun je zaključan.
login.signinTitle=Molimo vas da se prijavite
login.ssoSignIn=Prijavite se putem jedinstvene prijave
login.oauth2AutoCreateDisabled=OAUTH2 automatsko kreiranje korisnika je onemogućeno
login.oauth2AdminBlockedUser=Registracija ili prijava nekadreguiranih korisnika trenutno su blokirane. Molimo Vas da kontaktirate administratora.
login.oAuth2AutoCreateDisabled=OAUTH2 automatsko kreiranje korisnika je onemogućeno
login.oAuth2AdminBlockedUser=Registracija ili prijava nekadreguiranih korisnika trenutno su blokirane. Molimo Vas da kontaktirate administratora.
login.oauth2RequestNotFound=Zahtjev za autorizaciju nije pronađen
login.oauth2InvalidUserInfoResponse=Nevažeće informacije o korisniku
login.oauth2invalidRequest=Neispravan zahtjev

View File

@ -572,8 +572,8 @@ login.invalid=Érvénytelen felhasználónév vagy jelszó.
login.locked=A fiókja zárolva van.
login.signinTitle=Kérjük, jelentkezzen be
login.ssoSignIn=Bejelentkezés egyszeri bejelentkezéssel
login.oauth2AutoCreateDisabled=OAuth2 automatikus felhasználólétrehozás letiltva
login.oauth2AdminBlockedUser=A nem regisztrált felhasználók regisztrációja vagy bejelentkezése jelenleg le van tiltva. Kérjük, forduljon a rendszergazdához.
login.oAuth2AutoCreateDisabled=OAuth2 automatikus felhasználólétrehozás letiltva
login.oAuth2AdminBlockedUser=A nem regisztrált felhasználók regisztrációja vagy bejelentkezése jelenleg le van tiltva. Kérjük, forduljon a rendszergazdához.
login.oauth2RequestNotFound=A hitelesítési kérés nem található
login.oauth2InvalidUserInfoResponse=Érvénytelen felhasználói információ válasz
login.oauth2invalidRequest=Érvénytelen kérés

View File

@ -572,8 +572,8 @@ login.invalid=Nama pengguna atau kata sandi tidak valid.
login.locked=Akun Anda telah dikunci.
login.signinTitle=Silakan masuk
login.ssoSignIn=Masuk melalui Single Sign - on
login.oauth2AutoCreateDisabled=OAUTH2 Buat Otomatis Pengguna Dinonaktifkan
login.oauth2AdminBlockedUser=Registrasi atau login pengguna yang tidak terdaftar saat ini diblokir. Silakan hubungi administrator.
login.oAuth2AutoCreateDisabled=OAUTH2 Buat Otomatis Pengguna Dinonaktifkan
login.oAuth2AdminBlockedUser=Registrasi atau login pengguna yang tidak terdaftar saat ini diblokir. Silakan hubungi administrator.
login.oauth2RequestNotFound=Permintaan otorisasi tidak ditemukan
login.oauth2InvalidUserInfoResponse=Respons Info Pengguna Tidak Valid
login.oauth2invalidRequest=Permintaan Tidak Valid

View File

@ -572,8 +572,8 @@ login.invalid=Nome utente o password errati.
login.locked=Il tuo account è stato bloccato.
login.signinTitle=Per favore accedi
login.ssoSignIn=Accedi tramite Single Sign-on
login.oauth2AutoCreateDisabled=Creazione automatica utente OAUTH2 DISABILITATA
login.oauth2AdminBlockedUser=La registrazione o l'accesso degli utenti non registrati è attualmente bloccata. Si prega di contattare l'amministratore.
login.oAuth2AutoCreateDisabled=Creazione automatica utente OAUTH2 DISABILITATA
login.oAuth2AdminBlockedUser=La registrazione o l'accesso degli utenti non registrati è attualmente bloccata. Si prega di contattare l'amministratore.
login.oauth2RequestNotFound=Richiesta di autorizzazione non trovata
login.oauth2InvalidUserInfoResponse=Risposta relativa alle informazioni utente non valida
login.oauth2invalidRequest=Richiesta non valida

View File

@ -572,8 +572,8 @@ login.invalid=ユーザー名かパスワードが無効です。
login.locked=あなたのアカウントはロックされています。
login.signinTitle=サインインしてください
login.ssoSignIn=シングルサインオンでログイン
login.oauth2AutoCreateDisabled=OAuth 2自動作成ユーザーが無効
login.oauth2AdminBlockedUser=現在、未登録ユーザーの登録またはログインはブロックされています。管理者にお問い合わせください。
login.oAuth2AutoCreateDisabled=OAuth 2自動作成ユーザーが無効
login.oAuth2AdminBlockedUser=現在、未登録ユーザーの登録またはログインはブロックされています。管理者にお問い合わせください。
login.oauth2RequestNotFound=認証リクエストが見つかりません
login.oauth2InvalidUserInfoResponse=無効なユーザー情報の応答
login.oauth2invalidRequest=無効なリクエスト

View File

@ -572,8 +572,8 @@ login.invalid=사용자 이름 또는 비밀번호가 잘못되었습니다.
login.locked=계정이 잠겼습니다.
login.signinTitle=로그인해 주세요
login.ssoSignIn=단일 로그인으로 로그인
login.oauth2AutoCreateDisabled=OAuth2 사용자 자동 생성이 비활성화되었습니다
login.oauth2AdminBlockedUser=현재 미등록 사용자의 등록 또는 로그인이 차단되어 있습니다. 관리자에게 문의하세요.
login.oAuth2AutoCreateDisabled=OAuth2 사용자 자동 생성이 비활성화되었습니다
login.oAuth2AdminBlockedUser=현재 미등록 사용자의 등록 또는 로그인이 차단되어 있습니다. 관리자에게 문의하세요.
login.oauth2RequestNotFound=인증 요청을 찾을 수 없습니다
login.oauth2InvalidUserInfoResponse=잘못된 사용자 정보 응답
login.oauth2invalidRequest=잘못된 요청

View File

@ -572,8 +572,8 @@ login.invalid=Ongeldige gebruikersnaam of wachtwoord.
login.locked=Je account is geblokkeerd.
login.signinTitle=Gelieve in te loggen
login.ssoSignIn=Inloggen via Single Sign-on
login.oauth2AutoCreateDisabled=OAUTH2 Automatisch aanmaken gebruiker uitgeschakeld
login.oauth2AdminBlockedUser=Registratie of inloggen van niet-registreerde gebruikers is helaas momenteel geblokkeerd. Neem contact op met de beheerder.
login.oAuth2AutoCreateDisabled=OAUTH2 Automatisch aanmaken gebruiker uitgeschakeld
login.oAuth2AdminBlockedUser=Registratie of inloggen van niet-registreerde gebruikers is helaas momenteel geblokkeerd. Neem contact op met de beheerder.
login.oauth2RequestNotFound=Autorisatieverzoek niet gevonden
login.oauth2InvalidUserInfoResponse=Ongeldige reactie op gebruikersinfo
login.oauth2invalidRequest=Ongeldig verzoek

View File

@ -572,8 +572,8 @@ login.invalid=Ugyldig brukernavn eller passord.
login.locked=Kontoen din har blitt låst.
login.signinTitle=Vennligst logg inn
login.ssoSignIn=Logg inn via Enkel Pålogging
login.oauth2AutoCreateDisabled=OAUTH2 Auto-Opretting av bruker deaktivert
login.oauth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator.
login.oAuth2AutoCreateDisabled=OAUTH2 Auto-Opretting av bruker deaktivert
login.oAuth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator.
login.oauth2RequestNotFound=Autentiseringsforespørsel ikke funnet
login.oauth2InvalidUserInfoResponse=Ugyldig brukerinforespons
login.oauth2invalidRequest=Ugyldig forespørsel

View File

@ -572,8 +572,8 @@ login.invalid=Nieprawidłowe dane logowania
login.locked=Konto jest zablokowane
login.signinTitle=Zaloguj się
login.ssoSignIn=Zaloguj się za pomocą logowania jednokrotnego
login.oauth2AutoCreateDisabled=Wyłączono automatyczne tworzenie użytkownika OAUTH2
login.oauth2AdminBlockedUser=Rejestracja lub logowanie niezarejestrowanych użytkowników jest obecnie zablokowane. Prosimy o kontakt z administratorem.
login.oAuth2AutoCreateDisabled=Wyłączono automatyczne tworzenie użytkownika OAUTH2
login.oAuth2AdminBlockedUser=Rejestracja lub logowanie niezarejestrowanych użytkowników jest obecnie zablokowane. Prosimy o kontakt z administratorem.
login.oauth2RequestNotFound=Błąd logowania OAuth2
login.oauth2InvalidUserInfoResponse=Niewłaściwe dane logowania
login.oauth2invalidRequest=Nieprawidłowe żądanie

View File

@ -572,8 +572,8 @@ login.invalid=Usuário ou senha inválidos.
login.locked=Sua conta foi bloqueada.
login.signinTitle=Por favor, inicie a sessão
login.ssoSignIn=Iniciar sessão através de login único (SSO)
login.oauth2AutoCreateDisabled=Auto-Criar Usuário OAUTH2 Desativado
login.oauth2AdminBlockedUser=O registro ou login de usuários não registrados está atualmente bloqueado. Entre em contato com o administrador.
login.oAuth2AutoCreateDisabled=Auto-Criar Usuário OAUTH2 Desativado
login.oAuth2AdminBlockedUser=O registro ou login de usuários não registrados está atualmente bloqueado. Entre em contato com o administrador.
login.oauth2RequestNotFound=Solicitação de autorização não encontrada
login.oauth2InvalidUserInfoResponse=Resposta de informação de usuário inválida
login.oauth2invalidRequest=Requisição Inválida

View File

@ -572,8 +572,8 @@ login.invalid=Nome de utilizador ou palavra-passe inválidos.
login.locked=A sua conta foi bloqueada.
login.signinTitle=Por favor inicie sessão
login.ssoSignIn=Login via Single Sign-on
login.oauth2AutoCreateDisabled=Criação Automática de Utilizador OAUTH2 Desativada
login.oauth2AdminBlockedUser=O registo ou login de utilizadores não registados está atualmente bloqueado. Por favor contacte o administrador.
login.oAuth2AutoCreateDisabled=Criação Automática de Utilizador OAUTH2 Desativada
login.oAuth2AdminBlockedUser=O registo ou login de utilizadores não registados está atualmente bloqueado. Por favor contacte o administrador.
login.oauth2RequestNotFound=Pedido de autorização não encontrado
login.oauth2InvalidUserInfoResponse=Resposta de Informação de Utilizador Inválida
login.oauth2invalidRequest=Pedido Inválido

View File

@ -572,8 +572,8 @@ login.invalid=Nume de utilizator sau parolă invalidă.
login.locked=Contul tău a fost blocat.
login.signinTitle=Te rugăm să te autentifici
login.ssoSignIn=Conectare prin conectare unică
login.oauth2AutoCreateDisabled=OAUTH2 Creare automată utilizator dezactivată
login.oauth2AdminBlockedUser=Înregistrarea sau conectarea utilizatorilor neînregistrați este în prezent blocată. Te rugăm să contactezi administratorul.
login.oAuth2AutoCreateDisabled=OAUTH2 Creare automată utilizator dezactivată
login.oAuth2AdminBlockedUser=Înregistrarea sau conectarea utilizatorilor neînregistrați este în prezent blocată. Te rugăm să contactezi administratorul.
login.oauth2RequestNotFound=Cererea de autorizare nu a fost găsită
login.oauth2InvalidUserInfoResponse=Răspuns Invalid la Informațiile Utilizatorului
login.oauth2invalidRequest=Cerere Invalidă

View File

@ -572,8 +572,8 @@ login.invalid=Неверное имя пользователя или парол
login.locked=Ваша учетная запись заблокирована.
login.signinTitle=Пожалуйста, войдите
login.ssoSignIn=Вход через единый вход
login.oauth2AutoCreateDisabled=Автоматическое создание пользователей OAuth2 отключено
login.oauth2AdminBlockedUser=Регистрация или вход незарегистрированных пользователей в настоящее время заблокированы. Обратитесь к администратору.
login.oAuth2AutoCreateDisabled=Автоматическое создание пользователей OAuth2 отключено
login.oAuth2AdminBlockedUser=Регистрация или вход незарегистрированных пользователей в настоящее время заблокированы. Обратитесь к администратору.
login.oauth2RequestNotFound=Запрос авторизации не найден
login.oauth2InvalidUserInfoResponse=Недействительный ответ с информацией о пользователе
login.oauth2invalidRequest=Недействительный запрос

View File

@ -572,8 +572,8 @@ login.invalid=Neplatné používateľské meno alebo heslo.
login.locked=Váš účet bol uzamknutý.
login.signinTitle=Prosím, prihláste sa
login.ssoSignIn=Prihlásiť sa cez Single Sign-on
login.oauth2AutoCreateDisabled=Vytváranie používateľa cez OAUTH2 je zakázané
login.oauth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator.
login.oAuth2AutoCreateDisabled=Vytváranie používateľa cez OAUTH2 je zakázané
login.oAuth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator.
login.oauth2RequestNotFound=Authorization request not found
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
login.oauth2invalidRequest=Invalid Request

View File

@ -572,8 +572,8 @@ login.invalid=Neveljavno uporabniško ime ali geslo.
login.locked=Vaš račun je bil zaklenjen.
login.signinTitle=Prosim prijavite se
login.ssoSignIn=Prijava prek enotne prijave
login.oauth2AutoCreateDisabled=OAUTH2 Samodejno ustvarjanje uporabnika onemogočeno
login.oauth2AdminBlockedUser=Registracija ali prijava neregistriranih uporabnikov je trenutno blokirana. Prosimo kontaktirajte skrbnika.
login.oAuth2AutoCreateDisabled=OAUTH2 Samodejno ustvarjanje uporabnika onemogočeno
login.oAuth2AdminBlockedUser=Registracija ali prijava neregistriranih uporabnikov je trenutno blokirana. Prosimo kontaktirajte skrbnika.
login.oauth2RequestNotFound=Zahteva za avtorizacijo ni bila najdena
login.oauth2InvalidUserInfoResponse=Neveljaven odgovor z informacijami o uporabniku
login.oauth2invalidRequest=Neveljavna zahteva

View File

@ -572,8 +572,8 @@ login.invalid=Neispravno korisničko ime ili lozinka.
login.locked=Vaš nalog je zaključan.
login.signinTitle=Molimo vas da se prijavite
login.ssoSignIn=Prijavite se putem jedinstvene prijave
login.oauth2AutoCreateDisabled=OAUTH2 automatsko kreiranje korisnika je onemogućeno
login.oauth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator.
login.oAuth2AutoCreateDisabled=OAUTH2 automatsko kreiranje korisnika je onemogućeno
login.oAuth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator.
login.oauth2RequestNotFound=Authorization request not found
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
login.oauth2invalidRequest=Invalid Request

View File

@ -572,8 +572,8 @@ login.invalid=Ogiltigt användarnamn eller lösenord.
login.locked=Ditt konto har låsts.
login.signinTitle=Vänligen logga in
login.ssoSignIn=Logga in via enkel inloggning
login.oauth2AutoCreateDisabled=OAUTH2 Auto-skapa användare inaktiverad
login.oauth2AdminBlockedUser=Registrering eller inloggning av icke-registrerade användare är för närvarande blockerad. Kontakta administratören.
login.oAuth2AutoCreateDisabled=OAUTH2 Auto-skapa användare inaktiverad
login.oAuth2AdminBlockedUser=Registrering eller inloggning av icke-registrerade användare är för närvarande blockerad. Kontakta administratören.
login.oauth2RequestNotFound=Auktoriseringsbegäran hittades inte
login.oauth2InvalidUserInfoResponse=Ogiltigt svar på användarinformation
login.oauth2invalidRequest=Ogiltig begäran

View File

@ -572,8 +572,8 @@ login.invalid=ชื่อผู้ใช้หรือรหัสผ่าน
login.locked=บัญชีของคุณถูกล็อค
login.signinTitle=กรุณาลงชื่อเข้าใช้
login.ssoSignIn=เข้าสู่ระบบด้วย Single Sign-on
login.oauth2AutoCreateDisabled=การสร้างผู้ใช้ OAuth2 อัตโนมัติถูกปิดใช้งาน
login.oauth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator.
login.oAuth2AutoCreateDisabled=การสร้างผู้ใช้ OAuth2 อัตโนมัติถูกปิดใช้งาน
login.oAuth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator.
login.oauth2RequestNotFound=ไม่พบคำขอการอนุญาต
login.oauth2InvalidUserInfoResponse=การตอบกลับข้อมูลผู้ใช้ไม่ถูกต้อง
login.oauth2invalidRequest=คำขอไม่ถูกต้อง

View File

@ -572,8 +572,8 @@ login.invalid=Geçersiz kullanıcı adı veya şifre.
login.locked=Hesabınız kilitlendi.
login.signinTitle=Lütfen giriş yapınız.
login.ssoSignIn=Tek Oturum Açma ile Giriş Yap
login.oauth2AutoCreateDisabled=OAUTH2 Otomatik Oluşturma Kullanıcı Devre Dışı Bırakıldı
login.oauth2AdminBlockedUser=Kayıtlı olmayan kullanıcıların kayıt veya giriş yapması şu anda engellenmiştir. Lütfen yöneticiyle iletişime geçin.
login.oAuth2AutoCreateDisabled=OAUTH2 Otomatik Oluşturma Kullanıcı Devre Dışı Bırakıldı
login.oAuth2AdminBlockedUser=Kayıtlı olmayan kullanıcıların kayıt veya giriş yapması şu anda engellenmiştir. Lütfen yöneticiyle iletişime geçin.
login.oauth2RequestNotFound=Yetkilendirme isteği bulunamadı
login.oauth2InvalidUserInfoResponse=Geçersiz Kullanıcı Bilgisi Yanıtı
login.oauth2invalidRequest=Geçersiz İstek

View File

@ -572,8 +572,8 @@ login.invalid=Недійсне ім'я користувача або парол
login.locked=Ваш обліковий запис заблоковано.
login.signinTitle=Будь ласка, увійдіть
login.ssoSignIn=Увійти через єдиний вхід
login.oauth2AutoCreateDisabled=Автоматичне створення користувача OAUTH2 ВИМКНЕНО
login.oauth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator.
login.oAuth2AutoCreateDisabled=Автоматичне створення користувача OAUTH2 ВИМКНЕНО
login.oAuth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator.
login.oauth2RequestNotFound=Запит на авторизація не знайдено
login.oauth2InvalidUserInfoResponse=Недійсна відповідь з інформацією користувача
login.oauth2invalidRequest=Недійсний запит

View File

@ -572,8 +572,8 @@ login.invalid=Tên đăng nhập hoặc mật khẩu không hợp lệ.
login.locked=Tài khoản của bạn đã bị khóa.
login.signinTitle=Vui lòng đăng nhập
login.ssoSignIn=Đăng nhập qua Single Sign-on
login.oauth2AutoCreateDisabled=Tự động tạo người dùng OAUTH2 bị vô hiệu hóa
login.oauth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator.
login.oAuth2AutoCreateDisabled=Tự động tạo người dùng OAUTH2 bị vô hiệu hóa
login.oAuth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator.
login.oauth2RequestNotFound=Không tìm thấy yêu cầu ủy quyền
login.oauth2InvalidUserInfoResponse=Phản hồi thông tin người dùng không hợp lệ
login.oauth2invalidRequest=Yêu cầu không hợp lệ

View File

@ -572,8 +572,8 @@ login.invalid=སྤྱོད་མིང་ངམ་གསང་ཚིག་ན
login.locked=ཁྱེད་ཀྱི་ཐོ་མཛོད་ཟྭ་རྒྱག་བརྒྱབ་ཟིན།
login.signinTitle=ནང་འཛུལ་གནང་རོགས།
login.ssoSignIn=གཅིག་གྱུར་ནང་འཛུལ་བརྒྱུད་ནས་ནང་འཛུལ།
login.oauth2AutoCreateDisabled=OAUTH2 རང་འགུལ་སྤྱོད་མཁན་གསར་བཟོ་བཀག་སྡོམ་བྱས་ཟིན།
login.oauth2AdminBlockedUser=ད་ལྟ་ཐོ་འགོད་མ་བྱས་པའི་སྤྱོད་མཁན་གྱི་ཐོ་འགོད་དང་ནང་འཛུལ་བཀག་སྡོམ་བྱས་ཡོད། དོ་དམ་པར་འབྲེལ་བ་གནང་རོགས།
login.oAuth2AutoCreateDisabled=OAUTH2 རང་འགུལ་སྤྱོད་མཁན་གསར་བཟོ་བཀག་སྡོམ་བྱས་ཟིན།
login.oAuth2AdminBlockedUser=ད་ལྟ་ཐོ་འགོད་མ་བྱས་པའི་སྤྱོད་མཁན་གྱི་ཐོ་འགོད་དང་ནང་འཛུལ་བཀག་སྡོམ་བྱས་ཡོད། དོ་དམ་པར་འབྲེལ་བ་གནང་རོགས།
login.oauth2RequestNotFound=དབང་སྤྲོད་རེ་ཞུ་རྙེད་མ་བྱུང་།
login.oauth2InvalidUserInfoResponse=སྤྱོད་མཁན་གྱི་གནས་ཚུལ་ལན་འདེབས་ནོར་འཁྲུལ།
login.oauth2invalidRequest=རེ་ཞུ་ནོར་འཁྲུལ།

View File

@ -572,8 +572,8 @@ login.invalid=用户名或密码无效。
login.locked=您的账户已被锁定。
login.signinTitle=请登录
login.ssoSignIn=通过单点登录登录
login.oauth2AutoCreateDisabled=OAuth2 自动创建用户已禁用
login.oauth2AdminBlockedUser=目前已阻止未注册用户的注册或登录。请联系管理员。
login.oAuth2AutoCreateDisabled=OAuth2 自动创建用户已禁用
login.oAuth2AdminBlockedUser=目前已阻止未注册用户的注册或登录。请联系管理员。
login.oauth2RequestNotFound=找不到验证请求
login.oauth2InvalidUserInfoResponse=无效的用户信息响应
login.oauth2invalidRequest=无效请求

View File

@ -572,8 +572,8 @@ login.invalid=使用者名稱或密碼無效。
login.locked=您的帳號已被鎖定。
login.signinTitle=請登入
login.ssoSignIn=透過 SSO 單一登入
login.oauth2AutoCreateDisabled=OAuth 2.0 自動建立使用者功能已停用
login.oauth2AdminBlockedUser=目前不允許未註冊的使用者註冊或登入。請聯絡系統管理員。
login.oAuth2AutoCreateDisabled=OAuth 2.0 自動建立使用者功能已停用
login.oAuth2AdminBlockedUser=目前不允許未註冊的使用者註冊或登入。請聯絡系統管理員。
login.oauth2RequestNotFound=找不到驗證請求
login.oauth2InvalidUserInfoResponse=使用者資訊回應無效
login.oauth2invalidRequest=請求無效

View File

@ -28,37 +28,38 @@ security:
clientId: '' # client ID for Keycloak OAuth2
clientSecret: '' # client secret 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 | name | given_name | family_name | preferred_name]
google:
clientId: '' # client ID for Google OAuth2
clientSecret: '' # client secret for Google OAuth2
scopes: https://www.googleapis.com/auth/userinfo.email, https://www.googleapis.com/auth/userinfo.profile # scopes for Google OAuth2
useAsUsername: email # field to use as the username for Google OAuth2
scopes: email, profile # scopes for Google OAuth2
useAsUsername: email # field to use as the username for Google OAuth2. Available options are: [email | name | given_name | family_name]
github:
clientId: '' # client ID for GitHub OAuth2
clientSecret: '' # client secret for GitHub OAuth2
scopes: read:user # scope for GitHub OAuth2
useAsUsername: login # field to use as the username for GitHub OAuth2
issuer: '' # set to any provider that supports OpenID Connect Discovery (/.well-known/openid-configuration) endpoint
clientId: '' # client ID from your provider
clientSecret: '' # client secret from your provider
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
clientId: '' # client ID from your Provider
clientSecret: '' # client secret from your Provider
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
useAsUsername: email # 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
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:
enabled: false # Only enabled for paid enterprise clients (enterpriseEdition.enabled must be true)
provider: '' # The name of your Provider
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
registrationId: stirling
idpMetadataUri: https://dev-XXXXXXXX.okta.com/app/externalKey/sso/saml/metadata
idpSingleLogoutUrl: https://dev-XXXXXXXX.okta.com/app/dev-XXXXXXXX_stirlingpdf_1/externalKey/slo/saml
idpSingleLoginUrl: https://dev-XXXXXXXX.okta.com/app/dev-XXXXXXXX_stirlingpdf_1/externalKey/sso/saml
idpIssuer: http://www.okta.com/externalKey
idpCert: classpath:okta.crt
privateKey: classpath:saml-private-key.key
spCert: classpath:saml-public-cert.crt
registrationId: stirling # The name of your Service Provider (SP) app name. Should match the name in the path for your SSO & SLO URLs
idpMetadataUri: https://dev-XXXXXXXX.okta.com/app/externalKey/sso/saml/metadata # The uri for your Provider's metadata
idpSingleLoginUrl: https://dev-XXXXXXXX.okta.com/app/dev-XXXXXXXX_stirlingpdf_1/externalKey/sso/saml # The URL for initiating SSO. Provided by your Provider
idpSingleLogoutUrl: https://dev-XXXXXXXX.okta.com/app/dev-XXXXXXXX_stirlingpdf_1/externalKey/slo/saml # The URL for initiating SLO. Provided by your Provider
idpIssuer: '' # The ID of your Provider
idpCert: classpath:okta.cert # The certificate your Provider will use to authenticate your app's SAML authentication requests. Provided by your Provider
privateKey: classpath:saml-private-key.key # Your private key. Generated from your keypair
spCert: classpath:saml-public-cert.crt # Your signing certificate. Generated from your keypair
enterpriseEdition:
enabled: false # set to 'true' to enable enterprise edition

View File

@ -34,7 +34,7 @@
</th:block>
<!-- 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>
<form id="formsavechangeusername" class="bg-card mt-4 mb-4" th:action="@{'/api/v1/user/change-username'}" method="post">
<div class="mb-3">
@ -53,7 +53,7 @@
</th:block>
<!-- 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>
<form id="formsavechangepassword" class="bg-card mt-4 mb-4" th:action="@{'/api/v1/user/change-password'}" method="post">
<div class="mb-3">

View File

@ -253,11 +253,11 @@
<label for="cacheInputs" th:text="#{settings.cacheInputs.name}"></label>
</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>
</div>
<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>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" th:text="#{close}"></button>
</div>

View File

@ -42,7 +42,7 @@
const runningEE = /*[[${@runningEE}]]*/ false;
const SSOAutoLogin = /*[[${@SSOAutoLogin}]]*/ false;
const loginMethod = /*[[${loginMethod}]]*/ 'normal';
const providerList = /*[[${providerlist}]]*/ {};
const providerList = /*[[${providerList}]]*/ {};
const shouldAutoRedirect = !hasRedirectError &&
!hasLogout &&
!hasMessage &&
@ -98,14 +98,14 @@
<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>
<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>
<br>
<br>
<hr />
</div>
<div th:if="${erroroauth}" class="alert alert-danger text-center">
<div th:if="${erroroauth}" th:text="#{${erroroauth}}">OAuth2: Error Message</div>
<div th:if="${errorOAuth}" class="alert alert-danger text-center">
<div th:if="${errorOAuth}" th:text="#{${errorOAuth}}">OAuth2: Error Message</div>
</div>
<div th:if="${error}" class="alert alert-danger text-center">
@ -164,7 +164,7 @@
</button>
</div>
<div class="modal-body">
<div class="mb-3" th:each="provider : ${providerlist}">
<div class="mb-3" th:each="provider : ${providerList}">
<a th:href="@{|${provider.key}|}" th:text="${provider.value}" class="w-100 btn btn-lg btn-primary">Login Provider</a>
</div>
</div>

View File

@ -0,0 +1,262 @@
package stirling.software.SPDF.config.security;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import stirling.software.SPDF.model.ApplicationProperties;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class CustomLogoutSuccessHandlerTest {
@Mock
private ApplicationProperties applicationProperties;
@InjectMocks
private CustomLogoutSuccessHandler customLogoutSuccessHandler;
@Test
void testSuccessfulLogout() throws IOException {
HttpServletRequest request = mock(HttpServletRequest.class);
HttpServletResponse response = mock(HttpServletResponse.class);
String logoutPath = "logout=true";
when(response.isCommitted()).thenReturn(false);
when(request.getContextPath()).thenReturn("");
when(response.encodeRedirectURL(logoutPath)).thenReturn(logoutPath);
customLogoutSuccessHandler.onLogoutSuccess(request, response, null);
verify(response).sendRedirect(logoutPath);
}
@Test
void testSuccessfulLogoutViaOAuth2() throws IOException {
HttpServletRequest request = mock(HttpServletRequest.class);
HttpServletResponse response = mock(HttpServletResponse.class);
OAuth2AuthenticationToken oAuth2AuthenticationToken = mock(OAuth2AuthenticationToken.class);
ApplicationProperties.Security security = mock(ApplicationProperties.Security.class);
ApplicationProperties.Security.OAUTH2 oauth = mock(ApplicationProperties.Security.OAUTH2.class);
when(response.isCommitted()).thenReturn(false);
when(request.getParameter("oAuth2AuthenticationErrorWeb")).thenReturn(null);
when(request.getParameter("errorOAuth")).thenReturn(null);
when(request.getScheme()).thenReturn("http");
when(request.getServerName()).thenReturn("localhost");
when(request.getServerPort()).thenReturn(8080);
when(request.getContextPath()).thenReturn("");
when(applicationProperties.getSecurity()).thenReturn(security);
when(security.getOauth2()).thenReturn(oauth);
when(oAuth2AuthenticationToken.getAuthorizedClientRegistrationId()).thenReturn("test");
customLogoutSuccessHandler.onLogoutSuccess(request, response, oAuth2AuthenticationToken);
verify(response).sendRedirect("http://localhost:8080/login?logout=true");
}
@Test
void testUserIsDisabledRedirect() throws IOException {
String error = "userIsDisabled";
String url = "http://localhost:8080";
HttpServletRequest request = mock(HttpServletRequest.class);
HttpServletResponse response = mock(HttpServletResponse.class);
OAuth2AuthenticationToken authentication = mock(OAuth2AuthenticationToken.class);
ApplicationProperties.Security security = mock(ApplicationProperties.Security.class);
ApplicationProperties.Security.OAUTH2 oauth = mock(ApplicationProperties.Security.OAUTH2.class);
when(response.isCommitted()).thenReturn(false);
when(request.getParameter("oAuth2AuthenticationErrorWeb")).thenReturn(null);
when(request.getParameter("errorOAuth")).thenReturn(null);
when(request.getParameter("oAuth2AutoCreateDisabled")).thenReturn(null);
when(request.getParameter("oAuth2AdminBlockedUser")).thenReturn(null);
when(request.getParameter(error)).thenReturn("true");
when(request.getScheme()).thenReturn("http");
when(request.getServerName()).thenReturn("localhost");
when(request.getServerPort()).thenReturn(8080);
when(request.getContextPath()).thenReturn("");
when(applicationProperties.getSecurity()).thenReturn(security);
when(security.getOauth2()).thenReturn(oauth);
when(authentication.getAuthorizedClientRegistrationId()).thenReturn("test");
customLogoutSuccessHandler.onLogoutSuccess(request, response, authentication);
verify(response).sendRedirect(url + "/login?errorOAuth=" + error);
}
@Test
void testUserAlreadyExistsWebRedirect() throws IOException {
String error = "oAuth2AuthenticationErrorWeb";
String errorPath = "userAlreadyExistsWeb";
String url = "http://localhost:8080";
HttpServletRequest request = mock(HttpServletRequest.class);
HttpServletResponse response = mock(HttpServletResponse.class);
OAuth2AuthenticationToken authentication = mock(OAuth2AuthenticationToken.class);
ApplicationProperties.Security security = mock(ApplicationProperties.Security.class);
ApplicationProperties.Security.OAUTH2 oauth = mock(ApplicationProperties.Security.OAUTH2.class);
when(response.isCommitted()).thenReturn(false);
when(request.getParameter(error)).thenReturn("true");
when(request.getScheme()).thenReturn("http");
when(request.getServerName()).thenReturn("localhost");
when(request.getServerPort()).thenReturn(8080);
when(request.getContextPath()).thenReturn("");
when(applicationProperties.getSecurity()).thenReturn(security);
when(security.getOauth2()).thenReturn(oauth);
when(authentication.getAuthorizedClientRegistrationId()).thenReturn("test");
customLogoutSuccessHandler.onLogoutSuccess(request, response, authentication);
verify(response).sendRedirect(url + "/login?errorOAuth=" + errorPath);
}
@Test
void testErrorOAuthRedirect() throws IOException {
String error = "testError";
String url = "http://localhost:8080";
HttpServletRequest request = mock(HttpServletRequest.class);
HttpServletResponse response = mock(HttpServletResponse.class);
OAuth2AuthenticationToken authentication = mock(OAuth2AuthenticationToken.class);
ApplicationProperties.Security security = mock(ApplicationProperties.Security.class);
ApplicationProperties.Security.OAUTH2 oauth = mock(ApplicationProperties.Security.OAUTH2.class);
when(response.isCommitted()).thenReturn(false);
when(request.getParameter("oAuth2AuthenticationErrorWeb")).thenReturn(null);
when(request.getParameter("errorOAuth")).thenReturn("!!!" + error + "!!!");
when(request.getScheme()).thenReturn("http");
when(request.getServerName()).thenReturn("localhost");
when(request.getServerPort()).thenReturn(8080);
when(request.getContextPath()).thenReturn("");
when(applicationProperties.getSecurity()).thenReturn(security);
when(security.getOauth2()).thenReturn(oauth);
when(authentication.getAuthorizedClientRegistrationId()).thenReturn("test");
customLogoutSuccessHandler.onLogoutSuccess(request, response, authentication);
verify(response).sendRedirect(url + "/login?errorOAuth=" + error);
}
@Test
void testOAuth2AutoCreateDisabled() throws IOException {
String error = "oAuth2AutoCreateDisabled";
String url = "http://localhost:8080";
HttpServletRequest request = mock(HttpServletRequest.class);
HttpServletResponse response = mock(HttpServletResponse.class);
OAuth2AuthenticationToken authentication = mock(OAuth2AuthenticationToken.class);
ApplicationProperties.Security security = mock(ApplicationProperties.Security.class);
ApplicationProperties.Security.OAUTH2 oauth = mock(ApplicationProperties.Security.OAUTH2.class);
when(response.isCommitted()).thenReturn(false);
when(request.getParameter("oAuth2AuthenticationErrorWeb")).thenReturn(null);
when(request.getParameter("errorOAuth")).thenReturn(null);
when(request.getParameter(error)).thenReturn("true");
when(request.getContextPath()).thenReturn(url);
when(request.getScheme()).thenReturn("http");
when(request.getServerName()).thenReturn("localhost");
when(request.getServerPort()).thenReturn(8080);
when(request.getContextPath()).thenReturn("");
when(applicationProperties.getSecurity()).thenReturn(security);
when(security.getOauth2()).thenReturn(oauth);
when(authentication.getAuthorizedClientRegistrationId()).thenReturn("test");
customLogoutSuccessHandler.onLogoutSuccess(request, response, authentication);
verify(response).sendRedirect(url + "/login?errorOAuth=" + error);
}
@Test
void testOAuth2Error() throws IOException {
String error = "test";
String url = "http://localhost:8080";
HttpServletRequest request = mock(HttpServletRequest.class);
HttpServletResponse response = mock(HttpServletResponse.class);
OAuth2AuthenticationToken authentication = mock(OAuth2AuthenticationToken.class);
ApplicationProperties.Security security = mock(ApplicationProperties.Security.class);
ApplicationProperties.Security.OAUTH2 oauth = mock(ApplicationProperties.Security.OAUTH2.class);
when(response.isCommitted()).thenReturn(false);
when(request.getParameter("oAuth2AuthenticationErrorWeb")).thenReturn(null);
when(request.getParameter("errorOAuth")).thenReturn(null);
when(request.getParameter("oAuth2AutoCreateDisabled")).thenReturn(null);
when(request.getParameter("oAuth2AdminBlockedUser")).thenReturn(null);
when(request.getParameter("userIsDisabled")).thenReturn(null);
when(request.getParameter("error")).thenReturn("!@$!@£" + error + "£$%^*$");
when(request.getScheme()).thenReturn("http");
when(request.getServerName()).thenReturn("localhost");
when(request.getServerPort()).thenReturn(8080);
when(request.getContextPath()).thenReturn("");
when(applicationProperties.getSecurity()).thenReturn(security);
when(security.getOauth2()).thenReturn(oauth);
when(authentication.getAuthorizedClientRegistrationId()).thenReturn("test");
customLogoutSuccessHandler.onLogoutSuccess(request, response, authentication);
verify(response).sendRedirect(url + "/login?errorOAuth=" + error);
}
@Test
void testOAuth2BadCredentialsError() throws IOException {
String error = "badCredentials";
String url = "http://localhost:8080";
HttpServletRequest request = mock(HttpServletRequest.class);
HttpServletResponse response = mock(HttpServletResponse.class);
OAuth2AuthenticationToken authentication = mock(OAuth2AuthenticationToken.class);
ApplicationProperties.Security security = mock(ApplicationProperties.Security.class);
ApplicationProperties.Security.OAUTH2 oauth = mock(ApplicationProperties.Security.OAUTH2.class);
when(response.isCommitted()).thenReturn(false);
when(request.getParameter("oAuth2AuthenticationErrorWeb")).thenReturn(null);
when(request.getParameter("errorOAuth")).thenReturn(null);
when(request.getParameter("oAuth2AutoCreateDisabled")).thenReturn(null);
when(request.getParameter("oAuth2AdminBlockedUser")).thenReturn(null);
when(request.getParameter("userIsDisabled")).thenReturn(null);
when(request.getParameter("error")).thenReturn(null);
when(request.getParameter(error)).thenReturn("true");
when(request.getScheme()).thenReturn("http");
when(request.getServerName()).thenReturn("localhost");
when(request.getServerPort()).thenReturn(8080);
when(request.getContextPath()).thenReturn("");
when(applicationProperties.getSecurity()).thenReturn(security);
when(security.getOauth2()).thenReturn(oauth);
when(authentication.getAuthorizedClientRegistrationId()).thenReturn("test");
customLogoutSuccessHandler.onLogoutSuccess(request, response, authentication);
verify(response).sendRedirect(url + "/login?errorOAuth=" + error);
}
@Test
void testOAuth2AdminBlockedUser() throws IOException {
String error = "oAuth2AdminBlockedUser";
String url = "http://localhost:8080";
HttpServletRequest request = mock(HttpServletRequest.class);
HttpServletResponse response = mock(HttpServletResponse.class);
OAuth2AuthenticationToken authentication = mock(OAuth2AuthenticationToken.class);
ApplicationProperties.Security security = mock(ApplicationProperties.Security.class);
ApplicationProperties.Security.OAUTH2 oauth = mock(ApplicationProperties.Security.OAUTH2.class);
when(response.isCommitted()).thenReturn(false);
when(request.getParameter("oAuth2AuthenticationErrorWeb")).thenReturn(null);
when(request.getParameter("errorOAuth")).thenReturn(null);
when(request.getParameter("oAuth2AutoCreateDisabled")).thenReturn(null);
when(request.getParameter(error)).thenReturn("true");
when(request.getScheme()).thenReturn("http");
when(request.getServerName()).thenReturn("localhost");
when(request.getServerPort()).thenReturn(8080);
when(request.getContextPath()).thenReturn("");
when(applicationProperties.getSecurity()).thenReturn(security);
when(security.getOauth2()).thenReturn(oauth);
when(authentication.getAuthorizedClientRegistrationId()).thenReturn("test");
customLogoutSuccessHandler.onLogoutSuccess(request, response, authentication);
verify(response).sendRedirect(url + "/login?errorOAuth=" + error);
}
}

View File

@ -9,7 +9,7 @@ import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
import stirling.software.SPDF.model.exception.UnsupportedProviderException;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.mock;

View File

@ -0,0 +1,54 @@
package stirling.software.SPDF.utils.validation;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.junit.jupiter.MockitoExtension;
import stirling.software.SPDF.model.UsernameAttribute;
import stirling.software.SPDF.model.provider.GitHubProvider;
import stirling.software.SPDF.model.provider.GoogleProvider;
import stirling.software.SPDF.model.provider.KeycloakProvider;
import stirling.software.SPDF.model.provider.Provider;
import java.util.List;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class ValidatorTest {
@Test
void testSuccessfulValidation() {
var provider = mock(GitHubProvider.class);
when(provider.getClientId()).thenReturn("clientId");
when(provider.getClientSecret()).thenReturn("clientSecret");
when(provider.getScopes()).thenReturn(List.of("read:user"));
assertTrue(Validator.validateProvider(provider));
}
@ParameterizedTest
@MethodSource("providerParams")
void testUnsuccessfulValidation(Provider provider) {
assertFalse(Validator.validateProvider(provider));
}
public static Stream<Arguments> providerParams() {
Provider generic = null;
var google = new GoogleProvider(null, "clientSecret", List.of("scope"), UsernameAttribute.EMAIL);
var github = new GitHubProvider("clientId", "", List.of("scope"), UsernameAttribute.LOGIN);
return Stream.of(
Arguments.of(generic),
Arguments.of(google),
Arguments.of(github)
);
}
}