Java Opensaml me renvoie un subject null - impossible d'obtenir le nom de l'utilisateur

Le problème exposé dans ce sujet a été résolu.

Bonjour,

je fais actuellement face à un soucis assez étrange dans le cadre du développement d’une application Java EE avec JSF. Notons que je n’ai pas spring et que des impératifs techniques forcent à utiliser java 8.

Voici la situation : je désire implémenter le SSO via SAML. Comme je n’ai pas spring, impossible d’utiliser spring security, car ce dernier casse totalement le workflow de l’application. Je me suis donc rabattu sur la méthode "à la main" à base de la lib opensaml.

En m’inspirant des exemples disponibles sur ce dépôt, j’ai codé la base du système.


@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;
    }

}

code appelé quand l’utilisateur arrive sur la page d’accueil de l’application pour être redirigé vers l’IDP

La redirection se fait et va appeler le code suivant :

    @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);
            // jai besoin du user
            String user = authnRequest.getSubject().getNameID().getValue();
            // BOBJ SDK -- la raison de java 8
            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;
        }

    }
obtention du user

Mon seul soucis : getSubject retourne null.

Je me demandais donc : est-ce qu’il me manque une requête ou un attribut quelque part pour que je puisse avoir ce sujet?

Bonjour,

après plusieurs jours d’échec, et avoir même tenté une bounty sur stackoverflow, j’ai compris ce qui n’allait pas dans mon code.

Version courte :

Une fois la requête à l’IDP faite, on ne requêtait non pas le système de ticket SAML pour qu’il nous renvoie le message de réponse mais notre propre application. Forcément la requête ne contenant que la demande d’identité elle ne contient pas l’identité.

Version longue

Open Saml découvre à qui il doit demander l’identité via les lignes suivantes :

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"));

Dans mon code je dis donc que la requête de demande d’identité doit être envoyée… à moi même. Si on remplace l’url par idpEndpoint, il va commencer à fonctionner. Dans un premier temps, il va râler : il te faut une signature. Le code du "signing and verification" du dépôt que j’ai envoyé dans l’OP suffit.

Ensuite, il ne faut pas demander un NameId transient. Pour moi tout fonctionne bien avec unspecified.

Enfin, à la réception de la requête, le nom d’utilisateur sera dans :

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

Il est simplement nécessaire de vérifier que :

  • (((ResponseImpl)messageContext.getMessage()).getStatus() vaut SUCCESS
  • les signatures sont cohérentes
  • les assertions ne sont pas vides

Je passe en résolu. En espérant que ma galère aidera quelqu’un. Je vais bien sûr traduire cette réponse en anglais sur stackoverflow.

Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte