Create SAML2 SP-Initiated authnrequest using java

As of 2017, SAML is still most common standard used in enterprises for Single Sign On (SSO). SAML2 supports different flows such as IDP initiated flows and SP initiated flows for addressing different use cases for SSO federation.

I needed to implement SP initiated flow for one of internal use cases. Actually, we were deep linking from one app to another app and wanted to enforce authentication in between and also wanted to preserve deep link.

So how can we preserve deep links? does SAML standard support it? Answer is yes, below image from Okta is good overview of high level flow on how can we preserve deep links.

SP Initiated Login

High level Implementation

As noted in above diagram, Relaystate can be used to preserve deep links. We will set the RelayState of the SAML request with the deep link. When SAML response comes back, SP can use the RelayState to redirect the user to the appropriate resource.

In this example, I would just show sample Java implementation to generate SAML request using OpenSAML library.

Here is sample authN request example using HTTP get. We can also submit authN request using HTTP POST but we will not cover that in this example.

https://{IDP_APP_URL}?SAMLRequest={SAMLREQUEST_VALUE}&RelayState={RELAY_STATE_VALUE}

ex.

https://dev-552077.oktapreview.com/app/demodev552077_saml2app_1/exka9jbh72o7D4REL0h7/sso/saml?SAMLRequest=nZPdjtowEIVfJfJ9fkgCYS3CikJXRaJtBNle9M4xk8XdxE49DrBvv07ItlRq0apXkezjOTPfmczuz3XlHEGjUDIlIy8gDkiu9kI%2BpeQxf3Cn5H4%2BQ1ZXDV205iC38LMFNI59J5H2FylptaSKoUAqWQ1IDae7xecNDb2ANloZxVVFnAUiaGONlkpiW4PegT4KDo%2FbTUoOxjRIfb9ST0J6XNUNky%2Fd10dU%2BmLqi33jq2fDiPOgNIe%2BoZSUrEIgznqVkigCXjA2dqPyjrnxJJi6RVmUbhJHfBIW0zgZR1aJGUMUR%2Fj9FrGFtUTDpElJGIwSNxi7wSQPRzQa01HgRXH0nTjZMMwHIS%2BIbk1eXERIP%2BV55mZfdzlxvr2htgJyARvS3l1fIQ1vF2ZvIMm8w2apnU4nrwPTA4PzM7v7URySUCWrePtxExySmX9tNRg39IutvV5lqhL85dr%2F%2FZlWlTotNTBjYRrdQh9NzcztAt2J2LtlL6VNBwUNSEP8X60Niwb7PmW7MgbO5r96XHbLpAV22OHMuBnA0%2BvKy8pS3UJ55fDuEG7KOOVdaXvcLd1J6X23RMDtZLlmEhulzSWdv%2FYzH5L7B5Dh%2Bs%2Bfc%2F4K&RelayState=http%3A%2F%2Fapp1.company.com%3FarticleId%3D1234

Other thing to learn is that once we generate the SAMLRequest in XML form, It’s needed to be compressed, base64 encoded, url encoded so that it can be correctly decoded by the IDP.

Below is the sample code. Full code is present @ https://github.com/sunieldalal/loginapp or can be cloned using below command.

git clone https://github.com/sunieldalal/loginapp

Add AuthNRequest Builder


back to top

AuthNRequestBuilder.java
package com.slabs.login.service.login;
import org.opensaml.common.SAMLVersion;
import org.opensaml.saml2.core.AuthnContextClassRef;
import org.opensaml.saml2.core.AuthnContextComparisonTypeEnumeration;
import org.opensaml.saml2.core.AuthnRequest;
import org.opensaml.saml2.core.Issuer;
import org.opensaml.saml2.core.NameIDPolicy;
import org.opensaml.saml2.core.RequestedAuthnContext;
import org.opensaml.saml2.core.impl.AuthnContextClassRefBuilder;
import org.opensaml.saml2.core.impl.AuthnRequestBuilder;
import org.opensaml.saml2.core.impl.IssuerBuilder;
import org.opensaml.saml2.core.impl.NameIDPolicyBuilder;
import org.opensaml.saml2.core.impl.RequestedAuthnContextBuilder;

import java.util.UUID;
import org.joda.time.DateTime;

public class AuthNRequestBuilder {

  private static final String SAML2_NAME_ID_POLICY = "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent";
  private static final String SAML2_PROTOCOL = "urn:oasis:names:tc:SAML:2.0:protocol";
  private static final String SAML2_POST_BINDING = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST";
  private static final String SAML2_PASSWORD_PROTECTED_TRANSPORT = "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport";
  private static final String SAML2_ASSERTION = "urn:oasis:names:tc:SAML:2.0:assertion";

  /**
   * Generate an authentication request.
   *
   * @return AuthnRequest Object
   */
  public AuthnRequest buildAuthenticationRequest(String assertionConsumerServiceUrl, String issuerId) {

      //Generate ID
      DateTime issueInstant = new DateTime();
      AuthnRequestBuilder authRequestBuilder = new AuthnRequestBuilder();
      AuthnRequest authRequest = authRequestBuilder.buildObject(SAML2_PROTOCOL, "AuthnRequest", "samlp");
      authRequest.setForceAuthn(Boolean.FALSE);
      authRequest.setIsPassive(Boolean.FALSE);
      authRequest.setIssueInstant(issueInstant);
      authRequest.setProtocolBinding(SAML2_POST_BINDING);
      authRequest.setAssertionConsumerServiceURL(assertionConsumerServiceUrl);
      authRequest.setIssuer(buildIssuer( issuerId));
      authRequest.setNameIDPolicy(buildNameIDPolicy());
      authRequest.setRequestedAuthnContext(buildRequestedAuthnContext());
      authRequest.setID(UUID.randomUUID().toString());
      authRequest.setVersion(SAMLVersion.VERSION_20);

      return authRequest;
  }

  /**
   * Build the issuer object
   *
   * @return Issuer object
   */
  private static Issuer buildIssuer(String issuerId) {
      IssuerBuilder issuerBuilder = new IssuerBuilder();
      Issuer issuer = issuerBuilder.buildObject();
      issuer.setValue(issuerId);
      return issuer;
  }

  /**
   * Build the NameIDPolicy object
   *
   * @return NameIDPolicy object
   */
  private static NameIDPolicy buildNameIDPolicy() {
      NameIDPolicy nameIDPolicy = new NameIDPolicyBuilder().buildObject();
      nameIDPolicy.setFormat(SAML2_NAME_ID_POLICY);
      nameIDPolicy.setAllowCreate(Boolean.TRUE);
      return nameIDPolicy;
  }

  /**
   * Build the RequestedAuthnContext object
   *
   * @return RequestedAuthnContext object
   */
  private static RequestedAuthnContext buildRequestedAuthnContext() {

    //Create AuthnContextClassRef
    AuthnContextClassRefBuilder authnContextClassRefBuilder = new AuthnContextClassRefBuilder();
    AuthnContextClassRef authnContextClassRef =
      authnContextClassRefBuilder.buildObject(SAML2_ASSERTION, "AuthnContextClassRef", "saml");
    authnContextClassRef.setAuthnContextClassRef(SAML2_PASSWORD_PROTECTED_TRANSPORT);

    //Create RequestedAuthnContext
    RequestedAuthnContextBuilder requestedAuthnContextBuilder = new RequestedAuthnContextBuilder();
    RequestedAuthnContext requestedAuthnContext =
      requestedAuthnContextBuilder.buildObject();
    requestedAuthnContext.setComparison(AuthnContextComparisonTypeEnumeration.EXACT);
    requestedAuthnContext.getAuthnContextClassRefs().add(authnContextClassRef);

    return requestedAuthnContext;
  }

}

Add Logic to Generate SAML2 compliant AuthN SAML request


back to top

LoginService.java
package com.slabs.login.service.login;

public interface LoginService {

  public String getAuthNRedirectUrl(String idpAppURL, String relayState, String assertionConsumerServiceUrl,
      String issuerId);
}
LoginServiceImpl.java
package com.slabs.login.service.login;

import org.opensaml.DefaultBootstrap;
import org.opensaml.saml2.core.AuthnRequest;
import org.opensaml.xml.io.Marshaller;
import org.opensaml.xml.util.Base64;
import org.opensaml.xml.util.XMLHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.io.ByteArrayOutputStream;
import java.io.StringWriter;
import java.net.URLEncoder;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;

@Service
public class LoginServiceImpl implements LoginService {

  private static Logger LOGGER = LoggerFactory.getLogger(LoginServiceImpl.class.getName());

  {
    /* Initializing the OpenSAML library, Should be in some central place */
    try {
      DefaultBootstrap.bootstrap();
    }
    catch(Exception ex){
      LOGGER.error("Unable to initialize SAML", ex);
      throw new RuntimeException("Unable to initialize SAML");
    }
  }
  /*
  * Return's redirectUrl
  *  - Creates SAML2 AuthN object
  *  - Compresses it
  *  - Base 64 encode it
  *  - URL encode it
  *  - Appends RelayState
  */
  @Override
  public String getAuthNRedirectUrl(String idpAppURL, String relayState, String assertionConsumerServiceUrl, String issuerId){

    LOGGER.info("idpAppURL=" + idpAppURL + " relayState=" + relayState + " assertionConsumerServiceUrl=" + assertionConsumerServiceUrl + " issuerId=" + issuerId);
    String url = null;

    try {

      AuthNRequestBuilder authNRequestBuilder = new AuthNRequestBuilder();
      AuthnRequest authRequest = authNRequestBuilder.buildAuthenticationRequest(assertionConsumerServiceUrl, issuerId);
      String samlRequest = generateSAMLRequest(authRequest);

      // Prepare final Url
      url = idpAppURL + "?SAMLRequest=" + samlRequest + "&RelayState=" + URLEncoder.encode(relayState,"UTF-8");

    } catch (Exception ex) {
      LOGGER.error("Exception while creating AuthN request - " + ex.getMessage(), ex);
      throw new RuntimeException("Unable to generate redirect Url");
    }

    LOGGER.debug("redirect url is = " + url);
    return url;

  }


  /*
   * Converts AuthN object to xml, compresses it, base64 encode it and url encode it
   */
  private String generateSAMLRequest(AuthnRequest authRequest) throws Exception {

    Marshaller marshaller = org.opensaml.Configuration.getMarshallerFactory().getMarshaller(authRequest);
    org.w3c.dom.Element authDOM = marshaller.marshall(authRequest);
    StringWriter rspWrt = new StringWriter();
    XMLHelper.writeNode(authDOM, rspWrt);
    String messageXML = rspWrt.toString();

    Deflater deflater = new Deflater(Deflater.DEFLATED, true);
    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
    DeflaterOutputStream deflaterOutputStream = new DeflaterOutputStream(byteArrayOutputStream, deflater);
    deflaterOutputStream.write(messageXML.getBytes());
    deflaterOutputStream.close();
    String samlRequest = Base64.encodeBytes(byteArrayOutputStream.toByteArray(), Base64.DONT_BREAK_LINES);
    return URLEncoder.encode(samlRequest,"UTF-8");

  }
}

Hope it helps to save some time!

References


  • Following Okta link provide good overview of SP initiated login <http://developer.okta .com/standards/SAML/#understanding-sp-initiated-login-flow>
  • This example is inspired from <http://www.john-james-andersen .com/blog/programming/sample-saml-2-0-authnrequest-in-java.html>.

Version History


Date Description
2017-05-06    Initial Version