2020 */
2121package eu .openanalytics .containerproxy .auth .impl .saml ;
2222
23+ import eu .openanalytics .containerproxy .auth .UserLogoutHandler ;
2324import java .util .ArrayList ;
2425import java .util .Arrays ;
2526import java .util .Collection ;
3132import javax .inject .Inject ;
3233
3334import org .apache .commons .httpclient .HttpClient ;
35+ import org .apache .logging .log4j .LogManager ;
36+ import org .apache .logging .log4j .Logger ;
3437import org .apache .velocity .app .VelocityEngine ;
38+ import org .opensaml .saml2 .core .Attribute ;
3539import org .opensaml .saml2 .metadata .provider .HTTPMetadataProvider ;
3640import org .opensaml .saml2 .metadata .provider .MetadataProvider ;
3741import org .opensaml .saml2 .metadata .provider .MetadataProviderException ;
3842import org .opensaml .util .resource .ResourceException ;
43+ import org .opensaml .xml .XMLObject ;
3944import org .opensaml .xml .parse .StaticBasicParserPool ;
4045import org .opensaml .xml .parse .XMLParserException ;
46+ import org .opensaml .xml .schema .XSAny ;
47+ import org .opensaml .xml .schema .XSString ;
4148import org .springframework .beans .factory .annotation .Qualifier ;
4249import org .springframework .boot .autoconfigure .condition .ConditionalOnProperty ;
4350import org .springframework .context .annotation .Bean ;
5158import org .springframework .security .core .authority .SimpleGrantedAuthority ;
5259import org .springframework .security .core .userdetails .User ;
5360import org .springframework .security .core .userdetails .UsernameNotFoundException ;
54- import org .springframework .security .saml .SAMLAuthenticationProvider ;
55- import org .springframework .security .saml .SAMLBootstrap ;
56- import org .springframework .security .saml .SAMLCredential ;
57- import org .springframework .security .saml .SAMLEntryPoint ;
58- import org .springframework .security .saml .SAMLProcessingFilter ;
61+ import org .springframework .security .saml .*;
5962import org .springframework .security .saml .context .SAMLContextProvider ;
6063import org .springframework .security .saml .context .SAMLContextProviderImpl ;
6164import org .springframework .security .saml .key .EmptyKeyManager ;
7073import org .springframework .security .saml .processor .SAMLProcessorImpl ;
7174import org .springframework .security .saml .userdetails .SAMLUserDetailsService ;
7275import org .springframework .security .saml .util .VelocityFactory ;
76+ import org .springframework .security .saml .websso .SingleLogoutProfile ;
77+ import org .springframework .security .saml .websso .SingleLogoutProfileImpl ;
7378import org .springframework .security .saml .websso .WebSSOProfile ;
7479import org .springframework .security .saml .websso .WebSSOProfileConsumer ;
7580import org .springframework .security .saml .websso .WebSSOProfileConsumerHoKImpl ;
8186import org .springframework .security .web .SecurityFilterChain ;
8287import org .springframework .security .web .authentication .SavedRequestAwareAuthenticationSuccessHandler ;
8388import org .springframework .security .web .authentication .SimpleUrlAuthenticationFailureHandler ;
89+ import org .springframework .security .web .authentication .logout .LogoutHandler ;
90+ import org .springframework .security .web .authentication .logout .SecurityContextLogoutHandler ;
91+ import org .springframework .security .web .authentication .logout .SimpleUrlLogoutSuccessHandler ;
8492import org .springframework .security .web .util .matcher .AntPathRequestMatcher ;
8593
8694@ Configuration
8795@ ConditionalOnProperty (name ="proxy.authentication" , havingValue ="saml" )
8896public class SAMLConfiguration {
8997
9098 private static final String DEFAULT_NAME_ATTRIBUTE = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress" ;
91-
99+
100+ private static final String PROP_LOG_ATTRIBUTES = "proxy.saml.log-attributes" ;
101+ private static final String PROP_FORCE_AUTHN = "proxy.saml.force-authn" ;
102+ private static final String PROP_KEYSTORE = "proxy.saml.keystore" ;
103+ private static final String PROP_ENCRYPTION_CERT_NAME = "proxy.saml.encryption-cert-name" ;
104+ private static final String PROP_ENCRYPTION_CERT_PASSWORD = "proxy.saml.encryption-cert-password" ;
105+ private static final String PROP_ENCRYPTION_KEYSTORE_PASSWORD = "proxy.saml.keystore-password" ;
106+ private static final String PROP_APP_ENTITY_ID = "proxy.saml.app-entity-id" ;
107+ private static final String PROP_BASE_URL = "proxy.saml.app-base-url" ;
108+ private static final String PROP_METADATA_URL = "proxy.saml.idp-metadata-url" ;
109+
92110 @ Inject
93111 private Environment environment ;
94112
95113 @ Inject
96114 @ Lazy
97115 private AuthenticationManager authenticationManager ;
116+
117+ @ Inject
118+ private UserLogoutHandler userLogoutHandler ;
98119
99120 @ Bean
100121 public SAMLEntryPoint samlEntryPoint () {
101122 SAMLEntryPoint samlEntryPoint = new SAMLEntryPoint ();
102123 samlEntryPoint .setDefaultProfileOptions (defaultWebSSOProfileOptions ());
103124 return samlEntryPoint ;
104125 }
126+
127+ @ Bean
128+ public SingleLogoutProfile logoutProfile () {
129+ return new SingleLogoutProfileImpl ();
130+ }
131+
132+ @ Bean
133+ public SAMLLogoutFilter samlLogoutFilter () {
134+ return new SAMLLogoutFilter (successLogoutHandler (),
135+ new LogoutHandler []{userLogoutHandler , securityContextLogoutHandler ()},
136+ new LogoutHandler []{userLogoutHandler , securityContextLogoutHandler ()});
137+ }
138+
139+ /**
140+ * Filter responsible for the `/saml/SingleLogout` endpoint. This makes it possible for users to logout in the IDP
141+ * or any other application and get automatically logged out in ShinyProxy as well.
142+ */
143+ @ Bean
144+ public SAMLLogoutProcessingFilter samlLogoutProcessingFilter () {
145+ return new SAMLLogoutProcessingFilter (successLogoutHandler (),
146+ securityContextLogoutHandler ());
147+ }
148+
149+ @ Bean
150+ public SecurityContextLogoutHandler securityContextLogoutHandler () {
151+ SecurityContextLogoutHandler logoutHandler = new SecurityContextLogoutHandler ();
152+ logoutHandler .setInvalidateHttpSession (true );
153+ logoutHandler .setClearAuthentication (true );
154+ return logoutHandler ;
155+ }
156+
157+ @ Bean
158+ public SimpleUrlLogoutSuccessHandler successLogoutHandler () {
159+ SimpleUrlLogoutSuccessHandler successLogoutHandler = new SimpleUrlLogoutSuccessHandler ();
160+ successLogoutHandler .setDefaultTargetUrl ("/" );
161+ return successLogoutHandler ;
162+ }
105163
106164 @ Bean
107165 public WebSSOProfileOptions defaultWebSSOProfileOptions () {
108166 WebSSOProfileOptions webSSOProfileOptions = new WebSSOProfileOptions ();
109167 webSSOProfileOptions .setIncludeScoping (false );
110- webSSOProfileOptions .setForceAuthN (Boolean .valueOf (environment .getProperty ("proxy.saml.force-authn" , "false" )));
168+ webSSOProfileOptions .setForceAuthN (Boolean .valueOf (environment .getProperty (PROP_FORCE_AUTHN , "false" )));
111169 return webSSOProfileOptions ;
112170 }
113171
@@ -123,13 +181,13 @@ public WebSSOProfile webSSOprofile() {
123181
124182 @ Bean
125183 public KeyManager keyManager () {
126- String keystore = environment .getProperty ("proxy.saml.keystore" );
184+ String keystore = environment .getProperty (PROP_KEYSTORE );
127185 if (keystore == null || keystore .isEmpty ()) {
128186 return new EmptyKeyManager ();
129187 } else {
130- String certName = environment .getProperty ("proxy.saml.encryption-cert-name" );
131- String certPW = environment .getProperty ("proxy.saml.encryption-cert-password" );
132- String keystorePW = environment .getProperty ("proxy.saml.keystore-password" , certPW );
188+ String certName = environment .getProperty (PROP_ENCRYPTION_CERT_NAME );
189+ String certPW = environment .getProperty (PROP_ENCRYPTION_CERT_PASSWORD );
190+ String keystorePW = environment .getProperty (PROP_ENCRYPTION_KEYSTORE_PASSWORD , certPW );
133191
134192 Resource keystoreFile = new FileSystemResource (keystore );
135193 Map <String , String > passwords = new HashMap <>();
@@ -193,8 +251,8 @@ public MetadataDisplayFilter metadataDisplayFilter() throws MetadataProviderExce
193251
194252 @ Bean
195253 public MetadataGenerator metadataGenerator () {
196- String appEntityId = environment .getProperty ("proxy.saml.app-entity-id" );
197- String appBaseURL = environment .getProperty ("proxy.saml.app-base-url" );
254+ String appEntityId = environment .getProperty (PROP_APP_ENTITY_ID );
255+ String appBaseURL = environment .getProperty (PROP_BASE_URL );
198256
199257 MetadataGenerator metadataGenerator = new MetadataGenerator ();
200258 metadataGenerator .setEntityId (appEntityId );
@@ -215,7 +273,7 @@ public ExtendedMetadata extendedMetadata() {
215273
216274 @ Bean
217275 public ExtendedMetadataDelegate idpMetadata () throws MetadataProviderException , ResourceException {
218- String metadataURL = environment .getProperty ("proxy.saml.idp-metadata-url" );
276+ String metadataURL = environment .getProperty (PROP_METADATA_URL );
219277
220278 Timer backgroundTaskTimer = new Timer (true );
221279 HTTPMetadataProvider httpMetadataProvider = new HTTPMetadataProvider (backgroundTaskTimer , new HttpClient (), metadataURL ); httpMetadataProvider .setParserPool (parserPool ());
@@ -281,21 +339,32 @@ public WebSSOProfileConsumerHoKImpl hokWebSSOprofileConsumer() {
281339 public SAMLFilterSet samlFilter () throws Exception {
282340 List <SecurityFilterChain > chains = new ArrayList <SecurityFilterChain >();
283341 chains .add (new DefaultSecurityFilterChain (new AntPathRequestMatcher ("/saml/login/**" ), samlEntryPoint ()));
342+ chains .add (new DefaultSecurityFilterChain (new AntPathRequestMatcher ("/saml/logout/**" ), samlLogoutFilter ()));
343+ chains .add (new DefaultSecurityFilterChain (new AntPathRequestMatcher ("/saml/SingleLogout/**" ), samlLogoutProcessingFilter ()));
284344 chains .add (new DefaultSecurityFilterChain (new AntPathRequestMatcher ("/saml/SSO/**" ), samlWebSSOProcessingFilter ()));
285345 return new SAMLFilterSet (chains );
286346 }
287347
348+ private final Logger log = LogManager .getLogger (getClass ());
349+
350+
351+
288352 @ Bean
289353 public SAMLAuthenticationProvider samlAuthenticationProvider () {
290354 SAMLAuthenticationProvider samlAuthenticationProvider = new SAMLAuthenticationProvider ();
291355 samlAuthenticationProvider .setUserDetails (new SAMLUserDetailsService () {
292356 @ Override
293357 public Object loadUserBySAML (SAMLCredential credential ) throws UsernameNotFoundException {
294- String nameAttribute = environment .getProperty ("proxy.saml.name-attribute" , DEFAULT_NAME_ATTRIBUTE );
295- String nameValue = credential .getAttributeAsString (nameAttribute );
296- if (nameValue == null ) throw new UsernameNotFoundException ("Name attribute missing from SAML assertion: " + nameAttribute );
297-
298- List <GrantedAuthority > auth = new ArrayList <>();
358+
359+ if (Boolean .parseBoolean (environment .getProperty (PROP_LOG_ATTRIBUTES , "false" ))) {
360+ AttributeUtils .logAttributes (log , credential );
361+ }
362+
363+ String nameAttribute = environment .getProperty ("proxy.saml.name-attribute" , DEFAULT_NAME_ATTRIBUTE );
364+ String nameValue = credential .getAttributeAsString (nameAttribute );
365+ if (nameValue == null ) throw new UsernameNotFoundException ("Name attribute missing from SAML assertion: " + nameAttribute );
366+
367+ List <GrantedAuthority > auth = new ArrayList <>();
299368 String rolesAttribute = environment .getProperty ("proxy.saml.roles-attribute" );
300369 if (rolesAttribute != null && !rolesAttribute .trim ().isEmpty ()) {
301370 String [] roles = credential .getAttributeAsStringArray (rolesAttribute );
0 commit comments