mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2024-11-11 02:10:11 +01:00
Merge pull request #1284 from Ludy87/add_multi_oauth2
add multi OAuth2 Provider
This commit is contained in:
commit
f06d755899
@ -8,11 +8,7 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.context.ApplicationContextInitializer;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
@ -62,7 +58,7 @@ public class ConfigInitializer
|
||||
Files.exists(userPath) ? Files.readAllLines(userPath) : new ArrayList<>();
|
||||
|
||||
List<String> resultLines = new ArrayList<>();
|
||||
|
||||
int position = 0;
|
||||
for (String templateLine : templateLines) {
|
||||
// Check if the line is a comment
|
||||
if (templateLine.trim().startsWith("#")) {
|
||||
@ -70,7 +66,7 @@ public class ConfigInitializer
|
||||
if (!entry.isEmpty()) {
|
||||
// Check if this comment has been uncommented in userLines
|
||||
String key = entry.split(":")[0].trim();
|
||||
addLine(resultLines, userLines, templateLine, key);
|
||||
addLine(resultLines, userLines, templateLine, key, position);
|
||||
} else {
|
||||
resultLines.add(templateLine);
|
||||
}
|
||||
@ -78,12 +74,13 @@ public class ConfigInitializer
|
||||
// Check if the line is a key-value pair
|
||||
else if (templateLine.contains(":")) {
|
||||
String key = templateLine.split(":")[0].trim();
|
||||
addLine(resultLines, userLines, templateLine, key);
|
||||
addLine(resultLines, userLines, templateLine, key, position);
|
||||
}
|
||||
// Handle empty lines
|
||||
else if (templateLine.trim().length() == 0) {
|
||||
resultLines.add("");
|
||||
}
|
||||
position++;
|
||||
}
|
||||
|
||||
// Write the result to the user settings file
|
||||
@ -96,14 +93,18 @@ public class ConfigInitializer
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// TODO check parent value instead of just indent lines for duplicate keys (like enabled etc)
|
||||
private static void addLine(List<String> resultLines, List<String> userLines, String templateLine, String key) {
|
||||
private static void addLine(
|
||||
List<String> resultLines,
|
||||
List<String> userLines,
|
||||
String templateLine,
|
||||
String key,
|
||||
int position) {
|
||||
boolean added = false;
|
||||
int templateIndentationLevel = getIndentationLevel(templateLine);
|
||||
int pos = 0;
|
||||
for (String settingsLine : userLines) {
|
||||
if (settingsLine.trim().startsWith(key + ":")) {
|
||||
if (settingsLine.trim().startsWith(key + ":") && position == pos) {
|
||||
int settingsIndentationLevel = getIndentationLevel(settingsLine);
|
||||
// Check if it is correct settingsLine and has the same parent as templateLine
|
||||
if (settingsIndentationLevel == templateIndentationLevel) {
|
||||
@ -112,6 +113,7 @@ public class ConfigInitializer
|
||||
break;
|
||||
}
|
||||
}
|
||||
pos++;
|
||||
}
|
||||
if (!added) {
|
||||
resultLines.add(templateLine);
|
||||
|
@ -2,6 +2,8 @@ package stirling.software.SPDF.config.security;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
@ -36,7 +38,11 @@ import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationS
|
||||
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2LogoutSuccessHandler;
|
||||
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2UserService;
|
||||
import stirling.software.SPDF.model.ApplicationProperties;
|
||||
import stirling.software.SPDF.model.ApplicationProperties.GithubProvider;
|
||||
import stirling.software.SPDF.model.ApplicationProperties.GoogleProvider;
|
||||
import stirling.software.SPDF.model.ApplicationProperties.KeycloakProvider;
|
||||
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.repository.JPATokenRepositoryImpl;
|
||||
|
||||
@ -47,6 +53,8 @@ public class SecurityConfiguration {
|
||||
|
||||
@Autowired private CustomUserDetailsService userDetailsService;
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(SecurityConfiguration.class);
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
@ -140,6 +148,7 @@ public class SecurityConfiguration {
|
||||
|| trimmedUri.startsWith("/images/")
|
||||
|| trimmedUri.startsWith("/public/")
|
||||
|| trimmedUri.startsWith("/css/")
|
||||
|| trimmedUri.startsWith("/fonts/")
|
||||
|| trimmedUri.startsWith("/js/")
|
||||
|| trimmedUri.startsWith(
|
||||
"/api/v1/info/status");
|
||||
@ -150,7 +159,8 @@ public class SecurityConfiguration {
|
||||
.authenticationProvider(authenticationProvider());
|
||||
|
||||
// Handle OAUTH2 Logins
|
||||
if (applicationProperties.getSecurity().getOAUTH2().getEnabled()) {
|
||||
if (applicationProperties.getSecurity().getOAUTH2() != null
|
||||
&& applicationProperties.getSecurity().getOAUTH2().getEnabled()) {
|
||||
|
||||
http.oauth2Login(
|
||||
oauth2 ->
|
||||
@ -183,7 +193,8 @@ public class SecurityConfiguration {
|
||||
logout.logoutSuccessHandler(
|
||||
new CustomOAuth2LogoutSuccessHandler(
|
||||
this.applicationProperties,
|
||||
sessionRegistry())));
|
||||
sessionRegistry()))
|
||||
.invalidateHttpSession(true));
|
||||
}
|
||||
} else {
|
||||
http.csrf(csrf -> csrf.disable())
|
||||
@ -200,19 +211,127 @@ public class SecurityConfiguration {
|
||||
havingValue = "true",
|
||||
matchIfMissing = false)
|
||||
public ClientRegistrationRepository clientRegistrationRepository() {
|
||||
return new InMemoryClientRegistrationRepository(this.oidcClientRegistration());
|
||||
List<ClientRegistration> registrations = new ArrayList<>();
|
||||
|
||||
githubClientRegistration().ifPresent(registrations::add);
|
||||
oidcClientRegistration().ifPresent(registrations::add);
|
||||
googleClientRegistration().ifPresent(registrations::add);
|
||||
keycloakClientRegistration().ifPresent(registrations::add);
|
||||
|
||||
if (registrations.isEmpty()) {
|
||||
logger.error("At least one OAuth2 provider must be configured");
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
private ClientRegistration oidcClientRegistration() {
|
||||
return new InMemoryClientRegistrationRepository(registrations);
|
||||
}
|
||||
|
||||
private Optional<ClientRegistration> googleClientRegistration() {
|
||||
OAUTH2 oauth = applicationProperties.getSecurity().getOAUTH2();
|
||||
return ClientRegistrations.fromIssuerLocation(oauth.getIssuer())
|
||||
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")
|
||||
.clientId(google.getClientId())
|
||||
.clientSecret(google.getClientSecret())
|
||||
.scope(google.getScopes())
|
||||
.authorizationUri(google.getAuthorizationuri())
|
||||
.tokenUri(google.getTokenuri())
|
||||
.userInfoUri(google.getUserinfouri())
|
||||
.userNameAttributeName(google.getUseAsUsername())
|
||||
.clientName("Google")
|
||||
.redirectUri("{baseUrl}/login/oauth2/code/google")
|
||||
.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()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
Client client = oauth.getClient();
|
||||
if (client == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
KeycloakProvider keycloak = client.getKeycloak();
|
||||
|
||||
return keycloak != null && keycloak.isSettingsValid()
|
||||
? Optional.of(
|
||||
ClientRegistrations.fromIssuerLocation(keycloak.getIssuer())
|
||||
.registrationId("keycloak")
|
||||
.clientId(keycloak.getClientId())
|
||||
.clientSecret(keycloak.getClientSecret())
|
||||
.scope(keycloak.getScopes())
|
||||
.userNameAttributeName(keycloak.getUseAsUsername())
|
||||
.clientName("Keycloak")
|
||||
.build())
|
||||
: Optional.empty();
|
||||
}
|
||||
|
||||
private Optional<ClientRegistration> githubClientRegistration() {
|
||||
OAUTH2 oauth = applicationProperties.getSecurity().getOAUTH2();
|
||||
if (oauth == null || !oauth.getEnabled()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
Client client = oauth.getClient();
|
||||
if (client == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
GithubProvider github = client.getGithub();
|
||||
return github != null && github.isSettingsValid()
|
||||
? Optional.of(
|
||||
ClientRegistration.withRegistrationId("github")
|
||||
.clientId(github.getClientId())
|
||||
.clientSecret(github.getClientSecret())
|
||||
.scope(github.getScopes())
|
||||
.authorizationUri(github.getAuthorizationuri())
|
||||
.tokenUri(github.getTokenuri())
|
||||
.userInfoUri(github.getUserinfouri())
|
||||
.userNameAttributeName(github.getUseAsUsername())
|
||||
.clientName("GitHub")
|
||||
.redirectUri("{baseUrl}/login/oauth2/code/github")
|
||||
.authorizationGrantType(
|
||||
org.springframework.security.oauth2.core
|
||||
.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()) {
|
||||
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();
|
||||
.build());
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -101,6 +101,7 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
||||
contextPath + "/images/",
|
||||
contextPath + "/public/",
|
||||
contextPath + "/css/",
|
||||
contextPath + "/fonts/",
|
||||
contextPath + "/js/",
|
||||
contextPath + "/pdfjs/",
|
||||
contextPath + "/api/v1/info/status",
|
||||
|
@ -41,6 +41,7 @@ public class CustomOAuth2AuthenticationFailureHandler
|
||||
} else if (exception instanceof LockedException) {
|
||||
logger.error("Account locked: ", exception);
|
||||
getRedirectStrategy().sendRedirect(request, response, "/logout?error=locked");
|
||||
return;
|
||||
} else {
|
||||
logger.error("Unhandled authentication exception", exception);
|
||||
super.onAuthenticationFailure(request, response, exception);
|
||||
|
@ -6,6 +6,7 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.session.SessionRegistry;
|
||||
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
|
||||
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
|
||||
|
||||
import jakarta.servlet.ServletException;
|
||||
@ -14,6 +15,8 @@ import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import stirling.software.SPDF.model.ApplicationProperties;
|
||||
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
||||
import stirling.software.SPDF.model.Provider;
|
||||
import stirling.software.SPDF.utils.UrlUtils;
|
||||
|
||||
public class CustomOAuth2LogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
||||
|
||||
@ -33,11 +36,30 @@ public class CustomOAuth2LogoutSuccessHandler extends SimpleUrlLogoutSuccessHand
|
||||
public void onLogoutSuccess(
|
||||
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
||||
throws IOException, ServletException {
|
||||
|
||||
String param = "logout=true";
|
||||
String registrationId = null;
|
||||
String issuer = null;
|
||||
String clientId = null;
|
||||
|
||||
OAUTH2 oauth = applicationProperties.getSecurity().getOAUTH2();
|
||||
String provider = oauth.getProvider() != null ? oauth.getProvider() : "";
|
||||
|
||||
if (authentication instanceof OAuth2AuthenticationToken) {
|
||||
OAuth2AuthenticationToken oauthToken = (OAuth2AuthenticationToken) authentication;
|
||||
registrationId = oauthToken.getAuthorizedClientRegistrationId();
|
||||
|
||||
try {
|
||||
Provider provider = oauth.getClient().get(registrationId);
|
||||
issuer = provider.getIssuer();
|
||||
clientId = provider.getClientId();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
} else {
|
||||
registrationId = oauth.getProvider() != null ? oauth.getProvider() : "";
|
||||
issuer = oauth.getIssuer();
|
||||
clientId = oauth.getClientId();
|
||||
}
|
||||
|
||||
if (request.getParameter("oauth2AuthenticationErrorWeb") != null) {
|
||||
param = "erroroauth=oauth2AuthenticationErrorWeb";
|
||||
@ -49,36 +71,46 @@ public class CustomOAuth2LogoutSuccessHandler extends SimpleUrlLogoutSuccessHand
|
||||
param = "error=oauth2AutoCreateDisabled";
|
||||
}
|
||||
|
||||
String redirect_url = UrlUtils.getOrigin(request) + "/login?" + param;
|
||||
|
||||
HttpSession session = request.getSession(false);
|
||||
if (session != null) {
|
||||
String sessionId = session.getId();
|
||||
sessionRegistry.removeSessionInformation(sessionId);
|
||||
session.invalidate();
|
||||
logger.debug("Session invalidated: " + sessionId);
|
||||
logger.info("Session invalidated: " + sessionId);
|
||||
}
|
||||
|
||||
switch (provider) {
|
||||
switch (registrationId) {
|
||||
case "keycloak":
|
||||
// Add Keycloak specific logout URL if needed
|
||||
String logoutUrl =
|
||||
oauth.getIssuer()
|
||||
issuer
|
||||
+ "/protocol/openid-connect/logout"
|
||||
+ "?client_id="
|
||||
+ oauth.getClientId()
|
||||
+ clientId
|
||||
+ "&post_logout_redirect_uri="
|
||||
+ response.encodeRedirectURL(
|
||||
request.getScheme()
|
||||
+ "://"
|
||||
+ request.getHeader("host")
|
||||
+ "/login?"
|
||||
+ param);
|
||||
logger.debug("Redirecting to Keycloak logout URL: " + logoutUrl);
|
||||
+ response.encodeRedirectURL(redirect_url);
|
||||
logger.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";
|
||||
logger.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);
|
||||
// logger.info("Redirecting to Google logout URL: " + googleLogoutUrl);
|
||||
// response.sendRedirect(googleLogoutUrl);
|
||||
// break;
|
||||
default:
|
||||
String redirectUrl = request.getContextPath() + "/login?" + param;
|
||||
logger.debug("Redirecting to default logout URL: " + redirectUrl);
|
||||
logger.info("Redirecting to default logout URL: " + redirectUrl);
|
||||
response.sendRedirect(redirectUrl);
|
||||
break;
|
||||
}
|
||||
|
@ -1,57 +0,0 @@
|
||||
package stirling.software.SPDF.config.security.oauth2;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
|
||||
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
|
||||
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
|
||||
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
|
||||
|
||||
import stirling.software.SPDF.model.ApplicationProperties;
|
||||
|
||||
public class CustomOAuthUserService implements OAuth2UserService<OidcUserRequest, OidcUser> {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(CustomOAuthUserService.class);
|
||||
|
||||
private final OidcUserService delegate = new OidcUserService();
|
||||
|
||||
private ApplicationProperties applicationProperties;
|
||||
|
||||
public CustomOAuthUserService(ApplicationProperties applicationProperties) {
|
||||
this.applicationProperties = applicationProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
|
||||
String usernameAttribute =
|
||||
applicationProperties.getSecurity().getOAUTH2().getUseAsUsername();
|
||||
try {
|
||||
|
||||
OidcUser user = delegate.loadUser(userRequest);
|
||||
Map<String, Object> attributes = new HashMap<>(user.getAttributes());
|
||||
|
||||
// Ensure the preferred username attribute is present
|
||||
if (!attributes.containsKey(usernameAttribute)) {
|
||||
attributes.put(usernameAttribute, attributes.getOrDefault("email", ""));
|
||||
usernameAttribute = "email";
|
||||
logger.info("Adjusted username attribute to use email");
|
||||
}
|
||||
|
||||
// Return a new OidcUser with adjusted attributes
|
||||
return new DefaultOidcUser(
|
||||
user.getAuthorities(),
|
||||
userRequest.getIdToken(),
|
||||
user.getUserInfo(),
|
||||
usernameAttribute);
|
||||
} catch (java.lang.IllegalArgumentException e) {
|
||||
throw new OAuth2AuthenticationException(
|
||||
new OAuth2Error(e.getMessage()), e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,10 +1,13 @@
|
||||
package stirling.software.SPDF.controller.web;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.Authentication;
|
||||
@ -21,6 +24,11 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import stirling.software.SPDF.model.ApplicationProperties;
|
||||
import stirling.software.SPDF.model.ApplicationProperties.GithubProvider;
|
||||
import stirling.software.SPDF.model.ApplicationProperties.GoogleProvider;
|
||||
import stirling.software.SPDF.model.ApplicationProperties.KeycloakProvider;
|
||||
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
||||
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2.Client;
|
||||
import stirling.software.SPDF.model.Authority;
|
||||
import stirling.software.SPDF.model.Role;
|
||||
import stirling.software.SPDF.model.User;
|
||||
@ -31,6 +39,7 @@ import stirling.software.SPDF.repository.UserRepository;
|
||||
public class AccountWebController {
|
||||
|
||||
@Autowired ApplicationProperties applicationProperties;
|
||||
private static final Logger logger = LoggerFactory.getLogger(AccountWebController.class);
|
||||
|
||||
@GetMapping("/login")
|
||||
public String login(HttpServletRequest request, Model model, Authentication authentication) {
|
||||
@ -38,6 +47,33 @@ public class AccountWebController {
|
||||
return "redirect:/";
|
||||
}
|
||||
|
||||
Map<String, String> providerList = new HashMap<>();
|
||||
|
||||
OAUTH2 oauth = applicationProperties.getSecurity().getOAUTH2();
|
||||
if (oauth != null) {
|
||||
if (oauth.isSettingsValid()) {
|
||||
providerList.put("oidc", "OpenID Connect");
|
||||
}
|
||||
Client client = oauth.getClient();
|
||||
if (client != null) {
|
||||
GoogleProvider google = client.getGoogle();
|
||||
if (google.isSettingsValid()) {
|
||||
providerList.put("google", "Google");
|
||||
}
|
||||
|
||||
GithubProvider github = client.getGithub();
|
||||
if (github.isSettingsValid()) {
|
||||
providerList.put("github", "Github");
|
||||
}
|
||||
|
||||
KeycloakProvider keycloak = client.getKeycloak();
|
||||
if (keycloak.isSettingsValid()) {
|
||||
providerList.put("keycloak", "Keycloak");
|
||||
}
|
||||
}
|
||||
}
|
||||
model.addAttribute("providerlist", providerList);
|
||||
|
||||
model.addAttribute(
|
||||
"oAuth2Enabled", applicationProperties.getSecurity().getOAUTH2().getEnabled());
|
||||
|
||||
@ -80,6 +116,19 @@ public class AccountWebController {
|
||||
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;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
@ -6,6 +6,8 @@ import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.PropertySource;
|
||||
@ -23,6 +25,7 @@ public class ApplicationProperties {
|
||||
private Metrics metrics;
|
||||
private AutomaticallyGenerated automaticallyGenerated;
|
||||
private AutoPipeline autoPipeline;
|
||||
private static final Logger logger = LoggerFactory.getLogger(ApplicationProperties.class);
|
||||
|
||||
public AutoPipeline getAutoPipeline() {
|
||||
return autoPipeline != null ? autoPipeline : new AutoPipeline();
|
||||
@ -188,7 +191,6 @@ public class ApplicationProperties {
|
||||
}
|
||||
|
||||
public static class InitialLogin {
|
||||
|
||||
private String username;
|
||||
private String password;
|
||||
|
||||
@ -219,22 +221,21 @@ public class ApplicationProperties {
|
||||
}
|
||||
|
||||
public static class OAUTH2 {
|
||||
|
||||
private boolean enabled;
|
||||
private Boolean enabled = false;
|
||||
private String issuer;
|
||||
private String clientId;
|
||||
private String clientSecret;
|
||||
private boolean autoCreateUser;
|
||||
private Boolean autoCreateUser = false;
|
||||
private String useAsUsername;
|
||||
private Collection<String> scopes = new ArrayList<>();
|
||||
private String provider;
|
||||
private Client client = new Client();
|
||||
|
||||
private Collection<String> scopes = new ArrayList<String>();
|
||||
|
||||
public boolean getEnabled() {
|
||||
public Boolean getEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
public void setEnabled(Boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
@ -262,20 +263,17 @@ public class ApplicationProperties {
|
||||
this.clientSecret = clientSecret;
|
||||
}
|
||||
|
||||
public boolean getAutoCreateUser() {
|
||||
public Boolean getAutoCreateUser() {
|
||||
return autoCreateUser;
|
||||
}
|
||||
|
||||
public void setAutoCreateUser(boolean autoCreateUser) {
|
||||
public void setAutoCreateUser(Boolean autoCreateUser) {
|
||||
this.autoCreateUser = autoCreateUser;
|
||||
}
|
||||
|
||||
public String getUseAsUsername() {
|
||||
if (useAsUsername != null && useAsUsername.trim().length() > 0) {
|
||||
return useAsUsername;
|
||||
}
|
||||
return "email";
|
||||
}
|
||||
|
||||
public void setUseAsUsername(String useAsUsername) {
|
||||
this.useAsUsername = useAsUsername;
|
||||
@ -293,14 +291,44 @@ public class ApplicationProperties {
|
||||
return scopes;
|
||||
}
|
||||
|
||||
public void setScopes(String scpoes) {
|
||||
public void setScopes(String scopes) {
|
||||
List<String> scopesList =
|
||||
Arrays.stream(scpoes.split(","))
|
||||
Arrays.stream(scopes.split(","))
|
||||
.map(String::trim)
|
||||
.collect(Collectors.toList());
|
||||
this.scopes.addAll(scopesList);
|
||||
}
|
||||
|
||||
public Client getClient() {
|
||||
return client;
|
||||
}
|
||||
|
||||
public void setClient(Client client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public boolean isSettingsValid() {
|
||||
return isValid(this.getIssuer(), "issuer")
|
||||
&& isValid(this.getClientId(), "clientId")
|
||||
&& isValid(this.getClientSecret(), "clientSecret")
|
||||
&& isValid(this.getScopes(), "scopes")
|
||||
&& isValid(this.getUseAsUsername(), "useAsUsername");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "OAUTH2 [enabled="
|
||||
@ -315,12 +343,353 @@ public class ApplicationProperties {
|
||||
+ autoCreateUser
|
||||
+ ", useAsUsername="
|
||||
+ useAsUsername
|
||||
+ ", provider"
|
||||
+ ", provider="
|
||||
+ provider
|
||||
+ ", scopes="
|
||||
+ scopes
|
||||
+ "]";
|
||||
}
|
||||
|
||||
public static class Client {
|
||||
private GoogleProvider google = new GoogleProvider();
|
||||
private GithubProvider github = new GithubProvider();
|
||||
private KeycloakProvider keycloak = new KeycloakProvider();
|
||||
|
||||
public Provider get(String registrationId) throws Exception {
|
||||
switch (registrationId) {
|
||||
case "gogole":
|
||||
return getGoogle();
|
||||
case "github":
|
||||
return getGithub();
|
||||
case "keycloak":
|
||||
return getKeycloak();
|
||||
default:
|
||||
break;
|
||||
}
|
||||
throw new Exception("Provider not supported, use custom setting.");
|
||||
}
|
||||
|
||||
public GoogleProvider getGoogle() {
|
||||
return google;
|
||||
}
|
||||
|
||||
public void setGoogle(GoogleProvider google) {
|
||||
this.google = google;
|
||||
}
|
||||
|
||||
public GithubProvider getGithub() {
|
||||
return github;
|
||||
}
|
||||
|
||||
public void setGithub(GithubProvider github) {
|
||||
this.github = github;
|
||||
}
|
||||
|
||||
public KeycloakProvider getKeycloak() {
|
||||
return keycloak;
|
||||
}
|
||||
|
||||
public void setKeycloak(KeycloakProvider keycloak) {
|
||||
this.keycloak = keycloak;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Client [google="
|
||||
+ google
|
||||
+ ", github="
|
||||
+ github
|
||||
+ ", keycloak="
|
||||
+ keycloak
|
||||
+ "]";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static 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 =
|
||||
"https://www.googleapis.com/oauth2/v3/userinfo?alt=json";
|
||||
|
||||
public String getAuthorizationuri() {
|
||||
return authorizationUri;
|
||||
}
|
||||
|
||||
public String getTokenuri() {
|
||||
return tokenUri;
|
||||
}
|
||||
|
||||
public String getUserinfouri() {
|
||||
return userInfoUri;
|
||||
}
|
||||
|
||||
private String clientId;
|
||||
private String clientSecret;
|
||||
private Collection<String> scopes = new ArrayList<>();
|
||||
private String useAsUsername = "email";
|
||||
|
||||
@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.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
|
||||
+ ", clientSecret="
|
||||
+ (clientSecret != null && !clientSecret.isEmpty() ? "MASKED" : "NULL")
|
||||
+ ", scopes="
|
||||
+ scopes
|
||||
+ ", useAsUsername="
|
||||
+ useAsUsername
|
||||
+ "]";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
public static 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";
|
||||
|
||||
public String getAuthorizationuri() {
|
||||
return authorizationUri;
|
||||
}
|
||||
|
||||
public String getTokenuri() {
|
||||
return tokenUri;
|
||||
}
|
||||
|
||||
public String getUserinfouri() {
|
||||
return userInfoUri;
|
||||
}
|
||||
|
||||
private String clientId;
|
||||
private String clientSecret;
|
||||
private Collection<String> scopes = new ArrayList<>();
|
||||
private String useAsUsername = "login";
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
public Collection<String> getScopes() {
|
||||
if (scopes == null || scopes.isEmpty()) {
|
||||
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";
|
||||
}
|
||||
|
||||
public boolean isSettingsValid() {
|
||||
return super.isValid(this.getClientId(), "clientId")
|
||||
&& super.isValid(this.getClientSecret(), "clientSecret")
|
||||
&& super.isValid(this.getScopes(), "scopes")
|
||||
&& isValid(this.getUseAsUsername(), "useAsUsername");
|
||||
}
|
||||
}
|
||||
|
||||
public static class KeycloakProvider extends Provider {
|
||||
private String issuer;
|
||||
private String clientId;
|
||||
private String clientSecret;
|
||||
private Collection<String> scopes = new ArrayList<>();
|
||||
private String useAsUsername = "email";
|
||||
|
||||
@Override
|
||||
public String getIssuer() {
|
||||
return this.issuer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setIssuer(String issuer) {
|
||||
this.issuer = 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.add("openid");
|
||||
scopes.add("profile");
|
||||
scopes.add("email");
|
||||
}
|
||||
return scopes;
|
||||
}
|
||||
|
||||
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
|
||||
+ ", clientId="
|
||||
+ clientId
|
||||
+ ", clientSecret="
|
||||
+ (clientSecret != null && !clientSecret.isEmpty() ? "MASKED" : "NULL")
|
||||
+ ", scopes="
|
||||
+ scopes
|
||||
+ ", useAsUsername="
|
||||
+ useAsUsername
|
||||
+ "]";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
|
87
src/main/java/stirling/software/SPDF/model/Provider.java
Normal file
87
src/main/java/stirling/software/SPDF/model/Provider.java
Normal file
@ -0,0 +1,87 @@
|
||||
package stirling.software.SPDF.model;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
public class Provider implements ProviderInterface {
|
||||
private String name;
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
protected boolean isValid(String value, String name) {
|
||||
if (value != null && !value.trim().isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
// throw new IllegalArgumentException(getName() + ": " + name + " is required!");
|
||||
}
|
||||
|
||||
protected boolean isValid(Collection<String> value, String name) {
|
||||
if (value != null && !value.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
// throw new IllegalArgumentException(getName() + ": " + name + " is required!");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<String> getScopes() {
|
||||
// TODO Auto-generated method stub
|
||||
throw new UnsupportedOperationException("Unimplemented method 'getScope'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setScopes(String scopes) {
|
||||
// TODO Auto-generated method stub
|
||||
throw new UnsupportedOperationException("Unimplemented method 'setScope'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUseAsUsername() {
|
||||
// TODO Auto-generated method stub
|
||||
throw new UnsupportedOperationException("Unimplemented method 'getUseAsUsername'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUseAsUsername(String useAsUsername) {
|
||||
// TODO Auto-generated method stub
|
||||
throw new UnsupportedOperationException("Unimplemented method 'setUseAsUsername'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getIssuer() {
|
||||
// TODO Auto-generated method stub
|
||||
throw new UnsupportedOperationException("Unimplemented method 'getIssuer'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setIssuer(String issuer) {
|
||||
// TODO Auto-generated method stub
|
||||
throw new UnsupportedOperationException("Unimplemented method 'setIssuer'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getClientSecret() {
|
||||
// TODO Auto-generated method stub
|
||||
throw new UnsupportedOperationException("Unimplemented method 'getClientSecret'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setClientSecret(String clientSecret) {
|
||||
// TODO Auto-generated method stub
|
||||
throw new UnsupportedOperationException("Unimplemented method 'setClientSecret'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getClientId() {
|
||||
// TODO Auto-generated method stub
|
||||
throw new UnsupportedOperationException("Unimplemented method 'getClientId'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setClientId(String clientId) {
|
||||
// TODO Auto-generated method stub
|
||||
throw new UnsupportedOperationException("Unimplemented method 'setClientId'");
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
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);
|
||||
}
|
@ -5,6 +5,7 @@ public class RequestUriUtils {
|
||||
public static boolean isStaticResource(String requestURI) {
|
||||
|
||||
return requestURI.startsWith("/css/")
|
||||
|| requestURI.startsWith("/fonts/")
|
||||
|| requestURI.startsWith("/js/")
|
||||
|| requestURI.startsWith("/images/")
|
||||
|| requestURI.startsWith("/public/")
|
||||
|
15
src/main/java/stirling/software/SPDF/utils/UrlUtils.java
Normal file
15
src/main/java/stirling/software/SPDF/utils/UrlUtils.java
Normal file
@ -0,0 +1,15 @@
|
||||
package stirling.software.SPDF.utils;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
public class UrlUtils {
|
||||
|
||||
public static String getOrigin(HttpServletRequest request) {
|
||||
String scheme = request.getScheme(); // http or https
|
||||
String serverName = request.getServerName(); // localhost
|
||||
int serverPort = request.getServerPort(); // 8080
|
||||
String contextPath = request.getContextPath(); // /myapp
|
||||
|
||||
return scheme + "://" + serverName + ":" + serverPort + contextPath;
|
||||
}
|
||||
}
|
@ -452,6 +452,11 @@ login.locked=Your account has been locked.
|
||||
login.signinTitle=Please sign in
|
||||
login.ssoSignIn=تسجيل الدخول عبر تسجيل الدخول الأحادي
|
||||
login.oauth2AutoCreateDisabled=تم تعطيل مستخدم الإنشاء التلقائي لـ OAuth2
|
||||
login.oauth2RequestNotFound=Authorization request not found
|
||||
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
|
||||
login.oauth2invalidRequest=Invalid Request
|
||||
login.oauth2AccessDenied=Access Denied
|
||||
login.oauth2InvalidTokenResponse=Invalid Token Response
|
||||
|
||||
|
||||
#auto-redact
|
||||
|
@ -452,6 +452,11 @@ login.locked=Вашият акаунт е заключен.
|
||||
login.signinTitle=Моля впишете се
|
||||
login.ssoSignIn=Влизане чрез еднократно влизане
|
||||
login.oauth2AutoCreateDisabled=OAUTH2 Автоматично създаване на потребител е деактивирано
|
||||
login.oauth2RequestNotFound=Authorization request not found
|
||||
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
|
||||
login.oauth2invalidRequest=Invalid Request
|
||||
login.oauth2AccessDenied=Access Denied
|
||||
login.oauth2InvalidTokenResponse=Invalid Token Response
|
||||
|
||||
|
||||
#auto-redact
|
||||
|
@ -452,6 +452,11 @@ login.locked=Compte bloquejat
|
||||
login.signinTitle=Autenticat
|
||||
login.ssoSignIn=Inicia sessió mitjançant l'inici de sessió ún
|
||||
login.oauth2AutoCreateDisabled=L'usuari de creació automàtica OAUTH2 està desactivat
|
||||
login.oauth2RequestNotFound=Authorization request not found
|
||||
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
|
||||
login.oauth2invalidRequest=Invalid Request
|
||||
login.oauth2AccessDenied=Access Denied
|
||||
login.oauth2InvalidTokenResponse=Invalid Token Response
|
||||
|
||||
|
||||
#auto-redact
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -452,6 +452,12 @@ 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.oauth2RequestNotFound=Authorization request not found
|
||||
login.oauth2AccessDenied=Access Denied
|
||||
login.oauth2InvalidTokenResponse=Invalid Token Response
|
||||
login.oauth2RequestNotFound=Authorization Request not found
|
||||
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
|
||||
login.oauth2invalidRequest=Invalid Request
|
||||
|
||||
|
||||
#auto-redact
|
||||
|
@ -452,6 +452,11 @@ login.locked=Ο λογαριασμός σας έχει κλειδωθεί.
|
||||
login.signinTitle=Παρακαλώ, συνδεθείτε
|
||||
login.ssoSignIn=Σύνδεση μέσω μοναδικής σύνδεσης
|
||||
login.oauth2AutoCreateDisabled=Απενεργοποιήθηκε ο χρήστης αυτόματης δημιουργίας OAUTH2
|
||||
login.oauth2RequestNotFound=Authorization request not found
|
||||
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
|
||||
login.oauth2invalidRequest=Invalid Request
|
||||
login.oauth2AccessDenied=Access Denied
|
||||
login.oauth2InvalidTokenResponse=Invalid Token Response
|
||||
|
||||
|
||||
#auto-redact
|
||||
|
@ -452,6 +452,12 @@ 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.oauth2RequestNotFound=Authorization request not found
|
||||
login.oauth2AccessDenied=Access Denied
|
||||
login.oauth2InvalidTokenResponse=Invalid Token Response
|
||||
login.oauth2RequestNotFound=Authorization Request not found
|
||||
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
|
||||
login.oauth2invalidRequest=Invalid Request
|
||||
|
||||
|
||||
#auto-redact
|
||||
|
@ -452,6 +452,11 @@ 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.oauth2RequestNotFound=Authorization request not found
|
||||
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
|
||||
login.oauth2invalidRequest=Invalid Request
|
||||
login.oauth2AccessDenied=Access Denied
|
||||
login.oauth2InvalidTokenResponse=Invalid Token Response
|
||||
|
||||
|
||||
#auto-redact
|
||||
|
@ -452,6 +452,11 @@ 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.oauth2RequestNotFound=Authorization request not found
|
||||
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
|
||||
login.oauth2invalidRequest=Invalid Request
|
||||
login.oauth2AccessDenied=Access Denied
|
||||
login.oauth2InvalidTokenResponse=Invalid Token Response
|
||||
|
||||
|
||||
#auto-redact
|
||||
|
@ -452,6 +452,11 @@ 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.oauth2RequestNotFound=Authorization request not found
|
||||
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
|
||||
login.oauth2invalidRequest=Invalid Request
|
||||
login.oauth2AccessDenied=Access Denied
|
||||
login.oauth2InvalidTokenResponse=Invalid Token Response
|
||||
|
||||
|
||||
#auto-redact
|
||||
|
@ -452,6 +452,11 @@ 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.oauth2RequestNotFound=Authorization request not found
|
||||
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
|
||||
login.oauth2invalidRequest=Invalid Request
|
||||
login.oauth2AccessDenied=Access Denied
|
||||
login.oauth2InvalidTokenResponse=Invalid Token Response
|
||||
|
||||
|
||||
#auto-redact
|
||||
|
@ -452,6 +452,11 @@ login.locked=आपका खाता लॉक कर दिया गया
|
||||
login.signinTitle=कृपया साइन इन करें
|
||||
login.ssoSignIn=सिंगल साइन - ऑन के ज़रिए लॉग इन करें
|
||||
login.oauth2AutoCreateDisabled=OAUTH2 ऑटो - क्रिएट यूज़र अक्षम किया गया
|
||||
login.oauth2RequestNotFound=Authorization request not found
|
||||
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
|
||||
login.oauth2invalidRequest=Invalid Request
|
||||
login.oauth2AccessDenied=Access Denied
|
||||
login.oauth2InvalidTokenResponse=Invalid Token Response
|
||||
|
||||
|
||||
#auto-redact
|
||||
|
@ -452,6 +452,11 @@ login.locked=A fiókja zárolva lett!
|
||||
login.signinTitle=Kérjük, jelentkezzen be!
|
||||
login.ssoSignIn=Bejelentkezés egyszeri bejelentkezéssel
|
||||
login.oauth2AutoCreateDisabled=OAUTH2 Felhasználó automatikus létrehozása letiltva
|
||||
login.oauth2RequestNotFound=Authorization request not found
|
||||
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
|
||||
login.oauth2invalidRequest=Invalid Request
|
||||
login.oauth2AccessDenied=Access Denied
|
||||
login.oauth2InvalidTokenResponse=Invalid Token Response
|
||||
|
||||
|
||||
#auto-redact
|
||||
|
@ -452,6 +452,11 @@ 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.oauth2RequestNotFound=Authorization request not found
|
||||
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
|
||||
login.oauth2invalidRequest=Invalid Request
|
||||
login.oauth2AccessDenied=Access Denied
|
||||
login.oauth2InvalidTokenResponse=Invalid Token Response
|
||||
|
||||
|
||||
#auto-redact
|
||||
|
@ -452,6 +452,11 @@ 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.oauth2RequestNotFound=Authorization request not found
|
||||
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
|
||||
login.oauth2invalidRequest=Invalid Request
|
||||
login.oauth2AccessDenied=Access Denied
|
||||
login.oauth2InvalidTokenResponse=Invalid Token Response
|
||||
|
||||
|
||||
#auto-redact
|
||||
|
@ -452,6 +452,11 @@ login.locked=あなたのアカウントはロックされています。
|
||||
login.signinTitle=サインインしてください
|
||||
login.ssoSignIn=シングルサインオンでログイン
|
||||
login.oauth2AutoCreateDisabled=OAuth 2自動作成ユーザーが無効
|
||||
login.oauth2RequestNotFound=Authorization request not found
|
||||
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
|
||||
login.oauth2invalidRequest=Invalid Request
|
||||
login.oauth2AccessDenied=Access Denied
|
||||
login.oauth2InvalidTokenResponse=Invalid Token Response
|
||||
|
||||
|
||||
#auto-redact
|
||||
|
@ -452,6 +452,11 @@ login.locked=계정이 잠겼습니다.
|
||||
login.signinTitle=로그인해 주세요.
|
||||
login.ssoSignIn=싱글사인온을 통한 로그인
|
||||
login.oauth2AutoCreateDisabled=OAUTH2 사용자 자동 생성 비활성화됨
|
||||
login.oauth2RequestNotFound=Authorization request not found
|
||||
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
|
||||
login.oauth2invalidRequest=Invalid Request
|
||||
login.oauth2AccessDenied=Access Denied
|
||||
login.oauth2InvalidTokenResponse=Invalid Token Response
|
||||
|
||||
|
||||
#auto-redact
|
||||
|
@ -452,6 +452,11 @@ 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.oauth2RequestNotFound=Authorization request not found
|
||||
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
|
||||
login.oauth2invalidRequest=Invalid Request
|
||||
login.oauth2AccessDenied=Access Denied
|
||||
login.oauth2InvalidTokenResponse=Invalid Token Response
|
||||
|
||||
|
||||
#auto-redact
|
||||
|
@ -452,6 +452,11 @@ login.locked=Your account has been locked.
|
||||
login.signinTitle=Please sign in
|
||||
login.ssoSignIn=Zaloguj się za pomocą logowania jednokrotnego
|
||||
login.oauth2AutoCreateDisabled=Wyłączono automatyczne tworzenie użytkownika OAUTH2
|
||||
login.oauth2RequestNotFound=Authorization request not found
|
||||
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
|
||||
login.oauth2invalidRequest=Invalid Request
|
||||
login.oauth2AccessDenied=Access Denied
|
||||
login.oauth2InvalidTokenResponse=Invalid Token Response
|
||||
|
||||
|
||||
#auto-redact
|
||||
|
@ -452,6 +452,11 @@ login.locked=Your account has been locked.
|
||||
login.signinTitle=Please sign in
|
||||
login.ssoSignIn=Iniciar sessão através de início de sessão único
|
||||
login.oauth2AutoCreateDisabled=OAUTH2 Auto-Criar Usuário Desativado
|
||||
login.oauth2RequestNotFound=Authorization request not found
|
||||
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
|
||||
login.oauth2invalidRequest=Invalid Request
|
||||
login.oauth2AccessDenied=Access Denied
|
||||
login.oauth2InvalidTokenResponse=Invalid Token Response
|
||||
|
||||
|
||||
#auto-redact
|
||||
|
@ -452,6 +452,11 @@ login.locked=A sua conta foi bloqueada.
|
||||
login.signinTitle=Introduza os seus dados de acesso
|
||||
login.ssoSignIn=Iniciar sessão através de início de sessão único
|
||||
login.oauth2AutoCreateDisabled=OAUTH2 Criação Automática de Utilizador Desativada
|
||||
login.oauth2RequestNotFound=Authorization request not found
|
||||
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
|
||||
login.oauth2invalidRequest=Invalid Request
|
||||
login.oauth2AccessDenied=Access Denied
|
||||
login.oauth2InvalidTokenResponse=Invalid Token Response
|
||||
|
||||
|
||||
#auto-redact
|
||||
|
@ -452,6 +452,11 @@ login.locked=Your account has been locked.
|
||||
login.signinTitle=Please sign in
|
||||
login.ssoSignIn=Conectare prin conectare unică
|
||||
login.oauth2AutoCreateDisabled=OAUTH2 Creare automată utilizator dezactivată
|
||||
login.oauth2RequestNotFound=Authorization request not found
|
||||
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
|
||||
login.oauth2invalidRequest=Invalid Request
|
||||
login.oauth2AccessDenied=Access Denied
|
||||
login.oauth2InvalidTokenResponse=Invalid Token Response
|
||||
|
||||
|
||||
#auto-redact
|
||||
|
@ -452,6 +452,11 @@ login.locked=Ваша учетная запись заблокирована.
|
||||
login.signinTitle=Пожалуйста, войдите
|
||||
login.ssoSignIn=Вход через единый вход
|
||||
login.oauth2AutoCreateDisabled=OAUTH2 Автоматическое создание пользователя отключено
|
||||
login.oauth2RequestNotFound=Authorization request not found
|
||||
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
|
||||
login.oauth2invalidRequest=Invalid Request
|
||||
login.oauth2AccessDenied=Access Denied
|
||||
login.oauth2InvalidTokenResponse=Invalid Token Response
|
||||
|
||||
|
||||
#auto-redact
|
||||
|
@ -452,6 +452,11 @@ 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.oauth2RequestNotFound=Authorization request not found
|
||||
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
|
||||
login.oauth2invalidRequest=Invalid Request
|
||||
login.oauth2AccessDenied=Access Denied
|
||||
login.oauth2InvalidTokenResponse=Invalid Token Response
|
||||
|
||||
|
||||
#auto-redact
|
||||
|
@ -452,6 +452,11 @@ 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.oauth2RequestNotFound=Authorization request not found
|
||||
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
|
||||
login.oauth2invalidRequest=Invalid Request
|
||||
login.oauth2AccessDenied=Access Denied
|
||||
login.oauth2InvalidTokenResponse=Invalid Token Response
|
||||
|
||||
|
||||
#auto-redact
|
||||
|
@ -452,6 +452,11 @@ login.locked=Your account has been locked.
|
||||
login.signinTitle=Please sign in
|
||||
login.ssoSignIn=Logga in via enkel inloggning
|
||||
login.oauth2AutoCreateDisabled=OAUTH2 Auto-Create User inaktiverad
|
||||
login.oauth2RequestNotFound=Authorization request not found
|
||||
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
|
||||
login.oauth2invalidRequest=Invalid Request
|
||||
login.oauth2AccessDenied=Access Denied
|
||||
login.oauth2InvalidTokenResponse=Invalid Token Response
|
||||
|
||||
|
||||
#auto-redact
|
||||
|
@ -452,6 +452,11 @@ 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.oauth2RequestNotFound=Authorization request not found
|
||||
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
|
||||
login.oauth2invalidRequest=Invalid Request
|
||||
login.oauth2AccessDenied=Access Denied
|
||||
login.oauth2InvalidTokenResponse=Invalid Token Response
|
||||
|
||||
|
||||
#auto-redact
|
||||
|
@ -452,6 +452,11 @@ login.locked=Ваш обліковий запис заблоковано.
|
||||
login.signinTitle=Будь ласка, увійдіть
|
||||
login.ssoSignIn=Увійти через єдиний вхід
|
||||
login.oauth2AutoCreateDisabled=Автоматичне створення користувача OAUTH2 ВИМКНЕНО
|
||||
login.oauth2RequestNotFound=Authorization request not found
|
||||
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
|
||||
login.oauth2invalidRequest=Invalid Request
|
||||
login.oauth2AccessDenied=Access Denied
|
||||
login.oauth2InvalidTokenResponse=Invalid Token Response
|
||||
|
||||
|
||||
#auto-redact
|
||||
|
@ -452,6 +452,11 @@ login.locked=您的账户已被锁定。
|
||||
login.signinTitle=请登录
|
||||
login.ssoSignIn=通过单点登录登录
|
||||
login.oauth2AutoCreateDisabled=OAUTH2自动创建用户已禁用
|
||||
login.oauth2RequestNotFound=Authorization request not found
|
||||
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
|
||||
login.oauth2invalidRequest=Invalid Request
|
||||
login.oauth2AccessDenied=Access Denied
|
||||
login.oauth2InvalidTokenResponse=Invalid Token Response
|
||||
|
||||
|
||||
#auto-redact
|
||||
|
@ -452,6 +452,11 @@ login.locked=您的帳戶已被鎖定。
|
||||
login.signinTitle=請登入
|
||||
login.ssoSignIn=透過織網單一簽入
|
||||
login.oauth2AutoCreateDisabled=OAUTH2自動建立使用者已停用
|
||||
login.oauth2RequestNotFound=Authorization request not found
|
||||
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
|
||||
login.oauth2invalidRequest=Invalid Request
|
||||
login.oauth2AccessDenied=Access Denied
|
||||
login.oauth2InvalidTokenResponse=Invalid Token Response
|
||||
|
||||
|
||||
#auto-redact
|
||||
|
@ -19,6 +19,23 @@ security:
|
||||
# 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'
|
||||
# client:
|
||||
# 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
|
||||
# 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
|
||||
# keycloak:
|
||||
# issuer: "http://192.168.0.123:8888/realms/stirling-pdf" # URL of the Keycloak realm's OpenID Connect Discovery endpoint
|
||||
# clientId: "stirling-pdf" # Client ID for Keycloak OAuth2
|
||||
# clientSecret: "" # Client Secret for Keycloak OAuth2
|
||||
# scopes: "openid, profile, email" # Scopes for Keycloak OAuth2
|
||||
# useAsUsername: "email" # Field to use as the username for Keycloak OAuth2
|
||||
|
||||
system:
|
||||
defaultLocale: 'en-US' # Set the default language (e.g. 'de-DE', 'fr-FR', etc)
|
||||
|
@ -13,8 +13,6 @@
|
||||
document.addEventListener('modeChanged', function(e) {
|
||||
var mode = e.detail;
|
||||
|
||||
setInputMode("username", mode);
|
||||
setInputMode("password", mode);
|
||||
document.body.classList.remove("light-mode", "dark-mode", "rainbow-mode"); // remove all mode classes first
|
||||
|
||||
switch (mode) {
|
||||
@ -117,7 +115,7 @@
|
||||
|
||||
<h1 class="h1 mb-3 fw-normal" th:text="${@appName}">Stirling-PDF</h1>
|
||||
<div th:if="${oAuth2Enabled}">
|
||||
<a href="oauth2/authorization/oidc" class="w-100 btn btn-lg btn-primary" th:text="#{login.ssoSignIn}">Login Via SSO</a>
|
||||
<a href="#" class="w-100 btn btn-lg btn-primary" data-bs-toggle="modal" data-bs-target="#loginsModal" th:text="#{login.ssoSignIn}">Login Via SSO</a>
|
||||
<br>
|
||||
<br>
|
||||
<div th:if="${erroroauth}" class="alert alert-danger text-center">
|
||||
@ -126,7 +124,7 @@
|
||||
<hr />
|
||||
</div>
|
||||
|
||||
<div th:if="${error}" class="alert alert-danger text-danger text-center">
|
||||
<div th:if="${error}" class="alert alert-danger text-center">
|
||||
<div th:if="${error}" th:text="#{${error}}">OAuth2: Error Message</div>
|
||||
</div>
|
||||
<div class="text-danger text-center">
|
||||
@ -170,5 +168,27 @@
|
||||
</main>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
<div th:if="${oAuth2Enabled}" class="modal fade" id="loginsModal" tabindex="-1" role="dialog" aria-labelledby="loginsModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="loginsModalLabel" th:text="#{login.ssoSignIn}"></h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
|
||||
<span class="material-symbols-rounded">
|
||||
close
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3" th:each="provider : ${providerlist}">
|
||||
<a th:href="@{|/oauth2/authorization/${provider.key}|}" th:text="${provider.value}" class="w-100 btn btn-lg btn-primary">OpenID Connect</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" th:text="#{close}"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in New Issue
Block a user