From 29602cdb8758c365fe5bc01e7c2d0bd440e4ad88 Mon Sep 17 00:00:00 2001 From: Celestino Bellone Date: Sat, 17 Apr 2021 17:59:48 +0200 Subject: [PATCH] working openid login --- .../java/alfio/config/WebSecurityConfig.java | 51 ++++- .../AbstractFormBasedWebSecurity.java | 40 ++-- .../authentication/FormBasedWebSecurity.java | 15 +- .../OpenIdAdminWebSecurity.java | 50 ++--- .../OpenIdPublicWebSecurity.java | 69 ------- .../OpenIdAdminCallbackLoginFilter.java | 165 ---------------- ...r.java => OpenIdAuthenticationFilter.java} | 20 +- .../support/OpenIdCallbackLoginFilter.java | 72 +++++++ .../OpenIdPublicAuthenticationFilter.java | 6 +- .../OpenIdPublicCallbackLoginFilter.java | 5 +- .../java/alfio/extension/JSErrorReporter.java | 3 +- .../AdminOpenIdAuthenticationManager.java | 44 ++++- .../BaseOpenIdAuthenticationManager.java | 176 ++++++++++++++---- .../openid/OpenIdAuthenticationManager.java | 35 ++++ .../manager/openid/OpenIdConfiguration.java | 4 +- .../PublicOpenIdAuthenticationManager.java | 51 ++++- .../java/alfio/manager/user/UserManager.java | 3 +- src/main/java/alfio/model/user/User.java | 2 +- .../repository/user/AuthorityRepository.java | 8 +- .../user/join/UserOrganizationRepository.java | 9 +- .../java/alfio/util/PasswordGenerator.java | 2 +- .../admin/partials/configuration/system.html | 10 + .../AdminOpenIdAuthenticationManagerTest.java | 12 +- 23 files changed, 487 insertions(+), 365 deletions(-) delete mode 100644 src/main/java/alfio/config/authentication/OpenIdPublicWebSecurity.java delete mode 100644 src/main/java/alfio/config/authentication/support/OpenIdAdminCallbackLoginFilter.java rename src/main/java/alfio/config/authentication/support/{OpenIdAdminAuthenticationFilter.java => OpenIdAuthenticationFilter.java} (72%) create mode 100644 src/main/java/alfio/config/authentication/support/OpenIdCallbackLoginFilter.java create mode 100644 src/main/java/alfio/manager/openid/OpenIdAuthenticationManager.java diff --git a/src/main/java/alfio/config/WebSecurityConfig.java b/src/main/java/alfio/config/WebSecurityConfig.java index a9433ebbe6..0e3c0eb12e 100644 --- a/src/main/java/alfio/config/WebSecurityConfig.java +++ b/src/main/java/alfio/config/WebSecurityConfig.java @@ -19,12 +19,20 @@ import alfio.manager.openid.AdminOpenIdAuthenticationManager; import alfio.manager.openid.PublicOpenIdAuthenticationManager; import alfio.manager.system.ConfigurationManager; +import alfio.manager.user.UserManager; +import alfio.repository.user.AuthorityRepository; +import alfio.repository.user.OrganizationRepository; +import alfio.repository.user.UserRepository; +import alfio.repository.user.join.UserOrganizationRepository; +import alfio.util.Json; import lombok.extern.log4j.Log4j2; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; import org.springframework.core.env.Environment; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.csrf.CsrfTokenRepository; import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository; @@ -50,14 +58,49 @@ public CsrfTokenRepository getCsrfTokenRepository() { @Profile("openid") public AdminOpenIdAuthenticationManager adminOpenIdAuthenticationManager(Environment environment, HttpClient httpClient, - ConfigurationManager configurationManager) { - return new AdminOpenIdAuthenticationManager(environment, httpClient, configurationManager); + ConfigurationManager configurationManager, + UserManager userManager, + UserRepository userRepository, + AuthorityRepository authorityRepository, + OrganizationRepository organizationRepository, + UserOrganizationRepository userOrganizationRepository, + NamedParameterJdbcTemplate jdbcTemplate, + PasswordEncoder passwordEncoder, + Json json) { + return new AdminOpenIdAuthenticationManager(environment, + httpClient, + configurationManager, + userManager, + userRepository, + authorityRepository, + organizationRepository, + userOrganizationRepository, + jdbcTemplate, + passwordEncoder, + json); } @Bean public PublicOpenIdAuthenticationManager publicOpenIdAuthenticationManager(HttpClient httpClient, - ConfigurationManager configurationManager) { - return new PublicOpenIdAuthenticationManager(httpClient, configurationManager); + ConfigurationManager configurationManager, + UserManager userManager, + UserRepository userRepository, + AuthorityRepository authorityRepository, + OrganizationRepository organizationRepository, + UserOrganizationRepository userOrganizationRepository, + NamedParameterJdbcTemplate jdbcTemplate, + PasswordEncoder passwordEncoder, + Json json) { + return new PublicOpenIdAuthenticationManager(httpClient, + configurationManager, + userManager, + userRepository, + authorityRepository, + organizationRepository, + userOrganizationRepository, + jdbcTemplate, + passwordEncoder, + json); } } diff --git a/src/main/java/alfio/config/authentication/AbstractFormBasedWebSecurity.java b/src/main/java/alfio/config/authentication/AbstractFormBasedWebSecurity.java index 50e120df12..0942345592 100644 --- a/src/main/java/alfio/config/authentication/AbstractFormBasedWebSecurity.java +++ b/src/main/java/alfio/config/authentication/AbstractFormBasedWebSecurity.java @@ -17,9 +17,10 @@ package alfio.config.authentication; import alfio.config.Initializer; -import alfio.config.authentication.support.RecaptchaLoginFilter; -import alfio.config.authentication.support.UserCreatorBeforeLoginFilter; +import alfio.config.authentication.support.*; import alfio.manager.RecaptchaService; +import alfio.manager.openid.OpenIdAuthenticationManager; +import alfio.manager.openid.PublicOpenIdAuthenticationManager; import alfio.manager.system.ConfigurationManager; import alfio.manager.user.UserManager; import lombok.AllArgsConstructor; @@ -31,9 +32,11 @@ import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer; import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.authentication.AnonymousAuthenticationFilter; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.csrf.CsrfToken; import org.springframework.security.web.csrf.CsrfTokenRepository; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.NegatedRequestMatcher; import org.springframework.security.web.util.matcher.RequestHeaderRequestMatcher; @@ -56,23 +59,16 @@ abstract class AbstractFormBasedWebSecurity extends WebSecurityConfigurerAdapter private final CsrfTokenRepository csrfTokenRepository; private final DataSource dataSource; private final PasswordEncoder passwordEncoder; + private final PublicOpenIdAuthenticationManager publicOpenIdAuthenticationManager; @Override public void configure(AuthenticationManagerBuilder auth) throws Exception { auth.jdbcAuthentication().dataSource(dataSource) .usersByUsernameQuery("select username, password, enabled from ba_user where username = ?") .authoritiesByUsernameQuery("select username, role from authority where username = ?") - .passwordEncoder(passwordEncoder); - // call implementation-specific logic - customizeAuthenticationManager(auth); - } - - /** - * By using this method, implementations can customize the AuthenticationManager configuration - * - * @param auth - */ - protected void customizeAuthenticationManager(AuthenticationManagerBuilder auth) { + .passwordEncoder(passwordEncoder) + .and() + .authenticationProvider(new OpenIdAuthenticationProvider()); } @Override @@ -155,6 +151,9 @@ protected void configure(HttpSecurity http) throws Exception { .failureUrl("/authentication?failed") .and().logout().permitAll(); + http.addFilterBefore(openIdPublicCallbackLoginFilter(publicOpenIdAuthenticationManager), UsernamePasswordAuthenticationFilter.class) + .addFilterBefore(openIdPublicAuthenticationFilter(publicOpenIdAuthenticationManager), AnonymousAuthenticationFilter.class); + // http.addFilterBefore(new RecaptchaLoginFilter(recaptchaService, "/authenticate", "/authentication?recaptchaFailed", configurationManager), UsernamePasswordAuthenticationFilter.class); @@ -194,15 +193,14 @@ protected void configure(HttpSecurity http) throws Exception { protected void addAdditionalFilters(HttpSecurity http) throws Exception { } - protected UserManager getUserManager() { - return userManager; - } - - protected PasswordEncoder getPasswordEncoder() { - return passwordEncoder; + private OpenIdAuthenticationFilter openIdPublicAuthenticationFilter(OpenIdAuthenticationManager openIdAuthenticationManager) { + return new OpenIdAuthenticationFilter("/openid/authentication", openIdAuthenticationManager, "/"); } - protected Environment getEnvironment() { - return environment; + private OpenIdCallbackLoginFilter openIdPublicCallbackLoginFilter(OpenIdAuthenticationManager openIdAuthenticationManager) throws Exception { + // configurationManager + return new OpenIdCallbackLoginFilter(openIdAuthenticationManager, + new AntPathRequestMatcher("/openid/callback", "GET"), + authenticationManager()); } } diff --git a/src/main/java/alfio/config/authentication/FormBasedWebSecurity.java b/src/main/java/alfio/config/authentication/FormBasedWebSecurity.java index bef425af07..0dc5797dc3 100644 --- a/src/main/java/alfio/config/authentication/FormBasedWebSecurity.java +++ b/src/main/java/alfio/config/authentication/FormBasedWebSecurity.java @@ -17,6 +17,7 @@ package alfio.config.authentication; import alfio.manager.RecaptchaService; +import alfio.manager.openid.PublicOpenIdAuthenticationManager; import alfio.manager.system.ConfigurationManager; import alfio.manager.user.UserManager; import org.springframework.context.annotation.Configuration; @@ -33,7 +34,7 @@ */ @Profile("!openid") @Configuration -@Order(3) +@Order(1) public class FormBasedWebSecurity extends AbstractFormBasedWebSecurity { public FormBasedWebSecurity(Environment environment, UserManager userManager, @@ -41,7 +42,15 @@ public FormBasedWebSecurity(Environment environment, ConfigurationManager configurationManager, CsrfTokenRepository csrfTokenRepository, DataSource dataSource, - PasswordEncoder passwordEncoder) { - super(environment, userManager, recaptchaService, configurationManager, csrfTokenRepository, dataSource, passwordEncoder); + PasswordEncoder passwordEncoder, + PublicOpenIdAuthenticationManager publicOpenIdAuthenticationManager) { + super(environment, + userManager, + recaptchaService, + configurationManager, + csrfTokenRepository, + dataSource, + passwordEncoder, + publicOpenIdAuthenticationManager); } } diff --git a/src/main/java/alfio/config/authentication/OpenIdAdminWebSecurity.java b/src/main/java/alfio/config/authentication/OpenIdAdminWebSecurity.java index 66a9f4dc0a..d967e82f54 100644 --- a/src/main/java/alfio/config/authentication/OpenIdAdminWebSecurity.java +++ b/src/main/java/alfio/config/authentication/OpenIdAdminWebSecurity.java @@ -16,24 +16,19 @@ */ package alfio.config.authentication; -import alfio.config.authentication.support.OpenIdAdminAuthenticationFilter; -import alfio.config.authentication.support.OpenIdAdminCallbackLoginFilter; -import alfio.config.authentication.support.OpenIdAuthenticationProvider; +import alfio.config.authentication.support.OpenIdAuthenticationFilter; +import alfio.config.authentication.support.OpenIdCallbackLoginFilter; import alfio.config.authentication.support.OpenIdPublicAuthenticationFilter; import alfio.manager.RecaptchaService; import alfio.manager.openid.AdminOpenIdAuthenticationManager; +import alfio.manager.openid.PublicOpenIdAuthenticationManager; import alfio.manager.system.ConfigurationManager; import alfio.manager.user.UserManager; -import alfio.repository.user.AuthorityRepository; -import alfio.repository.user.OrganizationRepository; -import alfio.repository.user.UserRepository; -import alfio.repository.user.join.UserOrganizationRepository; import lombok.extern.log4j.Log4j2; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; import org.springframework.core.annotation.Order; import org.springframework.core.env.Environment; -import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @@ -49,10 +44,6 @@ public class OpenIdAdminWebSecurity extends AbstractFormBasedWebSecurity { private final AdminOpenIdAuthenticationManager adminOpenIdAuthenticationManager; - private final UserRepository userRepository; - private final AuthorityRepository authorityRepository; - private final UserOrganizationRepository userOrganizationRepository; - private final OrganizationRepository organizationRepository; public OpenIdAdminWebSecurity(Environment environment, UserManager userManager, @@ -62,36 +53,25 @@ public OpenIdAdminWebSecurity(Environment environment, DataSource dataSource, PasswordEncoder passwordEncoder, AdminOpenIdAuthenticationManager adminOpenIdAuthenticationManager, - UserRepository userRepository, - AuthorityRepository authorityRepository, - UserOrganizationRepository userOrganizationRepository, - OrganizationRepository organizationRepository) { - super(environment, userManager, recaptchaService, configurationManager, csrfTokenRepository, dataSource, passwordEncoder); + PublicOpenIdAuthenticationManager openIdAuthenticationManager) { + super(environment, + userManager, + recaptchaService, + configurationManager, + csrfTokenRepository, + dataSource, + passwordEncoder, + openIdAuthenticationManager); this.adminOpenIdAuthenticationManager = adminOpenIdAuthenticationManager; - this.userRepository = userRepository; - this.authorityRepository = authorityRepository; - this.userOrganizationRepository = userOrganizationRepository; - this.organizationRepository = organizationRepository; - } - - @Override - protected void customizeAuthenticationManager(AuthenticationManagerBuilder auth) { - auth.authenticationProvider(new OpenIdAuthenticationProvider()); } @Override protected void addAdditionalFilters(HttpSecurity http) throws Exception { - var callbackLoginFilter = new OpenIdAdminCallbackLoginFilter(adminOpenIdAuthenticationManager, + var callbackLoginFilter = new OpenIdCallbackLoginFilter(adminOpenIdAuthenticationManager, new AntPathRequestMatcher("/callback", "GET"), - authenticationManager(), - userRepository, - authorityRepository, - getPasswordEncoder(), - getUserManager(), - userOrganizationRepository, - organizationRepository); + authenticationManager()); http.addFilterBefore(callbackLoginFilter, UsernamePasswordAuthenticationFilter.class); log.trace("adding openid filter"); - http.addFilterAfter(new OpenIdAdminAuthenticationFilter("/authentication", adminOpenIdAuthenticationManager), OpenIdPublicAuthenticationFilter.class); + http.addFilterAfter(new OpenIdAuthenticationFilter("/authentication", adminOpenIdAuthenticationManager, "/"), OpenIdPublicAuthenticationFilter.class); } } diff --git a/src/main/java/alfio/config/authentication/OpenIdPublicWebSecurity.java b/src/main/java/alfio/config/authentication/OpenIdPublicWebSecurity.java deleted file mode 100644 index 9431d818c1..0000000000 --- a/src/main/java/alfio/config/authentication/OpenIdPublicWebSecurity.java +++ /dev/null @@ -1,69 +0,0 @@ -/** - * This file is part of alf.io. - * - * alf.io is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * alf.io is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with alf.io. If not, see . - */ -package alfio.config.authentication; - -import alfio.config.authentication.support.OpenIdAuthenticationProvider; -import alfio.config.authentication.support.OpenIdPublicAuthenticationFilter; -import alfio.config.authentication.support.OpenIdPublicCallbackLoginFilter; -import alfio.config.authentication.support.RecaptchaLoginFilter; -import alfio.manager.openid.PublicOpenIdAuthenticationManager; -import alfio.manager.system.ConfigurationManager; -import alfio.repository.user.AuthorityRepository; -import alfio.repository.user.OrganizationRepository; -import alfio.repository.user.UserRepository; -import alfio.repository.user.join.UserOrganizationRepository; -import lombok.extern.log4j.Log4j2; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.annotation.Order; -import org.springframework.core.env.Environment; -import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; - -@Configuration -@Order(2) -@Log4j2 -public class OpenIdPublicWebSecurity extends WebSecurityConfigurerAdapter { - - private final PublicOpenIdAuthenticationManager openIdAuthenticationManager; - private final ConfigurationManager configurationManager; - - public OpenIdPublicWebSecurity(Environment environment, - ConfigurationManager configurationManager, - PublicOpenIdAuthenticationManager openIdAuthenticationManager, - UserRepository userRepository, - AuthorityRepository authorityRepository, - UserOrganizationRepository userOrganizationRepository, - OrganizationRepository organizationRepository) { - this.openIdAuthenticationManager = openIdAuthenticationManager; - this.configurationManager = configurationManager; - } - - @Override - public void configure(AuthenticationManagerBuilder auth) throws Exception { - auth.authenticationProvider(new OpenIdAuthenticationProvider()); - } - - @Override - protected void configure(HttpSecurity http) throws Exception { - var callbackLoginFilter = new OpenIdPublicCallbackLoginFilter(configurationManager); - http.addFilterBefore(callbackLoginFilter, UsernamePasswordAuthenticationFilter.class); - log.trace("adding openid filter"); - http.addFilterBefore(new OpenIdPublicAuthenticationFilter(openIdAuthenticationManager), RecaptchaLoginFilter.class); - } -} diff --git a/src/main/java/alfio/config/authentication/support/OpenIdAdminCallbackLoginFilter.java b/src/main/java/alfio/config/authentication/support/OpenIdAdminCallbackLoginFilter.java deleted file mode 100644 index 713636b5f2..0000000000 --- a/src/main/java/alfio/config/authentication/support/OpenIdAdminCallbackLoginFilter.java +++ /dev/null @@ -1,165 +0,0 @@ -/** - * This file is part of alf.io. - * - * alf.io is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * alf.io is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with alf.io. If not, see . - */ -package alfio.config.authentication.support; - -import alfio.manager.openid.AdminOpenIdAuthenticationManager; -import alfio.manager.user.UserManager; -import alfio.model.user.Organization; -import alfio.model.user.Role; -import alfio.model.user.User; -import alfio.repository.user.AuthorityRepository; -import alfio.repository.user.OrganizationRepository; -import alfio.repository.user.UserRepository; -import alfio.repository.user.join.UserOrganizationRepository; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; -import org.springframework.security.web.util.matcher.AntPathRequestMatcher; -import org.springframework.security.web.util.matcher.RequestMatcher; - -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; - -public class OpenIdAdminCallbackLoginFilter extends AbstractAuthenticationProcessingFilter { - - private final RequestMatcher requestMatcher; - private final UserRepository userRepository; - private final AuthorityRepository authorityRepository; - private final PasswordEncoder passwordEncoder; - private final UserManager userManager; - private final UserOrganizationRepository userOrganizationRepository; - private final OrganizationRepository organizationRepository; - private final AdminOpenIdAuthenticationManager adminOpenIdAuthenticationManager; - - public OpenIdAdminCallbackLoginFilter(AdminOpenIdAuthenticationManager adminOpenIdAuthenticationManager, - AntPathRequestMatcher requestMatcher, - AuthenticationManager authenticationManager, - UserRepository userRepository, - AuthorityRepository authorityRepository, - PasswordEncoder passwordEncoder, - UserManager userManager, - UserOrganizationRepository userOrganizationRepository, - OrganizationRepository organizationRepository) { - super(requestMatcher); - this.setAuthenticationManager(authenticationManager); - this.userRepository = userRepository; - this.authorityRepository = authorityRepository; - this.passwordEncoder = passwordEncoder; - this.userManager = userManager; - this.userOrganizationRepository = userOrganizationRepository; - this.organizationRepository = organizationRepository; - this.requestMatcher = requestMatcher; - this.adminOpenIdAuthenticationManager = adminOpenIdAuthenticationManager; - } - - @Override - public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { - HttpServletRequest req = (HttpServletRequest) request; - HttpServletResponse res = (HttpServletResponse) response; - - if (requestMatcher.matches(req)) { - super.doFilter(req, res, chain); - } - - chain.doFilter(request, response); - } - - @Override - public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException { - String code = request.getParameter("code"); - if (code == null) { - logger.warn("Error: authorization code is null"); - throw new IllegalArgumentException("authorization code cannot be null"); - } - logger.trace("Received code. Attempting to exchange it with an access Token"); - OpenIdAlfioUser alfioUser = adminOpenIdAuthenticationManager.retrieveUserInfo(code); - - logger.trace("Got user info: "+alfioUser); - if (!userManager.usernameExists(alfioUser.getEmail())) { - createUser(alfioUser); - } - updateRoles(alfioUser.getAlfioRoles(), alfioUser.getEmail()); - updateOrganizations(alfioUser, response); - - List authorities = alfioUser.getAlfioRoles().stream().map(Role::getRoleName) - .map(SimpleGrantedAuthority::new).collect(Collectors.toList()); - OpenIdAlfioAuthentication authentication = new OpenIdAlfioAuthentication(authorities, alfioUser.getIdToken(), alfioUser.getSubject(), alfioUser.getEmail(), adminOpenIdAuthenticationManager.buildLogoutUrl()); - return getAuthenticationManager().authenticate(authentication); - } - - private void updateOrganizations(OpenIdAlfioUser alfioUser, HttpServletResponse response) throws IOException { - Optional userId = userRepository.findIdByUserName(alfioUser.getEmail()); - if (userId.isEmpty()) { - logger.error("Error: user not saved into the database"); - response.sendRedirect(adminOpenIdAuthenticationManager.buildLogoutUrl()); - return; - } - - Set databaseOrganizationIds = organizationRepository.findAllForUser(alfioUser.getEmail()).stream() - .map(Organization::getId).collect(Collectors.toSet()); - - if (alfioUser.isAdmin()) { - databaseOrganizationIds.forEach(orgId -> userOrganizationRepository.removeOrganizationUserLink(userId.get(), orgId)); - return; - } - - Set organizationIds = alfioUser.getAlfioOrganizationAuthorizations().keySet().stream() - .map(organizationRepository::findByNameOpenId) - .filter(Optional::isPresent) - .map(Optional::get) - .filter(Objects::nonNull) - .map(Organization::getId) - .collect(Collectors.toSet()); - - databaseOrganizationIds.stream() - .filter(orgId -> !organizationIds.contains(orgId)) - .forEach(orgId -> userOrganizationRepository.removeOrganizationUserLink(userId.get(), orgId)); - - if (organizationIds.isEmpty()) { - String message = "Error: The user needs to be ADMIN or to have at least one organization linked"; - logger.error(message); - response.sendRedirect(adminOpenIdAuthenticationManager.buildLogoutUrl()); - } - - organizationIds.stream().filter(orgId -> !databaseOrganizationIds.contains(orgId)) - .forEach(orgId -> userOrganizationRepository.create(userId.get(), orgId)); - } - - private void createUser(OpenIdAlfioUser user) { - userRepository.create(user.getEmail(), passwordEncoder.encode(user.getSubject()), user.getEmail(), user.getEmail(), user.getEmail(), true, User.Type.INTERNAL, null, null); - } - - private void updateRoles(Set roles, String username) { - authorityRepository.revokeAll(username); - roles.forEach(role -> authorityRepository.create(username, role.getRoleName())); - } - -} diff --git a/src/main/java/alfio/config/authentication/support/OpenIdAdminAuthenticationFilter.java b/src/main/java/alfio/config/authentication/support/OpenIdAuthenticationFilter.java similarity index 72% rename from src/main/java/alfio/config/authentication/support/OpenIdAdminAuthenticationFilter.java rename to src/main/java/alfio/config/authentication/support/OpenIdAuthenticationFilter.java index 48566a58a4..ffae6a7c18 100644 --- a/src/main/java/alfio/config/authentication/support/OpenIdAdminAuthenticationFilter.java +++ b/src/main/java/alfio/config/authentication/support/OpenIdAuthenticationFilter.java @@ -16,7 +16,7 @@ */ package alfio.config.authentication.support; -import alfio.manager.openid.AdminOpenIdAuthenticationManager; +import alfio.manager.openid.OpenIdAuthenticationManager; import lombok.extern.log4j.Log4j2; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; @@ -31,13 +31,17 @@ import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Log4j2 -public class OpenIdAdminAuthenticationFilter extends GenericFilterBean { +public class OpenIdAuthenticationFilter extends GenericFilterBean { private final RequestMatcher requestMatcher; - private final AdminOpenIdAuthenticationManager adminOpenIdAuthenticationManager; + private final OpenIdAuthenticationManager openIdAuthenticationManager; + private final String redirectURL; - public OpenIdAdminAuthenticationFilter(String loginURL, AdminOpenIdAuthenticationManager adminOpenIdAuthenticationManager) { + public OpenIdAuthenticationFilter(String loginURL, + OpenIdAuthenticationManager openIdAuthenticationManager, + String redirectURL) { this.requestMatcher = new AntPathRequestMatcher(loginURL, "GET"); - this.adminOpenIdAuthenticationManager = adminOpenIdAuthenticationManager; + this.openIdAuthenticationManager = openIdAuthenticationManager; + this.redirectURL = redirectURL; } @Override @@ -45,13 +49,13 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse res = (HttpServletResponse) response; - if (requestMatcher.matches(req)) { + if (requestMatcher.matches(req) && openIdAuthenticationManager.isEnabled()) { if (SecurityContextHolder.getContext().getAuthentication() != null || req.getParameterMap().containsKey("logout")) { - res.sendRedirect("/admin/"); + res.sendRedirect(redirectURL); return; } log.trace("calling buildAuthorizeUrl"); - res.sendRedirect(adminOpenIdAuthenticationManager.buildAuthorizeUrl()); + res.sendRedirect(openIdAuthenticationManager.buildAuthorizeUrl()); return; } diff --git a/src/main/java/alfio/config/authentication/support/OpenIdCallbackLoginFilter.java b/src/main/java/alfio/config/authentication/support/OpenIdCallbackLoginFilter.java new file mode 100644 index 0000000000..581a5c0b34 --- /dev/null +++ b/src/main/java/alfio/config/authentication/support/OpenIdCallbackLoginFilter.java @@ -0,0 +1,72 @@ +/** + * This file is part of alf.io. + * + * alf.io is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * alf.io is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with alf.io. If not, see . + */ +package alfio.config.authentication.support; + +import alfio.manager.openid.OpenIdAuthenticationManager; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class OpenIdCallbackLoginFilter extends AbstractAuthenticationProcessingFilter { + + private final RequestMatcher requestMatcher; + private final OpenIdAuthenticationManager openIdAuthenticationManager; + + public OpenIdCallbackLoginFilter(OpenIdAuthenticationManager openIdAuthenticationManager, + AntPathRequestMatcher requestMatcher, + AuthenticationManager authenticationManager) { + super(requestMatcher); + this.setAuthenticationManager(authenticationManager); + this.requestMatcher = requestMatcher; + this.openIdAuthenticationManager = openIdAuthenticationManager; + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + HttpServletRequest req = (HttpServletRequest) request; + HttpServletResponse res = (HttpServletResponse) response; + + if (requestMatcher.matches(req)) { + super.doFilter(req, res, chain); + } + + chain.doFilter(request, response); + } + + @Override + public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException { + String code = request.getParameter("code"); + if (code == null) { + logger.warn("Error: authorization code is null"); + throw new IllegalArgumentException("authorization code cannot be null"); + } + logger.trace("Received code. Attempting to exchange it with an access Token"); + var user = openIdAuthenticationManager.authenticateUser(code); + return getAuthenticationManager().authenticate(user); + } +} diff --git a/src/main/java/alfio/config/authentication/support/OpenIdPublicAuthenticationFilter.java b/src/main/java/alfio/config/authentication/support/OpenIdPublicAuthenticationFilter.java index c56f3b2c0a..9eede74c6c 100644 --- a/src/main/java/alfio/config/authentication/support/OpenIdPublicAuthenticationFilter.java +++ b/src/main/java/alfio/config/authentication/support/OpenIdPublicAuthenticationFilter.java @@ -16,6 +16,7 @@ */ package alfio.config.authentication.support; +import alfio.manager.openid.OpenIdAuthenticationManager; import alfio.manager.openid.PublicOpenIdAuthenticationManager; import lombok.extern.log4j.Log4j2; import org.springframework.security.core.context.SecurityContextHolder; @@ -34,10 +35,10 @@ @Log4j2 public class OpenIdPublicAuthenticationFilter extends GenericFilterBean { private final RequestMatcher requestMatcher; - private final PublicOpenIdAuthenticationManager openIdAuthenticationManager; + private final OpenIdAuthenticationManager openIdAuthenticationManager; public OpenIdPublicAuthenticationFilter(PublicOpenIdAuthenticationManager openIdAuthenticationManager) { - this.requestMatcher = new AntPathRequestMatcher("/public/authentication", "GET"); + this.requestMatcher = new AntPathRequestMatcher("/openid/authentication", "GET"); this.openIdAuthenticationManager = openIdAuthenticationManager; } @@ -55,7 +56,6 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha res.sendRedirect(openIdAuthenticationManager.buildAuthorizeUrl()); return; } - chain.doFilter(request, response); } } diff --git a/src/main/java/alfio/config/authentication/support/OpenIdPublicCallbackLoginFilter.java b/src/main/java/alfio/config/authentication/support/OpenIdPublicCallbackLoginFilter.java index 9d87b74a27..1891f868d1 100644 --- a/src/main/java/alfio/config/authentication/support/OpenIdPublicCallbackLoginFilter.java +++ b/src/main/java/alfio/config/authentication/support/OpenIdPublicCallbackLoginFilter.java @@ -35,13 +35,14 @@ public class OpenIdPublicCallbackLoginFilter extends AbstractAuthenticationProce private final ConfigurationManager configurationManager; public OpenIdPublicCallbackLoginFilter(ConfigurationManager configurationManager) { - super(new AntPathRequestMatcher("/public/callback", "GET")); + super(new AntPathRequestMatcher("/openid/callback", "GET")); this.configurationManager = configurationManager; } @Override protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) { - return configurationManager.getFor(OPENID_PUBLIC_ENABLED, ConfigurationLevel.system()).getValueAsBooleanOrDefault(); + return super.requiresAuthentication(request, response) + && configurationManager.getFor(OPENID_PUBLIC_ENABLED, ConfigurationLevel.system()).getValueAsBooleanOrDefault(); } @Override diff --git a/src/main/java/alfio/extension/JSErrorReporter.java b/src/main/java/alfio/extension/JSErrorReporter.java index ebe5d4ca02..aae5e6e666 100644 --- a/src/main/java/alfio/extension/JSErrorReporter.java +++ b/src/main/java/alfio/extension/JSErrorReporter.java @@ -18,7 +18,6 @@ package alfio.extension; import lombok.extern.log4j.Log4j2; -import org.apache.logging.log4j.Logger; import org.mozilla.javascript.ErrorReporter; import org.mozilla.javascript.EvaluatorException; @@ -39,7 +38,7 @@ public void warning(String message, String sourceName, int line, String lineSour @Override public EvaluatorException runtimeError(String message, String sourceName, int line, String lineSource, int lineOffset) { - return null; + return new EvaluatorException(message, sourceName, line, lineSource, lineOffset); } @Override diff --git a/src/main/java/alfio/manager/openid/AdminOpenIdAuthenticationManager.java b/src/main/java/alfio/manager/openid/AdminOpenIdAuthenticationManager.java index bfd148f8d3..512fb2d2bf 100644 --- a/src/main/java/alfio/manager/openid/AdminOpenIdAuthenticationManager.java +++ b/src/main/java/alfio/manager/openid/AdminOpenIdAuthenticationManager.java @@ -18,7 +18,14 @@ import alfio.config.authentication.support.OpenIdAlfioUser; import alfio.manager.system.ConfigurationManager; +import alfio.manager.user.UserManager; import alfio.model.user.Role; +import alfio.model.user.User; +import alfio.repository.user.AuthorityRepository; +import alfio.repository.user.OrganizationRepository; +import alfio.repository.user.UserRepository; +import alfio.repository.user.join.UserOrganizationRepository; +import alfio.util.Json; import com.auth0.jwt.interfaces.Claim; import lombok.SneakyThrows; import lombok.extern.log4j.Log4j2; @@ -26,6 +33,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.env.Environment; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.security.crypto.password.PasswordEncoder; import java.net.http.HttpClient; import java.util.*; @@ -42,8 +51,24 @@ public class AdminOpenIdAuthenticationManager extends BaseOpenIdAuthenticationMa public AdminOpenIdAuthenticationManager(Environment environment, HttpClient httpClient, - ConfigurationManager configurationManager) { - super(httpClient); + ConfigurationManager configurationManager, + UserManager userManager, + UserRepository userRepository, + AuthorityRepository authorityRepository, + OrganizationRepository organizationRepository, + UserOrganizationRepository userOrganizationRepository, + NamedParameterJdbcTemplate jdbcTemplate, + PasswordEncoder passwordEncoder, + Json json) { + super(httpClient, + userManager, + userRepository, + authorityRepository, + organizationRepository, + userOrganizationRepository, + jdbcTemplate, + passwordEncoder, + json); this.configurationContainer = new LazyConfigurationContainer(environment, configurationManager); } @@ -95,6 +120,16 @@ public List getScopes() { return List.of("openid", "email", "profile", openIdConfiguration().getRolesParameter(), openIdConfiguration().getAlfioGroupsParameter()); } + @Override + protected User.Type getUserType() { + return User.Type.INTERNAL; + } + + @Override + protected boolean syncRoles() { + return true; + } + private Map> extractOrganizationRoles(List alfioOrganizationAuthorizationsRaw) { Map> alfioOrganizationAuthorizations = new HashMap<>(); @@ -112,6 +147,11 @@ private Map> extractOrganizationRoles(List alfioOrga return alfioOrganizationAuthorizations; } + @Override + public boolean isEnabled() { + return true; + } + private static class LazyConfigurationContainer extends LazyInitializer { private final Environment environment; diff --git a/src/main/java/alfio/manager/openid/BaseOpenIdAuthenticationManager.java b/src/main/java/alfio/manager/openid/BaseOpenIdAuthenticationManager.java index 036796ff61..9cc35cd3cc 100644 --- a/src/main/java/alfio/manager/openid/BaseOpenIdAuthenticationManager.java +++ b/src/main/java/alfio/manager/openid/BaseOpenIdAuthenticationManager.java @@ -16,15 +16,29 @@ */ package alfio.manager.openid; +import alfio.config.authentication.support.OpenIdAlfioAuthentication; import alfio.config.authentication.support.OpenIdAlfioUser; +import alfio.manager.user.UserManager; +import alfio.model.user.Organization; +import alfio.model.user.Role; +import alfio.model.user.User; +import alfio.repository.user.AuthorityRepository; +import alfio.repository.user.OrganizationRepository; +import alfio.repository.user.UserRepository; +import alfio.repository.user.join.UserOrganizationRepository; import alfio.util.HttpUtils; import alfio.util.Json; +import alfio.util.PasswordGenerator; import com.auth0.jwt.JWT; import com.auth0.jwt.interfaces.Claim; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.log4j.Log4j2; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; @@ -32,26 +46,51 @@ import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; -import java.util.List; -import java.util.Map; +import java.util.*; +import java.util.stream.Collectors; import static alfio.util.HttpUtils.APPLICATION_FORM_URLENCODED; import static alfio.util.HttpUtils.APPLICATION_JSON; @Log4j2 -abstract class BaseOpenIdAuthenticationManager { +abstract class BaseOpenIdAuthenticationManager implements OpenIdAuthenticationManager { protected static final String CODE = "code"; protected static final String ID_TOKEN = "id_token"; protected static final String SUBJECT = "sub"; protected static final String EMAIL = "email"; protected final HttpClient httpClient; + private final UserManager userManager; + private final UserRepository userRepository; + private final AuthorityRepository authorityRepository; + private final OrganizationRepository organizationRepository; + private final UserOrganizationRepository userOrganizationRepository; + private final NamedParameterJdbcTemplate jdbcTemplate; + private final PasswordEncoder passwordEncoder; + private final Json json; - protected BaseOpenIdAuthenticationManager(HttpClient httpClient) { + protected BaseOpenIdAuthenticationManager(HttpClient httpClient, + UserManager userManager, + UserRepository userRepository, + AuthorityRepository authorityRepository, + OrganizationRepository organizationRepository, + UserOrganizationRepository userOrganizationRepository, + NamedParameterJdbcTemplate jdbcTemplate, + PasswordEncoder passwordEncoder, + Json json) { this.httpClient = httpClient; + this.userManager = userManager; + this.userRepository = userRepository; + this.authorityRepository = authorityRepository; + this.organizationRepository = organizationRepository; + this.userOrganizationRepository = userOrganizationRepository; + this.jdbcTemplate = jdbcTemplate; + this.passwordEncoder = passwordEncoder; + this.json = json; } - public final OpenIdAlfioUser retrieveUserInfo(String code) { + @Override + public final OpenIdAlfioAuthentication authenticateUser(String code) { log.trace("Attempting to retrieve Access Token"); var accessTokenResponse = retrieveAccessToken(code); String idToken = (String) accessTokenResponse.get(ID_TOKEN); @@ -60,38 +99,84 @@ public final OpenIdAlfioUser retrieveUserInfo(String code) { String subject = idTokenClaims.get(SUBJECT).asString(); String email = idTokenClaims.get(EMAIL).asString(); - return fromToken(idToken, subject, email, idTokenClaims); + var userInfo = fromToken(idToken, subject, email, idTokenClaims); + return createOrRetrieveUser(userInfo); } - protected abstract OpenIdAlfioUser fromToken(String idToken, String subject, String email, Map claims); + private OpenIdAlfioAuthentication createOrRetrieveUser(OpenIdAlfioUser user) { + if (!userManager.usernameExists(user.getEmail())) { + userRepository.create(user.getEmail(), + passwordEncoder.encode(PasswordGenerator.generateRandomPassword()), + user.getEmail(), + user.getEmail(), + user.getEmail(), + true, + getUserType(), + null, + null); + } - protected abstract OpenIdConfiguration openIdConfiguration(); + if(syncRoles()) { + updateRoles(user.getAlfioRoles(), user.getEmail()); + updateOrganizations(user); + } - protected abstract List getScopes(); + List authorities = user.getAlfioRoles().stream().map(Role::getRoleName) + .map(SimpleGrantedAuthority::new).collect(Collectors.toList()); + return new OpenIdAlfioAuthentication(authorities, user.getIdToken(), user.getSubject(), user.getEmail(), buildLogoutUrl()); + } - private Map retrieveAccessToken(String code){ - try { - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(buildClaimsRetrieverUrl())) - .header("Content-Type", openIdConfiguration().getContentType()) - .POST(HttpRequest.BodyPublishers.ofString(buildRetrieveClaimsUrlBody(code))) - .build(); + private void updateOrganizations(OpenIdAlfioUser alfioUser) { + int userId = userRepository.findIdByUserName(alfioUser.getEmail()).orElseThrow(); + var databaseOrganizationIds = organizationRepository.findAllForUser(alfioUser.getEmail()).stream() + .map(Organization::getId).collect(Collectors.toSet()); - HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); - if(HttpUtils.callSuccessful(response)) { - log.trace("Access Token successfully retrieved"); - return Json.fromJson(response.body(), new TypeReference<>() { - }); - } else { - log.warn("cannot retrieve access token"); - throw new IllegalStateException("cannot retrieve access token. Response from server: " +response.body()); - } - } catch (Exception e) { - log.error("There has been an error retrieving the access token from the idp using the authorization code", e); - throw new RuntimeException(e); + if (alfioUser.isAdmin()) { + userOrganizationRepository.removeOrganizationUserLinks(userId, databaseOrganizationIds); + return; + } + + Set organizationIds = alfioUser.getAlfioOrganizationAuthorizations().keySet().stream() + .map(organizationRepository::findByNameOpenId) + .filter(Optional::isPresent) + .map(Optional::get) + .filter(Objects::nonNull) + .map(Organization::getId) + .collect(Collectors.toSet()); + + var organizationsToUnlink = databaseOrganizationIds.stream() + .filter(orgId -> !organizationIds.contains(orgId)) + .collect(Collectors.toSet()); + + if (!organizationsToUnlink.isEmpty()) { + userOrganizationRepository.removeOrganizationUserLinks(userId, organizationsToUnlink); + } + + if (organizationIds.isEmpty()) { + throw new IllegalStateException("The user needs to be ADMIN or have at least one organization linked"); } + + var params = organizationIds.stream().filter(orgId -> !databaseOrganizationIds.contains(orgId)) + .map(id -> new MapSqlParameterSource("userId", userId).addValue("organizationId", id)) + .toArray(MapSqlParameterSource[]::new); + jdbcTemplate.batchUpdate(userOrganizationRepository.bulkCreate(), params); } + private void updateRoles(Set roles, String username) { + authorityRepository.revokeAll(username); + var rolesToAdd = roles.stream() + .map(r -> new MapSqlParameterSource("username", username).addValue("role", r)) + .toArray(MapSqlParameterSource[]::new); + jdbcTemplate.batchUpdate(authorityRepository.grantAll(), rolesToAdd); + } + + protected abstract OpenIdAlfioUser fromToken(String idToken, String subject, String email, Map claims); + protected abstract OpenIdConfiguration openIdConfiguration(); + protected abstract List getScopes(); + protected abstract User.Type getUserType(); + protected abstract boolean syncRoles(); + + @Override public String buildAuthorizeUrl() { log.trace("buildAuthorizeUrl, configuration: {}", this::openIdConfiguration); String scopeParameter = String.join("+", getScopes()); @@ -108,6 +193,7 @@ public String buildAuthorizeUrl() { return uri.toUriString(); } + @Override public String buildClaimsRetrieverUrl() { UriComponents uri = UriComponentsBuilder.newInstance() .scheme("https") @@ -117,6 +203,7 @@ public String buildClaimsRetrieverUrl() { return uri.toUriString(); } + @Override public String buildLogoutUrl() { UriComponents uri = UriComponentsBuilder.newInstance() .scheme("https") @@ -127,6 +214,7 @@ public String buildLogoutUrl() { return uri.toString(); } + @Override public String buildRetrieveClaimsUrlBody(String code) throws JsonProcessingException { var contentType = openIdConfiguration().getContentType(); if (contentType.equals(APPLICATION_JSON)) { @@ -138,7 +226,7 @@ public String buildRetrieveClaimsUrlBody(String code) throws JsonProcessingExcep throw new RuntimeException("the Content-Type specified is not supported"); } - private String buildAccessTokenUrlJson(String code) throws JsonProcessingException { + private String buildAccessTokenUrlJson(String code) { Map body = Map.of( "grant_type", "authorization_code", "code", code, @@ -146,10 +234,7 @@ private String buildAccessTokenUrlJson(String code) throws JsonProcessingExcepti "client_secret", openIdConfiguration().getClientSecret(), "redirect_uri", openIdConfiguration().getCallbackURI() ); - - ObjectMapper objectMapper = new ObjectMapper(); - - return objectMapper.writeValueAsString(body); + return json.asJsonString(body); } private String buildAccessTokenUrlForm(String code) { @@ -159,4 +244,27 @@ private String buildAccessTokenUrlForm(String code) { "&client_secret=" + openIdConfiguration().getClientSecret() + "&redirect_uri=" + openIdConfiguration().getCallbackURI(); } + + private Map retrieveAccessToken(String code){ + try { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(buildClaimsRetrieverUrl())) + .header("Content-Type", openIdConfiguration().getContentType()) + .POST(HttpRequest.BodyPublishers.ofString(buildRetrieveClaimsUrlBody(code))) + .build(); + + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + if(HttpUtils.callSuccessful(response)) { + log.trace("Access Token successfully retrieved"); + return Json.fromJson(response.body(), new TypeReference<>() { + }); + } else { + log.warn("cannot retrieve access token"); + throw new IllegalStateException("cannot retrieve access token. Response from server: " +response.body()); + } + } catch (Exception e) { + log.error("There has been an error retrieving the access token from the idp using the authorization code", e); + throw new RuntimeException(e); + } + } } diff --git a/src/main/java/alfio/manager/openid/OpenIdAuthenticationManager.java b/src/main/java/alfio/manager/openid/OpenIdAuthenticationManager.java new file mode 100644 index 0000000000..44bb55eb13 --- /dev/null +++ b/src/main/java/alfio/manager/openid/OpenIdAuthenticationManager.java @@ -0,0 +1,35 @@ +/** + * This file is part of alf.io. + * + * alf.io is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * alf.io is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with alf.io. If not, see . + */ +package alfio.manager.openid; + +import alfio.config.authentication.support.OpenIdAlfioAuthentication; +import com.fasterxml.jackson.core.JsonProcessingException; + +public interface OpenIdAuthenticationManager { + + boolean isEnabled(); + + OpenIdAlfioAuthentication authenticateUser(String code); + + String buildAuthorizeUrl(); + + String buildClaimsRetrieverUrl(); + + String buildLogoutUrl(); + + String buildRetrieveClaimsUrlBody(String code) throws JsonProcessingException; +} diff --git a/src/main/java/alfio/manager/openid/OpenIdConfiguration.java b/src/main/java/alfio/manager/openid/OpenIdConfiguration.java index ed37649456..43c36a39f4 100644 --- a/src/main/java/alfio/manager/openid/OpenIdConfiguration.java +++ b/src/main/java/alfio/manager/openid/OpenIdConfiguration.java @@ -25,6 +25,8 @@ import lombok.Setter; import org.springframework.core.env.Environment; +import java.util.Objects; + import static org.apache.commons.lang3.StringUtils.isNotBlank; import static org.apache.commons.lang3.StringUtils.removeEnd; @@ -61,7 +63,7 @@ public OpenIdConfiguration(@JsonProperty("domain") String domain, this.callbackURI = callbackURI; this.authenticationUrl = authenticationUrl; this.tokenEndpoint = tokenEndpoint; - this.contentType = contentType; + this.contentType = Objects.requireNonNullElse(contentType, "application/x-www-form-urlencoded"); this.rolesParameter = rolesParameter; this.alfioGroupsParameter = alfioGroupsParameter; this.logoutUrl = logoutUrl; diff --git a/src/main/java/alfio/manager/openid/PublicOpenIdAuthenticationManager.java b/src/main/java/alfio/manager/openid/PublicOpenIdAuthenticationManager.java index 8c1a81f01d..3f18324879 100644 --- a/src/main/java/alfio/manager/openid/PublicOpenIdAuthenticationManager.java +++ b/src/main/java/alfio/manager/openid/PublicOpenIdAuthenticationManager.java @@ -19,23 +19,48 @@ import alfio.config.authentication.support.OpenIdAlfioUser; import alfio.manager.system.ConfigurationLevel; import alfio.manager.system.ConfigurationManager; +import alfio.manager.user.UserManager; +import alfio.model.system.ConfigurationKeys; +import alfio.model.user.User; +import alfio.repository.user.AuthorityRepository; +import alfio.repository.user.OrganizationRepository; +import alfio.repository.user.UserRepository; +import alfio.repository.user.join.UserOrganizationRepository; import alfio.util.Json; import com.auth0.jwt.interfaces.Claim; +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.security.crypto.password.PasswordEncoder; import java.net.http.HttpClient; +import java.time.Duration; +import java.util.EnumSet; import java.util.List; import java.util.Map; import java.util.Set; import static alfio.model.system.ConfigurationKeys.OPENID_CONFIGURATION_JSON; +import static alfio.model.system.ConfigurationKeys.OPENID_PUBLIC_ENABLED; public class PublicOpenIdAuthenticationManager extends BaseOpenIdAuthenticationManager { private final ConfigurationManager configurationManager; + private final Cache, Map> activeCache = Caffeine.newBuilder() + .expireAfterWrite(Duration.ofMinutes(1)) + .build(); public PublicOpenIdAuthenticationManager(HttpClient httpClient, - ConfigurationManager configurationManager) { - super(httpClient); + ConfigurationManager configurationManager, + UserManager userManager, + UserRepository userRepository, + AuthorityRepository authorityRepository, + OrganizationRepository organizationRepository, + UserOrganizationRepository userOrganizationRepository, + NamedParameterJdbcTemplate jdbcTemplate, + PasswordEncoder passwordEncoder, + Json json) { + super(httpClient, userManager, userRepository, authorityRepository, organizationRepository, userOrganizationRepository, jdbcTemplate, passwordEncoder, json); this.configurationManager = configurationManager; } @@ -47,11 +72,31 @@ protected OpenIdAlfioUser fromToken(String idToken, String subject, String email @Override protected OpenIdConfiguration openIdConfiguration() { - return Json.fromJson(configurationManager.getFor(OPENID_CONFIGURATION_JSON, ConfigurationLevel.system()).getValueOrNull(), OpenIdConfiguration.class); + return Json.fromJson(loadCachedConfiguration().get(OPENID_CONFIGURATION_JSON).getValueOrNull(), OpenIdConfiguration.class); } @Override protected List getScopes() { return List.of("openid", "email", "profile"); } + + @Override + protected User.Type getUserType() { + return User.Type.PUBLIC; + } + + @Override + protected boolean syncRoles() { + return false; + } + + @Override + public boolean isEnabled() { + return loadCachedConfiguration().get(OPENID_PUBLIC_ENABLED).getValueAsBooleanOrDefault(); + } + + private Map loadCachedConfiguration() { + return activeCache.get(EnumSet.of(OPENID_PUBLIC_ENABLED, OPENID_CONFIGURATION_JSON), + k -> configurationManager.getFor(k, ConfigurationLevel.system())); + } } diff --git a/src/main/java/alfio/manager/user/UserManager.java b/src/main/java/alfio/manager/user/UserManager.java index bdf1787a14..9e145047b3 100644 --- a/src/main/java/alfio/manager/user/UserManager.java +++ b/src/main/java/alfio/manager/user/UserManager.java @@ -245,11 +245,10 @@ public UserWithPassword resetPassword(int userId) { } - public boolean updatePassword(String username, String newPassword) { + public void updatePassword(String username, String newPassword) { User user = userRepository.findByUsername(username).orElseThrow(IllegalStateException::new); Validate.isTrue(PasswordGenerator.isValid(newPassword), "invalid password"); Validate.isTrue(userRepository.resetPassword(user.getId(), passwordEncoder.encode(newPassword)) == 1, "error during password update"); - return true; } diff --git a/src/main/java/alfio/model/user/User.java b/src/main/java/alfio/model/user/User.java index 197e9f59da..5386462aee 100644 --- a/src/main/java/alfio/model/user/User.java +++ b/src/main/java/alfio/model/user/User.java @@ -26,7 +26,7 @@ public class User implements Serializable { public enum Type { - INTERNAL, DEMO, API_KEY + INTERNAL, DEMO, API_KEY, PUBLIC } private final int id; diff --git a/src/main/java/alfio/repository/user/AuthorityRepository.java b/src/main/java/alfio/repository/user/AuthorityRepository.java index 8db4f0e9e4..e43cee25e7 100644 --- a/src/main/java/alfio/repository/user/AuthorityRepository.java +++ b/src/main/java/alfio/repository/user/AuthorityRepository.java @@ -17,7 +17,10 @@ package alfio.repository.user; import alfio.model.user.Authority; -import ch.digitalfondue.npjt.*; +import ch.digitalfondue.npjt.Bind; +import ch.digitalfondue.npjt.Query; +import ch.digitalfondue.npjt.QueryRepository; +import ch.digitalfondue.npjt.QueryType; import java.util.List; import java.util.Set; @@ -38,6 +41,9 @@ public interface AuthorityRepository { @Query("INSERT INTO authority(username, role) VALUES (:username, :role)") int create(@Bind("username") String username, @Bind("role") String role); + @Query(type = QueryType.TEMPLATE, value = "INSERT INTO authority(username, role) VALUES (:username, :role)") + String grantAll(); + @Query("DELETE from authority where username = :username") int revokeAll(@Bind("username") String username); } diff --git a/src/main/java/alfio/repository/user/join/UserOrganizationRepository.java b/src/main/java/alfio/repository/user/join/UserOrganizationRepository.java index 48bfe8033f..e26aedf7a0 100644 --- a/src/main/java/alfio/repository/user/join/UserOrganizationRepository.java +++ b/src/main/java/alfio/repository/user/join/UserOrganizationRepository.java @@ -20,7 +20,9 @@ import ch.digitalfondue.npjt.Bind; import ch.digitalfondue.npjt.Query; import ch.digitalfondue.npjt.QueryRepository; +import ch.digitalfondue.npjt.QueryType; +import java.util.Collection; import java.util.List; @QueryRepository @@ -38,13 +40,16 @@ public interface UserOrganizationRepository { @Query("insert into j_user_organization (user_id, org_id) values(:userId, :organizationId)") int create(@Bind("userId") int userId, @Bind("organizationId") int organizationId); + @Query(type = QueryType.TEMPLATE, value = "insert into j_user_organization (user_id, org_id) values(:userId, :organizationId)") + String bulkCreate(); + @Query("update j_user_organization set org_id = :organizationId where user_id = :userId") int updateUserOrganization(@Bind("userId") int userId, @Bind("organizationId") int organizationId); @Query("select distinct(org_id) from j_user_organization where user_id in(:users)") List findOrganizationsForUsers(@Bind("users") List users); - @Query("delete from j_user_organization where user_id = :userId and org_id = :organizationId") - int removeOrganizationUserLink(@Bind("userId") int userId, @Bind("organizationId") int organizationId); + @Query("delete from j_user_organization where user_id = :userId and org_id in (:organizationIds)") + int removeOrganizationUserLinks(@Bind("userId") int userId, @Bind("organizationIds") Collection organizationIds); } diff --git a/src/main/java/alfio/util/PasswordGenerator.java b/src/main/java/alfio/util/PasswordGenerator.java index b0839d19e1..c5c6da2611 100644 --- a/src/main/java/alfio/util/PasswordGenerator.java +++ b/src/main/java/alfio/util/PasswordGenerator.java @@ -35,7 +35,7 @@ public final class PasswordGenerator { private static final SecureRandom RANDOM = new SecureRandom(); private static final char[] PASSWORD_CHARACTERS; private static final boolean DEV_MODE; - private static final int MAX_LENGTH = 14; + private static final int MAX_LENGTH = 20; private static final int MIN_LENGTH = 10; private static final Pattern VALIDATION_PATTERN; diff --git a/src/main/webapp/resources/angular-templates/admin/partials/configuration/system.html b/src/main/webapp/resources/angular-templates/admin/partials/configuration/system.html index 76c2210b84..4a4e8d8263 100644 --- a/src/main/webapp/resources/angular-templates/admin/partials/configuration/system.html +++ b/src/main/webapp/resources/angular-templates/admin/partials/configuration/system.html @@ -34,6 +34,16 @@

Reservation Process

+ +
+
+ +
+
+