Skip to content

Commit

Permalink
Add OAUTH2 OIDC login support (#1140)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
sahilph authored Apr 29, 2024
1 parent 777e512 commit d9fa8f7
Show file tree
Hide file tree
Showing 12 changed files with 282 additions and 5 deletions.
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security:3.2.4'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.1.2.RELEASE'
implementation "org.springframework.boot:spring-boot-starter-data-jpa:3.2.4"
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client:3.2.4'

//2.2.x requires rebuild of DB file.. need migration path
implementation "com.h2database:h2:2.1.214"
Expand Down
39 changes: 39 additions & 0 deletions exampleYmlFiles/docker-compose-latest-security-with-sso.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
version: '3.3'
services:
stirling-pdf:
container_name: Stirling-PDF-Security
image: frooodle/s-pdf:latest
deploy:
resources:
limits:
memory: 4G
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP' && curl -fL http://localhost:8080/ | grep -q 'Please sign in'"]
interval: 5s
timeout: 10s
retries: 16
ports:
- 8080:8080
volumes:
- /stirling/latest/data:/usr/share/tessdata:rw
- /stirling/latest/config:/configs:rw
- /stirling/latest/logs:/logs:rw
environment:
DOCKER_ENABLE_SECURITY: "true"
SECURITY_ENABLELOGIN: "true"
SECURITY_OAUTH2_ENABLED: "true"
SECURITY_OAUTH2_AUTOCREATEUSER: "true" # This is set to true to allow auto-creation of non-existing users in Striling-PDF
SECURITY_OAUTH2_ISSUER: "https://accounts.google.com" # Change with any other provider that supports OpenID Connect Discovery (/.well-known/openid-configuration) end-point
SECURITY_OAUTH2_CLIENTID: "<YOUR CLIENT ID>.apps.googleusercontent.com" # Client ID from your provider
SECURITY_OAUTH2_CLIENTSECRET: "<YOUR CLIENT SECRET>" # Client Secret from your provider
PUID: 1002
PGID: 1002
UMASK: "022"
SYSTEM_DEFAULTLOCALE: en-US
UI_APPNAME: Stirling-PDF
UI_HOMEDESCRIPTION: Demo site for Stirling-PDF Latest with Security
UI_APPNAMENAVBAR: Stirling-PDF Latest
SYSTEM_MAXFILESIZE: "100"
METRICS_ENABLED: "true"
SYSTEM_GOOGLEVISIBILITY: "true"
restart: on-failure:5
Original file line number Diff line number Diff line change
@@ -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");
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -10,20 +14,30 @@
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
Expand All @@ -42,6 +56,8 @@ public PasswordEncoder passwordEncoder() {
@Qualifier("loginEnabled")
public boolean loginEnabledValue;

@Autowired ApplicationProperties applicationProperties;

@Autowired private UserAuthenticationFilter userAuthenticationFilter;

@Autowired private LoginAttemptService loginAttemptService;
Expand Down Expand Up @@ -87,7 +103,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
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(
Expand Down Expand Up @@ -124,6 +140,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
: uri;

return trimmedUri.startsWith("/login")
|| trimmedUri.startsWith("/oauth")
|| trimmedUri.endsWith(".svg")
|| trimmedUri.startsWith(
"/register")
Expand All @@ -140,6 +157,33 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.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());
Expand All @@ -148,6 +192,24 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
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;
Expand All @@ -19,6 +21,7 @@
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;
Expand All @@ -28,12 +31,16 @@
@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) {
Expand Down Expand Up @@ -85,14 +92,29 @@ public String account(HttpServletRequest request, Model model, Authentication au
}
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(
Expand Down
Loading

0 comments on commit d9fa8f7

Please sign in to comment.