Skip to content

Commit 4300076

Browse files
committed
Merge pull request 'SAML Improvements' (#37) from feature/24534 into develop
2 parents 26711ee + 8c94e0b commit 4300076

File tree

4 files changed

+123
-30
lines changed

4 files changed

+123
-30
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/**
2+
* ContainerProxy
3+
*
4+
* Copyright (C) 2016-2020 Open Analytics
5+
*
6+
* ===========================================================================
7+
*
8+
* This program is free software: you can redistribute it and/or modify
9+
* it under the terms of the Apache License as published by
10+
* The Apache Software Foundation, either version 2 of the License, or
11+
* (at your option) any later version.
12+
*
13+
* This program is distributed in the hope that it will be useful,
14+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
* Apache License for more details.
17+
*
18+
* You should have received a copy of the Apache License
19+
* along with this program. If not, see <http://www.apache.org/licenses/>
20+
*/
21+
package eu.openanalytics.containerproxy.auth.impl.saml;
22+
23+
24+
import org.springframework.security.authentication.AnonymousAuthenticationToken;
25+
import org.springframework.security.core.Authentication;
26+
import org.springframework.security.core.context.SecurityContextHolder;
27+
import org.springframework.web.filter.GenericFilterBean;
28+
29+
import javax.servlet.*;
30+
import java.io.IOException;
31+
32+
/**
33+
* A filter that sets a request attribute when the user is already logged in.
34+
* This is used to know whether a user was already logged in when performing a SAML SSO request.
35+
* If so, the user probably has clicked the back button and should therefore be redirected to the main page.
36+
*
37+
* Note: we don't redirect to the main page here, because that seems to be to intrusive. There may be good reasons
38+
* to land on the SSO page again.
39+
*/
40+
public class AlreadyLoggedInFilter extends GenericFilterBean {
41+
42+
public static final String REQ_PROP_AUTH_BEFORE_SSO = "SP_REQ_PROP_AUTH_BEFORE_SSO";
43+
44+
@Override
45+
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
46+
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
47+
if (auth != null && !(auth instanceof AnonymousAuthenticationToken)) {
48+
request.setAttribute(REQ_PROP_AUTH_BEFORE_SSO, "true");
49+
}
50+
chain.doFilter(request, response);
51+
}
52+
53+
}
54+

src/main/java/eu/openanalytics/containerproxy/auth/impl/saml/AuthenticationFailureHandler.java

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,33 +20,80 @@
2020
*/
2121
package eu.openanalytics.containerproxy.auth.impl.saml;
2222

23+
import org.apache.logging.log4j.LogManager;
24+
import org.apache.logging.log4j.Logger;
25+
import org.opensaml.common.SAMLException;
26+
import org.springframework.security.authentication.CredentialsExpiredException;
27+
import org.springframework.security.core.Authentication;
2328
import org.springframework.security.core.AuthenticationException;
29+
import org.springframework.security.core.context.SecurityContext;
30+
import org.springframework.security.core.context.SecurityContextHolder;
2431
import org.springframework.security.saml.SAMLStatusException;
2532
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
2633
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
2734

2835
import javax.servlet.ServletException;
2936
import javax.servlet.http.HttpServletRequest;
3037
import javax.servlet.http.HttpServletResponse;
38+
import javax.servlet.http.HttpSession;
3139
import java.io.IOException;
40+
import java.util.Objects;
3241

33-
public class AuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
42+
import static eu.openanalytics.containerproxy.auth.impl.saml.AlreadyLoggedInFilter.REQ_PROP_AUTH_BEFORE_SSO;
43+
44+
public class AuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
45+
46+
private final Logger logger = LogManager.getLogger(getClass());
3447

3548
public void onAuthenticationFailure(HttpServletRequest request,
3649
HttpServletResponse response, AuthenticationException exception)
3750
throws IOException, ServletException {
3851

39-
4052
if (exception.getCause() instanceof SAMLStatusException) {
4153
SAMLStatusException samlException = (SAMLStatusException) exception.getCause();
4254

4355
if (samlException.getStatusCode().equals("urn:oasis:names:tc:SAML:2.0:status:RequestDenied")) {
4456
response.sendRedirect(ServletUriComponentsBuilder.fromCurrentContextPath().path("/app-access-denied").build().toUriString());
4557
return;
4658
}
59+
60+
} else if (exception.getCause() instanceof SAMLException) {
61+
SAMLException samlException = (SAMLException) exception.getCause();
62+
63+
if (isOrWasAuthenticated(request) && (
64+
samlException.getMessage().startsWith("Response issue time is either too old or with date in the future")
65+
|| samlException.getMessage().startsWith("InResponseToField of the Response doesn't correspond to sent message"))
66+
|| samlException.getMessage().equals("Unsupported request")) {
67+
response.sendRedirect(ServletUriComponentsBuilder.fromCurrentContextPath().path("/").build().toUriString());
68+
return;
69+
} else if (samlException.getCause() instanceof CredentialsExpiredException) {
70+
logger.warn("The credentials of the user has expired, this typically indicates a misconfiguration, see https://shinyproxy.io/faq/#the-credentials-of-the-user-expire-when-using-saml for more information!");
71+
response.sendRedirect(ServletUriComponentsBuilder.fromCurrentContextPath().path("/auth-error").build().toUriString());
72+
return;
73+
}
4774
}
4875

4976
super.onAuthenticationFailure(request, response, exception);
5077
}
5178

79+
private boolean isOrWasAuthenticated(HttpServletRequest request) {
80+
if (Objects.equals(request.getAttribute(REQ_PROP_AUTH_BEFORE_SSO), "true")) {
81+
// Before doing a SSO request we check whether the user is authenticated, if so we set the SP_REQ_PROP_AUTH_BEFORE_SSO
82+
// property. If the auth failed, the Spring SecurityContext is cleared and thus we cannot use that to
83+
// check whether the user is authenticated.
84+
return true;
85+
}
86+
87+
HttpSession session = request.getSession();
88+
Object obj = session.getAttribute("SPRING_SECURITY_CONTEXT");
89+
if (obj instanceof SecurityContext) {
90+
SecurityContext ctx = (SecurityContext) obj;
91+
Authentication auth = ctx.getAuthentication();
92+
// in some cases the session may still contain the security context so we fallback to that
93+
return auth != null && auth.getPrincipal() != null;
94+
}
95+
96+
return false;
97+
}
98+
5299
}

src/main/java/eu/openanalytics/containerproxy/auth/impl/saml/SAMLConfiguration.java

Lines changed: 18 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -21,31 +21,17 @@
2121
package eu.openanalytics.containerproxy.auth.impl.saml;
2222

2323
import eu.openanalytics.containerproxy.auth.UserLogoutHandler;
24-
import java.util.ArrayList;
25-
import java.util.Arrays;
26-
import java.util.Collection;
27-
import java.util.HashMap;
28-
import java.util.List;
29-
import java.util.Map;
30-
import java.util.Timer;
31-
32-
import javax.inject.Inject;
33-
3424
import eu.openanalytics.containerproxy.auth.impl.SAMLAuthenticationBackend;
3525
import org.apache.commons.httpclient.HttpClient;
3626
import org.apache.logging.log4j.LogManager;
3727
import org.apache.logging.log4j.Logger;
3828
import org.apache.velocity.app.VelocityEngine;
39-
import org.opensaml.saml2.core.Attribute;
4029
import org.opensaml.saml2.metadata.provider.HTTPMetadataProvider;
4130
import org.opensaml.saml2.metadata.provider.MetadataProvider;
4231
import org.opensaml.saml2.metadata.provider.MetadataProviderException;
4332
import org.opensaml.util.resource.ResourceException;
44-
import org.opensaml.xml.XMLObject;
4533
import org.opensaml.xml.parse.StaticBasicParserPool;
4634
import org.opensaml.xml.parse.XMLParserException;
47-
import org.opensaml.xml.schema.XSAny;
48-
import org.opensaml.xml.schema.XSString;
4935
import org.springframework.beans.factory.annotation.Qualifier;
5036
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
5137
import org.springframework.context.annotation.Bean;
@@ -60,7 +46,6 @@
6046
import org.springframework.security.core.userdetails.User;
6147
import org.springframework.security.core.userdetails.UsernameNotFoundException;
6248
import org.springframework.security.saml.*;
63-
import org.springframework.security.saml.context.SAMLContextProvider;
6449
import org.springframework.security.saml.context.SAMLContextProviderImpl;
6550
import org.springframework.security.saml.key.EmptyKeyManager;
6651
import org.springframework.security.saml.key.JKSKeyManager;
@@ -74,14 +59,7 @@
7459
import org.springframework.security.saml.processor.SAMLProcessorImpl;
7560
import org.springframework.security.saml.userdetails.SAMLUserDetailsService;
7661
import org.springframework.security.saml.util.VelocityFactory;
77-
import org.springframework.security.saml.websso.SingleLogoutProfile;
78-
import org.springframework.security.saml.websso.SingleLogoutProfileImpl;
79-
import org.springframework.security.saml.websso.WebSSOProfile;
80-
import org.springframework.security.saml.websso.WebSSOProfileConsumer;
81-
import org.springframework.security.saml.websso.WebSSOProfileConsumerHoKImpl;
82-
import org.springframework.security.saml.websso.WebSSOProfileConsumerImpl;
83-
import org.springframework.security.saml.websso.WebSSOProfileImpl;
84-
import org.springframework.security.saml.websso.WebSSOProfileOptions;
62+
import org.springframework.security.saml.websso.*;
8563
import org.springframework.security.web.DefaultSecurityFilterChain;
8664
import org.springframework.security.web.FilterChainProxy;
8765
import org.springframework.security.web.SecurityFilterChain;
@@ -92,6 +70,9 @@
9270
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
9371
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
9472

73+
import javax.inject.Inject;
74+
import java.util.*;
75+
9576
@Configuration
9677
@ConditionalOnProperty(name="proxy.authentication", havingValue="saml")
9778
public class SAMLConfiguration {
@@ -100,6 +81,7 @@ public class SAMLConfiguration {
10081

10182
private static final String PROP_LOG_ATTRIBUTES = "proxy.saml.log-attributes";
10283
private static final String PROP_FORCE_AUTHN = "proxy.saml.force-authn";
84+
private static final String PROP_MAX_AUTHENTICATION_AGE = "proxy.saml.max-authentication-age";
10385
private static final String PROP_KEYSTORE = "proxy.saml.keystore";
10486
private static final String PROP_ENCRYPTION_CERT_NAME = "proxy.saml.encryption-cert-name";
10587
private static final String PROP_ENCRYPTION_CERT_PASSWORD = "proxy.saml.encryption-cert-password";
@@ -117,7 +99,7 @@ public class SAMLConfiguration {
11799

118100
@Inject
119101
private UserLogoutHandler userLogoutHandler;
120-
102+
121103
@Bean
122104
public SAMLEntryPoint samlEntryPoint() {
123105
SAMLEntryPoint samlEntryPoint = new SAMLEntryPoint();
@@ -326,9 +308,19 @@ public SAMLProcessingFilter samlWebSSOProcessingFilter() throws Exception {
326308
return samlWebSSOProcessingFilter;
327309
}
328310

311+
@Bean
312+
public AlreadyLoggedInFilter alreadyLoggedInFilter() {
313+
return new AlreadyLoggedInFilter();
314+
}
315+
329316
@Bean
330317
public WebSSOProfileConsumer webSSOprofileConsumer() {
331-
return new WebSSOProfileConsumerImpl();
318+
WebSSOProfileConsumerImpl res = new WebSSOProfileConsumerImpl();
319+
Integer maxAuthenticationAge = environment.getProperty(PROP_MAX_AUTHENTICATION_AGE, Integer.class);
320+
if (maxAuthenticationAge != null) {
321+
res.setMaxAuthenticationAge(maxAuthenticationAge);
322+
}
323+
return res;
332324
}
333325

334326
@Bean
@@ -342,7 +334,7 @@ public SAMLFilterSet samlFilter() throws Exception {
342334
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/login/**"), samlEntryPoint()));
343335
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/logout/**"), samlLogoutFilter()));
344336
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/SingleLogout/**"), samlLogoutProcessingFilter()));
345-
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/SSO/**"), samlWebSSOProcessingFilter()));
337+
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/SSO/**"), alreadyLoggedInFilter(), samlWebSSOProcessingFilter()));
346338
return new SAMLFilterSet(chains);
347339
}
348340

src/main/resources/templates/auth-error.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@
3939
<body>
4040
<div class="container">
4141
<h2>An error occurred during the authentication procedure.</h2>
42-
<p><b>If you are a user of <span th:text="${application_name}"></span>:</b> please report this issue to your administrator.</p>
43-
<p><b>If you are an administrator of <span th:text="${application_name}"></span>:</b> this error page is typically shown because of an configuration error in the OpenID setup. See the ShinyProxy logs for more information.</p>
42+
<p><b>If you are a user of <span th:text="${application_name}"></span>:</b> please report this issue to your administrator and try to log out from your Identity Provider.</p>
43+
<p><b>If you are an administrator of <span th:text="${application_name}"></span>:</b> this error page is typically shown because of an configuration error in the authentication setup. See the ShinyProxy logs for more information.</p>
4444
</div>
4545

4646
<style>

0 commit comments

Comments
 (0)