Expand my Community achievements bar.

SOLVED

SAML Config for Author for Alternate Path

Avatar

Level 4

We have SAML authentication working on AEM Author 6.5 SP13. Users can visit the author at https://author and are redirected to the IdP. On successful authentication at the IdP, they get redirected back to https://author and are logged in.

I have the need to use SAML authentication not on the default/root (i.e. not on https://author) - but on an alternate path. I specified this in the "Path" parameter of the "Adobe Granite SAML 2.0 Authentication Handler" config, e.g. /foo

This path does not exist in the repository. 

Ideally, users who know this https://author/foo URL are directed to the IdP for authentication and those who visit the root (i.e. https://author) use local authentication.

It seems to be working except, users who visit https://author/foo cannot authenticate, they receive an "Authentication Failed" error. 

If the "Path" parameter of "Adobe Granite SAML 2.0 Authentication Handler" config is switched from /foo back to / - SAML authentication works - but again, for / - we'd prefer to use local authentication.

When using /foo - in the logs there's a warning about:

org.apache.sling.auth.core.AuthUtil isRedirectValid: Redirect target must not be empty or null (there is not much info on what this warning actually means).

When the metadata for the author/SP was shared with the IdP initially, the following element is present:

 

<md:AssertionConsumerService
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://author/saml_login"
index="1" />

 

I found the following: https://helpx.adobe.com/au/experience-manager/kb/saml-demo.html

 

Sitewide Anonymous Access with Optional Authentication
The authentication handler is built around protecting content from anonymous access via the Path configuration. If all pages on the AEM site need to be accessible anonymously, but authentication also needs tobe an option, the Path configuration value can be set to a non-existent path. This will enable SAML authentication but also allow anonymous access to all pages on the site. If this strategy is used, make sure that the SAMLReponse POSTs to the correct saml_login path (see next item).


The Path configuration and saml_login
The IdP’s SAMLResponse must be posted to the page ‘saml_login’. However, the ‘saml_login’ page must be within the path that the authentication handler protects (i.e. the Path configuration). For instance, if the Path configuration is ‘/‘ the IdP can post to http://localhost:4502/saml_login. If the Path is ‘/content/geometrixx’ the IdP can post to http://localhost:4502/content/geometrixx/saml_login or http://localhost:4502/content/geometrixx/does-not-exist/saml_login but http://localhost:4502/content/saml_login will not work.

 

If I asked the IdP to update the metadata to the following:

 

<md:AssertionConsumerService
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://author/foo/saml_login"
index="1" />

 

Would I be able to authenticate to the author at https://author/foo with the IdP and continue to use local authentication at https://author ? The documentation quoted mentions a /content/... path though also talks about non-existing paths (as in /foo) - will purely non-existing paths (like /foo) work?

Is it possible to have multiple of these IdP elements? For example, If I wanted to also have SAML authentication at https://author/bar - could I ask the IdP to have an additional element like:

 

<md:AssertionConsumerService
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://author/bar/saml_login"
index="1" />

 

Or if SAML doesn't allow the IdP to have multiple of these, would using a single prefix like:

 

<md:AssertionConsumerService
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://author/foo/saml_login"
index="1" />

 

And then asking users to either login at https://author/foo or https://author/foo/bar work? Assume /foo and /foo/bar are specified in "Path" parameter of the "Adobe Granite SAML 2.0 Authentication Handler" config.

Thanks for any info!

1 Accepted Solution

Avatar

Correct answer by
Level 3

@this-that-the-otter The default redirect is what I would consider a fallback strategy in the case that the SAML request didn't provide a redirect path. My understanding of the SAML process is not extensive but I believe the workflow is this:

  1. AEM determines if the user is authenticated correctly for the provided path (in the SAML configuration)
  2. If authentication is required, a new request is signed using the IdP public certificate in the Trust Store
  3. The current path, e.g. /foo/something, is stored in a cookie called saml_request_path
  4. The default Sling Authentication handler uses its SAML extension to validate the cookie, otherwise use the default redirect set in the SAML configuration

I believe within this process that the IdP could in theory change the redirect but I'm merely suggesting that as a theory because I can't see the inner workings of how the SAML bundle works in AEM; more so based on the open source parts of Sling.

Regarding the sign-out behaviour, AEM doesn't know what type of user is being signed out. To be clear, AEM doesn't actually handle sign-out at all as this is controlled via the Sling Authentication handler again. The reason you would see the standard Granite login screen is that it is the default path set in one of the many OSGi configurations for authentication. From memory, I don't believe you can provide context to it either as it is just a single-line text input.

Extending on what I mentioned earlier, creating a Sling Filter would help with the redirect once authenticated via SAML which I will put some example code together and post through a bit later today. In terms of sign-out, that may be possible to handle via a Sling Filter too but it would almost feel like a hack to me. What would be better is finding the configuration and then having it point to a custom resource that handles the logout behaviour as you desire.

Once I have done some further digging I'll provide some additional details.

View solution in original post

7 Replies

Avatar

Level 3

@this-that-the-otter I'm pretty sure I understand the desired outcome so let's break things down.

  1. You only require SAML for a non-default author URL, i.e /foo
  2. You want the standard AEM authentication to remain for /

If that is correct then what you can do is this, keep the IdP Consumer URL as https://author/foo/saml_login and set your com.adobe.granite.auth.saml.SamlAuthenticationHandler configuration to:

  1. path = /foo
  2. defaultRedirectUrl = /aem/start.html

What this will do is ensure requests outside the scope of /foo (https://author/) use AEMs default auth handler while requests to https://author/foo use your SAML configuration but ultimately take the user back to the start screen. The redirect URL can be whatever you like provided that your IdP isn't forcing its own redirect URL.

If this doesn't work I will do some more in-depth local testing to understand how the Sling authentication handler deals with two type of authentication scenarios.

Avatar

Level 4

Here's what happened after the IdP team changed the consumer URL from https://author/saml_login to https://author/foo/saml_login and after I updated the protected path to /foo in the SAML config as well as changing the redirect to /aem/start.html.

 

/foo redirects to the IdP, after successful authentication, the IdP redirects back to AEM. A login session is created but the /aem/start.html redirect doesn't trigger on login. Instead, the URL remains /foo and a 404 is returned. If I go to /aem/start.html I am logged in.

 

The "Sign Out" button returns the user to the default login page and not the SAML configured "Logout URL" and - clicking the Sign Out link doesn't quite sign the user out from the IdP.

 

Returning to /foo after clicking Sign Out returns the same 404. I think here it quickly rechecks the session w/ IdP and IdP says: it's there. Going to /aem/start.html, the session/login remains.

 

Notably, the user does seem logged out until returning to /foo where again, it looks like the IdP is consulted and returns the session to AEM. After which, /aem/start.html is working and user is logged in.

If after clicking "Sign Out" the user goes to / or /aem/start.html - they are (logged out) and directed to the default login page. Maybe this aspect is expected behavior.

 

I am checking to see if the IdP imposes its own redirect, as you noted, in case that might be affecting things.

Avatar

Level 3

@this-that-the-otter I had a feeling that the redirect behaviour might not be within the control of AEM. First check with the IdP if they are able to configure a custom redirect with the SAML response. If they are unable to do this then I would suggest creating a Sling Filter that intercepts the SAML response and handles the redirect manually. It is not the most eloquent solution by any stretch of the imagination, but it beats building a custom Sling Authenticator.

The "Sign Out" button is a little more tricky to configure depending on what your IdP supports. The SAML configuration does have the ability to configure the logout behaviour via Handle Logout (handleLogout) and Logout URL (logoutUrl). Some IdP's use the same IdP URL (idpUrl) for the Logout URL but check with them to see what they support.

Beyond this configuration, it is not possible (to my knowledge) to change the logout behaviour in AEM without building a custom Sling Authenticator. The best place to start is with the IdP and SAML configurations to identify what you can do without custom code.

I also forgot to talk about multiple Consumer URLs, as far as I'm aware is not supported by a single configuration, but rather individual configurations in AEM while your IdP may support multiple Consumer URLs for the same app.

Avatar

Level 4

@cshawaus Thanks for the additional info about multiple consumer URLs.

I'll check with the IdP administrator about doing the redirect there.

When you said: I had a feeling that the redirect behaviour might not be within the control of AEM - did you mean because the IdP is doing it?

I don't think there's any specific IdP redirect happening. It seems to redirect the user back to the AEM URL that initiated the login, i.e., /foo, /foo/bar, etc. - where there will be a 404 because the AEM SAML configured redirect doesn't trigger.

I changed the "default redirect" back to / from /aem/start.html - and it still does not trigger - I still get a 404 and remain on /foo.

When we're using our default SAML config, where the author is protected from the root - i.e. / - we also set the "default redirect" to / - I wodner how the "default redirect" comes into play (if at all) in this working configuration?

It's interesting the Sign Out button works and eventually uses the configuration specified IdP "Logout URL" to log the user out of the IdP when the protected URL is / - but if the protected URL is /foo - the Sign Out button seems to log the user out of AEM via the default login page: /libs/granite/core/content/login.html?resource=%2F&$$login$$=%24%24login%24%24&j_reason=unknown&j_reason_code=unknown

Does AEM keep track of users who have logged in locally vs via SAML - and send them to different logout URLs depending on which they used to login?

While I haven't heard if the IdP is doing a specific redirect currently (it doesn't seem like it) or if it is possible to do one, the IdP admin did say the following:

The SAML logout depends on what the SP can do since not all SPs implement a proper logout flow. In general, the SAML logout has two steps: first is the local logout, done by the SP and then the SP should redirect the user to the IdP where the IdP logout happens. 

Avatar

Correct answer by
Level 3

@this-that-the-otter The default redirect is what I would consider a fallback strategy in the case that the SAML request didn't provide a redirect path. My understanding of the SAML process is not extensive but I believe the workflow is this:

  1. AEM determines if the user is authenticated correctly for the provided path (in the SAML configuration)
  2. If authentication is required, a new request is signed using the IdP public certificate in the Trust Store
  3. The current path, e.g. /foo/something, is stored in a cookie called saml_request_path
  4. The default Sling Authentication handler uses its SAML extension to validate the cookie, otherwise use the default redirect set in the SAML configuration

I believe within this process that the IdP could in theory change the redirect but I'm merely suggesting that as a theory because I can't see the inner workings of how the SAML bundle works in AEM; more so based on the open source parts of Sling.

Regarding the sign-out behaviour, AEM doesn't know what type of user is being signed out. To be clear, AEM doesn't actually handle sign-out at all as this is controlled via the Sling Authentication handler again. The reason you would see the standard Granite login screen is that it is the default path set in one of the many OSGi configurations for authentication. From memory, I don't believe you can provide context to it either as it is just a single-line text input.

Extending on what I mentioned earlier, creating a Sling Filter would help with the redirect once authenticated via SAML which I will put some example code together and post through a bit later today. In terms of sign-out, that may be possible to handle via a Sling Filter too but it would almost feel like a hack to me. What would be better is finding the configuration and then having it point to a custom resource that handles the logout behaviour as you desire.

Once I have done some further digging I'll provide some additional details.

Avatar

Level 4

Hello @cshawaus - I was able to compile the SamlAuthenticationFilter.java file into SamlAuthenticationFilter.class. How should I install this into AEM for testing? 

 

Thanks!

Avatar

Level 3

@this-that-the-otter Here is something I put together quickly (untested) which will intercept the SAML POST request (/foo/saml_login) and redirect the user back to the start page.

Ensure you update the /foo/ URLs where needed.

import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.servlets.HttpConstants;
import org.apache.sling.servlets.annotations.SlingServletFilter;
import org.apache.sling.servlets.annotations.SlingServletFilterScope;
import org.apache.sling.servlets.annotations.SlingServletName;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.propertytypes.ServiceRanking;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.*;
import java.io.IOException;

@Component(service = Filter.class)
@SlingServletFilter(
	scope = SlingServletFilterScope.REQUEST,
	pattern = "/foo/saml_login",
	methods = {HttpConstants.METHOD_POST}
)
@SlingServletName(servletName = "SAML Authentication Filter")
@ServiceRanking(-700)
public class SamlAuthenticationFilter implements Filter {
	private final Logger log = LoggerFactory.getLogger(SamlAuthenticationFilter.class);

	@Override
	public void init(FilterConfig filterConfig) throws ServletException {
		log.debug("SAML authentication filter created.");
	}

	@Override
	public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
						 FilterChain filterChain) throws IOException, ServletException {
		if (!(servletRequest instanceof SlingHttpServletRequest) || !(servletResponse instanceof SlingHttpServletResponse)) {
			filterChain.doFilter(servletRequest, servletResponse);
			return;
		}

		log.debug("SAML authentication filter handing request....");

		final SlingHttpServletRequest slingRequest = (SlingHttpServletRequest) servletRequest;
		final SlingHttpServletResponse slingResponse = (SlingHttpServletResponse) servletResponse;

		// Ensure the request is actually for the SAML login handler
		if (!StringUtils.equals(slingRequest.getPathInfo(), "/foo/saml_login")) {
			filterChain.doFilter(servletRequest, servletResponse);
			return;
		}

		// Take the user back to the start page
		slingResponse.sendRedirect("/");

		filterChain.doFilter(servletRequest, servletResponse);
	}

	@Override
	public void destroy() {
		log.debug("SAML authentication filter destroyed.");
	}
}

I am still digging into a nice solution to change the default logout redirect behaviour.