Add OAUTH2 OIDC login support (#1140)
* Somewhat working * Change Autocreate logic * Add OAuth Error Message if Auto create Disabled * Display OAUTH2 username(email) in Account Settings * Disable Change user/pass for Oauth2 user * Hide SSO Button if SSO login Disabled * Remove some spaces and comments * Add OAUTH2 Login example docker-compose file * Add Some Comments * Hide Printing of Client secret * Remove OAUTH2 Beans and replace with applicationProperties * Add conditional annotation to Bean Creation * Update settings.yml.template Add OAUTH2 enabling template. * Update messages_en_GB.properties
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
package stirling.software.SPDF.config.security;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import jakarta.servlet.ServletException;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.session.SessionRegistry;
|
||||
import org.springframework.security.core.session.SessionRegistryImpl;
|
||||
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
|
||||
|
||||
public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler
|
||||
{
|
||||
@Bean
|
||||
public SessionRegistry sessionRegistry() {
|
||||
return new SessionRegistryImpl();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException
|
||||
{
|
||||
HttpSession session = request.getSession(false);
|
||||
if (session != null) {
|
||||
String sessionId = session.getId();
|
||||
sessionRegistry()
|
||||
.removeSessionInformation(
|
||||
sessionId);
|
||||
}
|
||||
|
||||
if(request.getParameter("oauth2AutoCreateDisabled") != null)
|
||||
{
|
||||
response.sendRedirect(request.getContextPath()+"/login?error=oauth2AutoCreateDisabled");
|
||||
}
|
||||
else
|
||||
{
|
||||
response.sendRedirect(request.getContextPath() + "/login?logout=true");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,11 @@
|
||||
package stirling.software.SPDF.config.security;
|
||||
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
@@ -10,20 +14,30 @@ import org.springframework.security.config.annotation.method.configuration.Enabl
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.session.SessionRegistry;
|
||||
import org.springframework.security.core.session.SessionRegistryImpl;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
|
||||
import org.springframework.security.web.savedrequest.NullRequestCache;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistrations;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import stirling.software.SPDF.model.ApplicationProperties;
|
||||
import stirling.software.SPDF.repository.JPATokenRepositoryImpl;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity()
|
||||
@EnableMethodSecurity
|
||||
@@ -42,6 +56,8 @@ public class SecurityConfiguration {
|
||||
@Qualifier("loginEnabled")
|
||||
public boolean loginEnabledValue;
|
||||
|
||||
@Autowired ApplicationProperties applicationProperties;
|
||||
|
||||
@Autowired private UserAuthenticationFilter userAuthenticationFilter;
|
||||
|
||||
@Autowired private LoginAttemptService loginAttemptService;
|
||||
@@ -87,7 +103,7 @@ public class SecurityConfiguration {
|
||||
logout ->
|
||||
logout.logoutRequestMatcher(
|
||||
new AntPathRequestMatcher("/logout"))
|
||||
.logoutSuccessUrl("/login?logout=true")
|
||||
.logoutSuccessHandler(new CustomLogoutSuccessHandler()) // Use a Custom Logout Handler to handle custom error message if OAUTH2 Auto Create is disabled
|
||||
.invalidateHttpSession(true) // Invalidate session
|
||||
.deleteCookies("JSESSIONID", "remember-me")
|
||||
.addLogoutHandler(
|
||||
@@ -124,6 +140,7 @@ public class SecurityConfiguration {
|
||||
: uri;
|
||||
|
||||
return trimmedUri.startsWith("/login")
|
||||
|| trimmedUri.startsWith("/oauth")
|
||||
|| trimmedUri.endsWith(".svg")
|
||||
|| trimmedUri.startsWith(
|
||||
"/register")
|
||||
@@ -140,6 +157,33 @@ public class SecurityConfiguration {
|
||||
.authenticated())
|
||||
.userDetailsService(userDetailsService)
|
||||
.authenticationProvider(authenticationProvider());
|
||||
|
||||
// Handle OAUTH2 Logins
|
||||
if (applicationProperties.getSecurity().getOAUTH2().getEnabled()) {
|
||||
|
||||
http.oauth2Login( oauth2 -> oauth2
|
||||
.loginPage("/oauth2")
|
||||
/*
|
||||
This Custom handler is used to check if the OAUTH2 user trying to log in, already exists in the database.
|
||||
If user exists, login proceeds as usual. If user does not exist, then it is autocreated but only if 'OAUTH2AutoCreateUser'
|
||||
is set as true, else login fails with an error message advising the same.
|
||||
*/
|
||||
.successHandler(new AuthenticationSuccessHandler() {
|
||||
@Override
|
||||
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
|
||||
Authentication authentication) throws ServletException , IOException{
|
||||
OAuth2User oauthUser = (OAuth2User) authentication.getPrincipal();
|
||||
if (userService.processOAuth2PostLogin(oauthUser.getAttribute("email"), applicationProperties.getSecurity().getOAUTH2().getAutoCreateUser())) {
|
||||
response.sendRedirect("/");
|
||||
}
|
||||
else{
|
||||
response.sendRedirect("/logout?oauth2AutoCreateDisabled=true");
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
http.csrf(csrf -> csrf.disable())
|
||||
.authorizeHttpRequests(authz -> authz.anyRequest().permitAll());
|
||||
@@ -148,6 +192,24 @@ public class SecurityConfiguration {
|
||||
return http.build();
|
||||
}
|
||||
|
||||
// Client Registration Repository for OAUTH2 OIDC Login
|
||||
@Bean
|
||||
@ConditionalOnProperty(value = "security.oauth2.enabled" , havingValue = "true", matchIfMissing = false)
|
||||
public ClientRegistrationRepository clientRegistrationRepository() {
|
||||
return new InMemoryClientRegistrationRepository(this.oidcClientRegistration());
|
||||
}
|
||||
|
||||
private ClientRegistration oidcClientRegistration() {
|
||||
return ClientRegistrations.fromOidcIssuerLocation(applicationProperties.getSecurity().getOAUTH2().getIssuer())
|
||||
.registrationId("oidc")
|
||||
.clientId(applicationProperties.getSecurity().getOAUTH2().getClientId())
|
||||
.clientSecret(applicationProperties.getSecurity().getOAUTH2().getClientSecret())
|
||||
.scope("openid", "profile", "email")
|
||||
.userNameAttributeName("email")
|
||||
.clientName("OIDC")
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public IPRateLimitingFilter rateLimitingFilter() {
|
||||
int maxRequestsPerIp = 1000000; // Example limit TODO add config level
|
||||
|
||||
@@ -30,6 +30,24 @@ public class UserService implements UserServiceInterface {
|
||||
|
||||
@Autowired private PasswordEncoder passwordEncoder;
|
||||
|
||||
// Handle OAUTH2 login and user auto creation.
|
||||
public boolean processOAuth2PostLogin(String username, boolean autoCreateUser) {
|
||||
Optional<User> existUser = userRepository.findByUsernameIgnoreCase(username);
|
||||
if (existUser.isPresent()) {
|
||||
return true;
|
||||
}
|
||||
if (autoCreateUser) {
|
||||
User user = new User();
|
||||
user.setUsername(username);
|
||||
user.setEnabled(true);
|
||||
user.setFirstLogin(false);
|
||||
user.addAuthority(new Authority( Role.USER.getRoleId(), user));
|
||||
userRepository.save(user);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public Authentication getAuthentication(String apiKey) {
|
||||
User user = getUserByApiKey(apiKey);
|
||||
if (user == null) {
|
||||
|
||||
@@ -6,9 +6,11 @@ import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -19,6 +21,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import stirling.software.SPDF.model.ApplicationProperties;
|
||||
import stirling.software.SPDF.model.Authority;
|
||||
import stirling.software.SPDF.model.Role;
|
||||
import stirling.software.SPDF.model.User;
|
||||
@@ -28,12 +31,16 @@ import stirling.software.SPDF.repository.UserRepository;
|
||||
@Tag(name = "Account Security", description = "Account Security APIs")
|
||||
public class AccountWebController {
|
||||
|
||||
@Autowired ApplicationProperties applicationProperties;
|
||||
|
||||
@GetMapping("/login")
|
||||
public String login(HttpServletRequest request, Model model, Authentication authentication) {
|
||||
if (authentication != null && authentication.isAuthenticated()) {
|
||||
return "redirect:/";
|
||||
}
|
||||
|
||||
model.addAttribute("oAuth2Enabled", applicationProperties.getSecurity().getOAUTH2().getEnabled());
|
||||
|
||||
model.addAttribute("currentPage", "login");
|
||||
|
||||
if (request.getParameter("error") != null) {
|
||||
@@ -85,14 +92,29 @@ public class AccountWebController {
|
||||
}
|
||||
if (authentication != null && authentication.isAuthenticated()) {
|
||||
Object principal = authentication.getPrincipal();
|
||||
String username = null;
|
||||
|
||||
if (principal instanceof UserDetails) {
|
||||
// Cast the principal object to UserDetails
|
||||
UserDetails userDetails = (UserDetails) principal;
|
||||
|
||||
// Retrieve username and other attributes
|
||||
String username = userDetails.getUsername();
|
||||
username = userDetails.getUsername();
|
||||
|
||||
// Add oAuth2 Login attributes to the model
|
||||
model.addAttribute("oAuth2Login", false);
|
||||
}
|
||||
if (principal instanceof OAuth2User) {
|
||||
// Cast the principal object to OAuth2User
|
||||
OAuth2User userDetails = (OAuth2User) principal;
|
||||
|
||||
// Retrieve username and other attributes
|
||||
username = userDetails.getAttribute("email");
|
||||
|
||||
// Add oAuth2 Login attributes to the model
|
||||
model.addAttribute("oAuth2Login", true);
|
||||
}
|
||||
if (username != null) {
|
||||
// Fetch user details from the database
|
||||
Optional<User> user =
|
||||
userRepository.findByUsernameIgnoreCase(
|
||||
|
||||
@@ -118,6 +118,7 @@ public class ApplicationProperties {
|
||||
private Boolean enableLogin;
|
||||
private Boolean csrfDisabled;
|
||||
private InitialLogin initialLogin;
|
||||
private OAUTH2 oauth2;
|
||||
private int loginAttemptCount;
|
||||
private long loginResetTimeMinutes;
|
||||
|
||||
@@ -145,6 +146,14 @@ public class ApplicationProperties {
|
||||
this.initialLogin = initialLogin;
|
||||
}
|
||||
|
||||
public OAUTH2 getOAUTH2() {
|
||||
return oauth2 != null ? oauth2 : new OAUTH2();
|
||||
}
|
||||
|
||||
public void setOAUTH2(OAUTH2 oauth2) {
|
||||
this.oauth2 = oauth2;
|
||||
}
|
||||
|
||||
public Boolean getEnableLogin() {
|
||||
return enableLogin;
|
||||
}
|
||||
@@ -165,6 +174,8 @@ public class ApplicationProperties {
|
||||
public String toString() {
|
||||
return "Security [enableLogin="
|
||||
+ enableLogin
|
||||
+ ", oauth2="
|
||||
+ oauth2
|
||||
+ ", initialLogin="
|
||||
+ initialLogin
|
||||
+ ", csrfDisabled="
|
||||
@@ -202,6 +213,70 @@ public class ApplicationProperties {
|
||||
+ "]";
|
||||
}
|
||||
}
|
||||
|
||||
public static class OAUTH2 {
|
||||
|
||||
private boolean enabled;
|
||||
private String issuer;
|
||||
private String clientId;
|
||||
private String clientSecret;
|
||||
private boolean autoCreateUser;
|
||||
|
||||
public boolean getEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public String getIssuer() {
|
||||
return issuer;
|
||||
}
|
||||
|
||||
public void setIssuer(String issuer) {
|
||||
this.issuer = issuer;
|
||||
}
|
||||
|
||||
public String getClientId() {
|
||||
return clientId;
|
||||
}
|
||||
|
||||
public void setClientId(String clientId) {
|
||||
this.clientId = clientId;
|
||||
}
|
||||
|
||||
public String getClientSecret() {
|
||||
return clientSecret;
|
||||
}
|
||||
|
||||
public void setClientSecret(String clientSecret) {
|
||||
this.clientSecret = clientSecret;
|
||||
}
|
||||
|
||||
public boolean getAutoCreateUser() {
|
||||
return autoCreateUser;
|
||||
}
|
||||
|
||||
public void setAutoCreateUser(boolean autoCreateUser) {
|
||||
this.autoCreateUser = autoCreateUser;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "OAUTH2 [enabled="
|
||||
+ enabled
|
||||
+ ", issuer="
|
||||
+ issuer
|
||||
+ ", clientId="
|
||||
+ clientId
|
||||
+ ", clientSecret="
|
||||
+ (clientSecret!= null && !clientSecret.isEmpty() ? "MASKED" : "NULL")
|
||||
+ ", autoCreateUser="
|
||||
+ autoCreateUser
|
||||
+ "]";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class System {
|
||||
|
||||
Reference in New Issue
Block a user