javasamljava-ee-8opensaml

Java Opensaml 3.4.6 : authnrequest subject is null - impossible to get user name


Developing a Java EE/JSF application, I am trying to include SAML sso functionality into it. Due to technical requirements (SAP BOBJ SDK) I need to use java 8, so I must stick with opensaml 3.x branch. As the application is some years old, I cannot add spring/spring-security to it just for SAML, that's why my code focuses on raw opensaml usage.

Mimicking the example code of this repository, I implemented the authentication basics:

This first code is called when I reach the "login" page. And send the AuthnRequest to my IDP


@Log4j2
@Named
public class SAMLAuthForWPBean implements Serializable {

    private static final BasicParserPool PARSER_POOL = new BasicParserPool();

    static {
        PARSER_POOL.setMaxPoolSize(100);
        PARSER_POOL.setCoalescing(true);
        PARSER_POOL.setIgnoreComments(true);
        PARSER_POOL.setIgnoreElementContentWhitespace(true);
        PARSER_POOL.setNamespaceAware(true);
        PARSER_POOL.setExpandEntityReferences(false);
        PARSER_POOL.setXincludeAware(false);

        final Map<String, Boolean> features = new HashMap<>();
        features.put("http://xml.org/sax/features/external-general-entities", Boolean.FALSE);
        features.put("http://xml.org/sax/features/external-parameter-entities", Boolean.FALSE);
        features.put("http://apache.org/xml/features/disallow-doctype-decl", Boolean.TRUE);
        features.put("http://apache.org/xml/features/validation/schema/normalized-value", Boolean.FALSE);
        features.put("http://javax.xml.XMLConstants/feature/secure-processing", Boolean.TRUE);

        PARSER_POOL.setBuilderFeatures(features);
        PARSER_POOL.setBuilderAttributes(new HashMap<>());

    }

    private String idpEndpoint = "url de azure por";
    private String entityId = "glados";
    private boolean isLogged;

    @Inject
    private LoginBean loginBean;
    @Inject
    private MainBean mainBean;
    @Inject
    private TechnicalConfigurationBean technicalConfigurationBean;

    @PostConstruct
    public void init() {
        if (!PARSER_POOL.isInitialized()) {
            try {
                PARSER_POOL.initialize();
            } catch (ComponentInitializationException e) {
                LOGGER.error("Could not initialize parser pool", e);
            }
        }
        XMLObjectProviderRegistry registry = new XMLObjectProviderRegistry();
        ConfigurationService.register(XMLObjectProviderRegistry.class, registry);
        registry.setParserPool(PARSER_POOL);
        // forge auth endpoint
    }

    public boolean needLogon() {
        return isLogged;
    }

    public void createRedirection(HttpServletRequest request, HttpServletResponse response)
            throws MessageEncodingException,
            ComponentInitializationException, ResolverException {
        // see this link to build authnrequest with metadata https://blog.samlsecurity.com/2011/01/redirect-with-authnrequest-opensaml2.html
        init();
        AuthnRequest authnRequest;
        authnRequest = OpenSAMLUtils.buildSAMLObject(AuthnRequest.class);
        authnRequest.setIssueInstant(DateTime.now());
        FilesystemMetadataResolver metadataResolver = new FilesystemMetadataResolver(new File("wp.metadata.xml"));
        metadataResolver.setParserPool(PARSER_POOL);
        metadataResolver.setRequireValidMetadata(true);
        metadataResolver.setId(metadataResolver.getClass().getCanonicalName());
        metadataResolver.initialize();

        /*
         * EntityDescriptor urlDescriptor = metadataResolver.resolveSingle( new CriteriaSet( new BindingCriterion(
         * Arrays.asList("urn:oasis:names:tc:SAML:2.0:bindings:metadata"))));
         */
        /*entityId = "https://192.168.50.102:8443/360.suite/loginSAML.xhtml";*/
        entityId = "glados";

                //idp endpoint, je pense => à obtenir des metadata
        authnRequest.setDestination(idpEndpoint);

        authnRequest.setProtocolBinding(SAMLConstants.SAML2_POST_BINDING_URI);
        // app endpoint
        authnRequest.setAssertionConsumerServiceURL("https://192.168.1.14:8443/360.suite/loginSAML.xhtml");
        authnRequest.setID(OpenSAMLUtils.generateSecureRandomId());
        authnRequest.setIssuer(buildIssuer());
        authnRequest.setNameIDPolicy(buildNameIdPolicy());

        MessageContext context = new MessageContext();
        context.setMessage(authnRequest);
        SAMLPeerEntityContext peerEntityContext = context.getSubcontext(SAMLPeerEntityContext.class, true);
        SAMLEndpointContext endpointContext = peerEntityContext.getSubcontext(SAMLEndpointContext.class, true);
        endpointContext.setEndpoint(URLToEndpoint("https://192.168.1.14:8443/360.suite/loginSAML.xhtml"));
        VelocityEngine velocityEngine = new VelocityEngine();
        velocityEngine.setProperty("resource.loader", "classpath");
        velocityEngine.setProperty("classpath.resource.loader.class",
                "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader");
        velocityEngine.init();
        HTTPPostEncoder encoder = new HTTPPostEncoder();
        encoder.setVelocityEngine(velocityEngine);
        encoder.setMessageContext(context);
        encoder.setHttpServletResponse(response);

        encoder.initialize();
        encoder.encode();

    }

    public String doSAMLLogon(HttpServletRequest request, HttpServletResponse response) {

        isLogged = true;
        technicalConfigurationBean.init();
        return loginBean.generateSSOSession(request, technicalConfigurationBean.getSsoPreferences(),
                new SamlSSO(technicalConfigurationBean.getCmsPreferences().getCms()));
    }

    private NameIDPolicy buildNameIdPolicy() {
        NameIDPolicy nameIDPolicy = OpenSAMLUtils.buildSAMLObject(NameIDPolicy.class);
        nameIDPolicy.setAllowCreate(true);
        nameIDPolicy.setFormat(NameIDType.TRANSIENT);
        return nameIDPolicy;
    }

    private Endpoint URLToEndpoint(String URL) {
        SingleSignOnService endpoint = OpenSAMLUtils.buildSAMLObject(SingleSignOnService.class);
        endpoint.setBinding(SAMLConstants.SAML2_REDIRECT_BINDING_URI);
        endpoint.setLocation(URL);

        return endpoint;
    }

    private Issuer buildIssuer() {
        Issuer issuer = OpenSAMLUtils.buildSAMLObject(Issuer.class);
        issuer.setValue(entityId);

        return issuer;
    }

}

The redirect is successfully processed and the IDP sends back a POST request to my application that call this code :

    @Override
    public IEnterpriseSession logon(HttpServletRequest request) throws SDKException, Three60Exception {

        HTTPPostDecoder decoder = new HTTPPostDecoder();
        decoder.setHttpServletRequest(request);
        AuthnRequest authnRequest;
        try {
            decoder.initialize();

            decoder.decode();
            MessageContext messageContext = decoder.getMessageContext();

            authnRequest = (AuthnRequest) messageContext.getMessage();
            OpenSAMLUtils.logSAMLObject(authnRequest);
            // Here I Need the user
            String user = authnRequest.getSubject().getNameID().getValue();
            // BOBJ SDK
            String secret = TrustedSso.getSecret();
            ISessionMgr sm = CrystalEnterprise.getSessionMgr();
            final ITrustedPrincipal trustedPrincipal = sm.createTrustedPrincipal(user, cms, secret);
            return sm.logon(trustedPrincipal);
        } catch (ComponentInitializationException | MessageDecodingException e) {
            return null;
        }

    }

The issue here is that getSubject() is null on this query.

What did I miss here? Do I need to perform other requests? Do I need to add other configuration in my AuthnRequest?


Solution

  • As stated in the comment, I found the reason why my code was not working. As I also asked this question on a french forum, can can find the translation of this answer here.

    Short answer :

    Opensaml knows where to send the authn request thanks to the SAMLPeerEntityContext. In my code I put my own application as the target of this request instead of using the idp HTTP-POST bind endpoint. Once this was changed, everything worked, the idp was answering back the SAMLResponse with proper name.

    Long version

    On my code, I was building the entity context like this :

    SAMLPeerEntityContext peerEntityContext = context.getSubcontext(SAMLPeerEntityContext.class, true);
            SAMLEndpointContext endpointContext = peerEntityContext.getSubcontext(SAMLEndpointContext.class, true);
            endpointContext.setEndpoint(URLToEndpoint("https://192.168.1.14:8443/360.suite/loginSAML.xhtml"));
    

    This code forces the authn request to be sent to my own application instead of the IDP. As this is the request, it cannot contain the identity.

    If I replace this URL by idpEndpoint which I got from the IDP metadata file, the full workflow works as expected. First something will not work as my IDP forces requests to be signed, so I need to add a signature part.

    The "signing and verification" sample of this repository just works for that.

    Then, as I need a real identity, I must NOT ask for a transient nameid. In my tests, UNSPECIFIED worked, but PERSISTENT should also make it.

    Last, in the ACS receiver, I do NOT receive an authn request but a SAMLResponse with assertions. The code will therefore look like :

    String userName =
                        ((ResponseImpl) messageContext.getMessage()).getAssertions().get(0).getSubject().getNameID()
                                .getValue();
    

    I simplified the code but one, of course, has to check that :

    Thanks @identigral for your answer in the comment