Skip to content

Commit

Permalink
Major Enhancements to SAML2 and OAuth2 Integration with Simplified Se…
Browse files Browse the repository at this point in the history
…curity Configurations (#2040)

* implement Saml2 login/logout

* changed: deprecation code

* relyingPartyRegistrations only enabled samle
  • Loading branch information
Ludy87 authored Oct 20, 2024
1 parent 227d18a commit eff1843
Show file tree
Hide file tree
Showing 32 changed files with 1,083 additions and 842 deletions.
16 changes: 11 additions & 5 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,9 @@ java {
repositories {
mavenCentral()
maven { url "https://jitpack.io" }
maven {
url "https://build.shibboleth.net/nexus/content/repositories/releases/"
}
maven { url "https://build.shibboleth.net/nexus/content/repositories/releases/" }
maven {
url "https://build.shibboleth.net/maven/releases/"
url 'https://build.shibboleth.net/maven/releases'
}
}

Expand Down Expand Up @@ -135,7 +133,7 @@ dependencies {
implementation "org.springframework.boot:spring-boot-starter-thymeleaf:$springBootVersion"
implementation 'com.posthog.java:posthog:1.1.1'
implementation 'com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20240325.1'


if (System.getenv("DOCKER_ENABLE_SECURITY") != "false") {
implementation "org.springframework.boot:spring-boot-starter-security:$springBootVersion"
Expand All @@ -148,6 +146,14 @@ dependencies {
//2.2.x requires rebuild of DB file.. need migration path
runtimeOnly "com.h2database:h2:2.1.214"
// implementation "com.h2database:h2:2.2.224"
constraints {
implementation "org.opensaml:opensaml-core"
implementation "org.opensaml:opensaml-saml-api"
implementation "org.opensaml:opensaml-saml-impl"
}
implementation "org.springframework.security:spring-security-saml2-service-provider"

implementation 'com.coveo:saml-client:5.0.0'
}

testImplementation "org.springframework.boot:spring-boot-starter-test:$springBootVersion"
Expand Down
17 changes: 15 additions & 2 deletions src/main/java/stirling/software/SPDF/SPdfApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,12 @@ public class SPdfApplication {
@Autowired private Environment env;
@Autowired ApplicationProperties applicationProperties;

private static String baseUrlStatic;
private static String serverPortStatic;

@Value("${baseUrl:http://localhost}")
private String baseUrl;

@Value("${server.port:8080}")
public void setServerPortStatic(String port) {
if ("auto".equalsIgnoreCase(port)) {
Expand Down Expand Up @@ -65,12 +69,13 @@ private static boolean isPortAvailable(int port) {

@PostConstruct
public void init() {
baseUrlStatic = this.baseUrl;
// Check if the BROWSER_OPEN environment variable is set to true
String browserOpenEnv = env.getProperty("BROWSER_OPEN");
boolean browserOpen = browserOpenEnv != null && "true".equalsIgnoreCase(browserOpenEnv);
if (browserOpen) {
try {
String url = "http://localhost:" + getStaticPort();
String url = baseUrl + ":" + getStaticPort();

String os = System.getProperty("os.name").toLowerCase();
Runtime rt = Runtime.getRuntime();
Expand Down Expand Up @@ -138,10 +143,18 @@ public static void main(String[] args) throws IOException, InterruptedException

private static void printStartupLogs() {
logger.info("Stirling-PDF Started.");
String url = "http://localhost:" + getStaticPort();
String url = baseUrlStatic + ":" + getStaticPort();
logger.info("Navigate to {}", url);
}

public static String getStaticBaseUrl() {
return baseUrlStatic;
}

public String getNonStaticBaseUrl() {
return baseUrlStatic;
}

public static String getStaticPort() {
return serverPortStatic;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,237 @@
package stirling.software.SPDF.config.security;

import java.io.IOException;
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPrivateKey;
import java.util.ArrayList;
import java.util.List;

import org.springframework.core.io.Resource;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;

import com.coveo.saml.SamlClient;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.SPdfApplication;
import stirling.software.SPDF.config.security.saml2.CertificateUtils;
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
import stirling.software.SPDF.model.ApplicationProperties.Security.SAML2;
import stirling.software.SPDF.model.Provider;
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
import stirling.software.SPDF.utils.UrlUtils;

@Slf4j
@AllArgsConstructor
public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {

private final ApplicationProperties applicationProperties;

@Override
public void onLogoutSuccess(
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {

if (request.getParameter("userIsDisabled") != null) {
getRedirectStrategy()
.sendRedirect(request, response, "/login?erroroauth=userIsDisabled");
return;
if (!response.isCommitted()) {
// Handle user logout due to disabled account
if (request.getParameter("userIsDisabled") != null) {
response.sendRedirect(
request.getContextPath() + "/login?erroroauth=userIsDisabled");
return;
}
// Handle OAuth2 authentication error
if (request.getParameter("oauth2AuthenticationErrorWeb") != null) {
response.sendRedirect(
request.getContextPath() + "/login?erroroauth=userAlreadyExistsWeb");
return;
}
if (authentication != null) {
// Handle SAML2 logout redirection
if (authentication instanceof Saml2Authentication) {
getRedirect_saml2(request, response, authentication);
return;
}
// Handle OAuth2 logout redirection
else if (authentication instanceof OAuth2AuthenticationToken) {
getRedirect_oauth2(request, response, authentication);
return;
}
// Handle Username/Password logout
else if (authentication instanceof UsernamePasswordAuthenticationToken) {
getRedirectStrategy().sendRedirect(request, response, "/login?logout=true");
return;
}
// Handle unknown authentication types
else {
log.error(
"authentication class unknown: "
+ authentication.getClass().getSimpleName());
getRedirectStrategy().sendRedirect(request, response, "/login?logout=true");
return;
}
} else {
// Redirect to login page after logout
getRedirectStrategy().sendRedirect(request, response, "/login?logout=true");
return;
}
}
}

// Redirect for SAML2 authentication logout
private void getRedirect_saml2(
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException {

SAML2 samlConf = applicationProperties.getSecurity().getSaml2();
String registrationId = samlConf.getRegistrationId();

Saml2Authentication samlAuthentication = (Saml2Authentication) authentication;
CustomSaml2AuthenticatedPrincipal principal =
(CustomSaml2AuthenticatedPrincipal) samlAuthentication.getPrincipal();

String nameIdValue = principal.getName();

try {
// Read certificate from the resource
Resource certificateResource = samlConf.getSpCert();
X509Certificate certificate = CertificateUtils.readCertificate(certificateResource);

List<X509Certificate> certificates = new ArrayList<>();
certificates.add(certificate);

// Construct URLs required for SAML configuration
String serverUrl =
SPdfApplication.getStaticBaseUrl() + ":" + SPdfApplication.getStaticPort();

String relyingPartyIdentifier =
serverUrl + "/saml2/service-provider-metadata/" + registrationId;

String assertionConsumerServiceUrl = serverUrl + "/login/saml2/sso/" + registrationId;

String idpUrl = samlConf.getIdpSingleLogoutUrl();

String idpIssuer = samlConf.getIdpIssuer();

// Create SamlClient instance for SAML logout
SamlClient samlClient =
new SamlClient(
relyingPartyIdentifier,
assertionConsumerServiceUrl,
idpUrl,
idpIssuer,
certificates,
SamlClient.SamlIdpBinding.POST);

// Read private key for service provider
Resource privateKeyResource = samlConf.getPrivateKey();
RSAPrivateKey privateKey = CertificateUtils.readPrivateKey(privateKeyResource);

// Set service provider keys for the SamlClient
samlClient.setSPKeys(certificate, privateKey);

// Redirect to identity provider for logout
samlClient.redirectToIdentityProvider(response, null, nameIdValue);
} catch (Exception e) {
log.error(nameIdValue, e);
getRedirectStrategy().sendRedirect(request, response, "/login?logout=true");
}
}

// Redirect for OAuth2 authentication logout
private void getRedirect_oauth2(
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException {
String param = "logout=true";
String registrationId = null;
String issuer = null;
String clientId = null;
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2();

if (authentication instanceof OAuth2AuthenticationToken) {
OAuth2AuthenticationToken oauthToken = (OAuth2AuthenticationToken) authentication;
registrationId = oauthToken.getAuthorizedClientRegistrationId();

try {
// Get OAuth2 provider details from configuration
Provider provider = oauth.getClient().get(registrationId);
issuer = provider.getIssuer();
clientId = provider.getClientId();
} catch (UnsupportedProviderException e) {
log.error(e.getMessage());
}
} else {
registrationId = oauth.getProvider() != null ? oauth.getProvider() : "";
issuer = oauth.getIssuer();
clientId = oauth.getClientId();
}
String errorMessage = "";
// Handle different error scenarios during logout
if (request.getParameter("oauth2AuthenticationErrorWeb") != null) {
param = "erroroauth=oauth2AuthenticationErrorWeb";
} else if ((errorMessage = request.getParameter("error")) != null) {
param = "error=" + sanitizeInput(errorMessage);
} else if ((errorMessage = request.getParameter("erroroauth")) != null) {
param = "erroroauth=" + sanitizeInput(errorMessage);
} else if (request.getParameter("oauth2AutoCreateDisabled") != null) {
param = "error=oauth2AutoCreateDisabled";
} else if (request.getParameter("oauth2_admin_blocked_user") != null) {
param = "erroroauth=oauth2_admin_blocked_user";
} else if (request.getParameter("userIsDisabled") != null) {
param = "erroroauth=userIsDisabled";
} else if (request.getParameter("badcredentials") != null) {
param = "error=badcredentials";
}

String redirect_url = UrlUtils.getOrigin(request) + "/login?" + param;

// Redirect based on OAuth2 provider
switch (registrationId.toLowerCase()) {
case "keycloak":
// Add Keycloak specific logout URL if needed
String logoutUrl =
issuer
+ "/protocol/openid-connect/logout"
+ "?client_id="
+ clientId
+ "&post_logout_redirect_uri="
+ response.encodeRedirectURL(redirect_url);
log.info("Redirecting to Keycloak logout URL: " + logoutUrl);
response.sendRedirect(logoutUrl);
break;
case "github":
// Add GitHub specific logout URL if needed
String githubLogoutUrl = "https://github.com/logout";
log.info("Redirecting to GitHub logout URL: " + githubLogoutUrl);
response.sendRedirect(githubLogoutUrl);
break;
case "google":
// Add Google specific logout URL if needed
// String googleLogoutUrl =
// "https://accounts.google.com/Logout?continue=https://appengine.google.com/_ah/logout?continue="
// + response.encodeRedirectURL(redirect_url);
log.info("Google does not have a specific logout URL");
// log.info("Redirecting to Google logout URL: " + googleLogoutUrl);
// response.sendRedirect(googleLogoutUrl);
// break;
default:
String defaultRedirectUrl = request.getContextPath() + "/login?" + param;
log.info("Redirecting to default logout URL: " + defaultRedirectUrl);
response.sendRedirect(defaultRedirectUrl);
break;
}
}

getRedirectStrategy().sendRedirect(request, response, "/login?logout=true");
// Sanitize input to avoid potential security vulnerabilities
private String sanitizeInput(String input) {
return input.replaceAll("[^a-zA-Z0-9 ]", "");
}
}
Loading

0 comments on commit eff1843

Please sign in to comment.