Comment configurer une authentification CAS par WS Soap ?

Clement Dedenis   lun 23/12/2019 - 12:15  162 vues

Comment configurer une authentification CAS par WS Soap ?

Tout d'abord afin de configurer une authentification CAS par WS Soap nous devons :

Partir du principe que vous avez suivi le tutoriel “Comment configurer l’authentification CAS”.
Les dossiers pour l’exemple se situent à ces emplacements :

  • Pour le CAS
cas60X
  • Pour Tomcat
/opt/tomcat9.0.14

Nous pouvons accéder à Tomcat à partir du navigateur :

Pour l’exemple nous utiliserons le WS gratuit “Number Conversion Service

  • Utilisateur : Login
  • Mot de passe : Login/334

Chaque requête et réponse du protocole Soap dans CAS se définissent par une classe Java. Deux classes sont nécessaires pour faire Appel à un WebService, la requête et sa réponse.


Pour faire la connexion au CAS en utilisant une requête sur un WS Soap nous devons prendre en considération les points suivants :

Dans le module d’authentification par WS Soap il y a trois fonctions déjà implémentées : 

getSoapAuthenticationRequest
getSoapAuthenticationResponse
MapItemType

Dans notre WS nous avons les fonctions suivantes :

NumberToWords
NumberToWordsResponse

Nous devons créer des classes Java pour rendre le CAS compatible avec notre WebService Soap. Pour faire des requêtes Soap il faut faire du code XML. Nous allons créer la structure d’appel au Webservice dans un fichier nommé users.xsd.


Pour démarrer la configuration par WS Soap du CAS nous allons ajouter les dépendances dans le fichier “build.gradle”. Dans le dossier cas60X il faut ouvrir le fichier build.gradle.
Sous le commentaire  “// Other CAS dependencies/modules may be listed here...”, nous ajoutons :

compile "org.apereo.cas:cas-server-support-soap-authentication:${casServerVersion}"
compile("org.springframework.ws:spring-ws-security:3.0.6.RELEASE")

Ces deux lignes indiquent au compilateur d’importer les paquets “soap authentification” et “spring-ws-security”. Ces paquets possèdent les classes nous permettant de faire des appels à des WS Soap.

Ensuite, nous allons ajouter une ligne de configuration dans le fichier etc/cas/config/cas.properties :
Nous devons ouvrir le fichier cas.properties dans le dossier etc/cas/config/ et y ajouter la ligne :

cas.authn.soap.url=http://www.dataaccess.com/webservicesserver/numberconversion.wso

Cette ligne indique au CAS l’URL d’envoi par défaut des requêtes Soap (généralement le lien vers le WS).

Nous allons par la suite écrire les classes Java qui correspondront aux requêtes et aux réponses de notre WS. Ces classes auront des annotations @XML. Ces annotations permettent la transformation de la classe en fichier XML lors de l’envoi de la requête.
Dans le dossier cas60X/src/main nous allons créer un nouveau dossier “java/com/server/cas/demo/authentication/soap/generated” :

cd cas60X/src/main
mkdir -p java/com/server/cas/demo/authentication/soap/generated

Dans le dossier que nous avons créé nous allons instancier les classes Java correspondant aux requêtes et aux réponses de notre WS. Il est impératif que le nom du fichier, de la classe et de la requête soit le même.

Pour la requête NumberToWords :

touch java/com/server/cas/demo/authentication/soap/generated/NumberToWords.java

Dans ce fichier nous allons déclarer la classe “NumberToWords”. Dans cette classe les fonctions “getters” et “setters” doivent être déclarées pour chaque variable. Chaque variable est un élément (XML) de notre requête, dans ce cas nous avons seulement la variable “ubinum”.

NumberToWords.java :

package com.server.cas.demo.authentication.soap.generated;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;

/**
 * This is {@link NumberToWords}.
 */

@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "", propOrder = {
    "ubiNum"
})

@XmlRootElement(name = "NumberToWords")
public class NumberToWords {

    @XmlElement(required = true)
    protected String ubiNum;

    public String getUbiNum() {
        return ubiNum;
    }

    public void setUbiNum(String value) {
        this.ubiNum = value;
    }
}

Pour la réponse NumberToWordsResponse :

touch java/com/server/cas/demo/authentication/soap/generated/NumberToWordsResponse.java

Dans ce fichier on suit le même principe que pour la requête : getters/setters pour chaque variable. Chaque variable est un élément (XML) de la réponse Soap. Dans ce cas nous avons seulement la variable “NumberToWordsResult”.

NumberToWordsResult.java :

package com.server.cas.demo.authentication.soap.generated;

import java.util.ArrayList;
import java.util.List;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;

/**
 * This is {@link NumberToWordsResponse}.
 */

@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "", propOrder = {
    "NumberToWordsResult"
})

@XmlRootElement(name = "NumberToWordsResponse")
public class NumberToWordsResponse {

    protected String NumberToWordsResult;

    public String getNumberToWordsResult() {
        return NumberToWordsResult;
    }

    public void setNumberToWordsResult(final String value) {
        this.NumberToWordsResult = value;
    }
}

Suivant la création de ces classes nous allons instancier la classe ObjectFactory. Cette classe sera notre gestionnaire d’objet. Nous allons passer par cette classe pour créer nos requêtes. Nous déclarerons dans cette classe une fonction pour chaque requête que nous allons envoyer.

touch java/com/server/cas/demo/authentication/soap/generated/ObjectFactory.java

ObjectFactory.java :

package com.server.cas.demo.authentication.soap.generated;

import javax.xml.bind.annotation.XmlRegistry;

/**
 * This is {@link ObjectFactory}.
 */

@XmlRegistry
public class ObjectFactory {

    public ObjectFactory() { }

    public NumberToWords createNumberToWords() {
        return new NumberToWords();
    }

    public NumberToWordsResponse createNumberToWordsResponse() {
        return new NumberToWordsResponse();
    }
}

Ensuite nous allons ajouter le fichier “package-info.java” pour déclarer le “XmlSchema”. Nous avons besoin de déclarer “XmlSchema” car c’est l’annotation qui permet de définir l’enveloppe de la requête Soap. Nous le déclarons dans le fichier “package-info.java” pour ne pas avoir à le déclarer dans chaque fichier. Pour envoyer et recevoir les requêtes, nous avons besoin du namespace du WebService.

package-info.java :

@javax.xml.bind.annotation.XmlSchema(
    namespace = "http://www.dataaccess.com/webservicesserver/", 
    elementFormDefault = javax.xml.bind.annotation.XmlNsForm.QUALIFIED
)
package com.server.cas.demo.authentication.soap.generated;

Ensuite, nous allons continuer les modifications au service d’authentification par WS Soap. Puis nous modifierons le fonctionnement du gestionnaire des requêtes pour avoir la possibilité de faire des requêtes à notre WS Soap.
Dans le dossier java/com/server/cas/demo/authentication/ on crée le fichier TestSoapAuthenticationClient.java

touch java/com/server/cas/demo/authentication/TestSoapAuthenticationClient.java

Dans ce fichier on va définir la fonction qui permet d’envoyer la requête “NumberToWords” et de recevoir la réponse “NumberToWordsResponse”.

TestSoapAuthenticationClient.java :

package com.server.cas.demo.authentication;

import com.server.cas.demo.authentication.soap.generated.*;
import org.apereo.cas.authentication.SoapAuthenticationClient;

/**
 * This is {@link TestSoapAuthenticationClient}.
 */

public class TestSoapAuthenticationClient extends SoapAuthenticationClient {
    public NumberToWordsResponse sendNumberToWordsRequest(final NumberToWords request) {
        return (NumberToWordsResponse) getWebServiceTemplate().marshalSendAndReceive(request);
    }
}

Ensuite, nous devons modifier le fonctionnement du gestionnaire d'authentification pour nous connecter avec notre WS Soap. Créons le fichier TestSoapAuthenticationHandler.java dans le dossier java/com/server/cas/demo/authentication/.

touch java/com/server/cas/demo/authentication/TestSoapAuthenticationHandler.java

Dans ce fichier il faut redéfinir la fonction qui permet de faire l’authentification avec Soap (“authenticateUsernamePasswordInternal”).

TestSoapAuthenticationHandler.java :

package com.server.cas.demo.authentication;

import org.apereo.cas.authentication.credential.UsernamePasswordCredential;
import org.apereo.cas.authentication.principal.PrincipalFactory;
import com.server.cas.demo.authentication.soap.generated.ObjectFactory;
import org.apereo.cas.services.ServicesManager;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import javax.security.auth.login.FailedLoginException;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
import org.apereo.cas.authentication.SoapAuthenticationHandler;
import org.apereo.cas.authentication.AuthenticationHandlerExecutionResult;
import com.google.common.base.Splitter;

/**
 * This is {@link TestSoapAuthenticationHandler}.
 */

@Slf4j

public class TestSoapAuthenticationHandler extends SoapAuthenticationHandler {

    private final TestSoapAuthenticationClient soapAuthenticationClient;

    public TestSoapAuthenticationHandler(final String name, final ServicesManager servicesManager,
                                     final PrincipalFactory principalFactory, final Integer order,
                                     final TestSoapAuthenticationClient soapAuthenticationClient) {
        super(name, servicesManager, principalFactory, order, soapAuthenticationClient);
        this.soapAuthenticationClient = soapAuthenticationClient;
    }

    @Override
    protected AuthenticationHandlerExecutionResult authenticateUsernamePasswordInternal(final UsernamePasswordCredential credential,
                                                                                        final String originalPassword) throws GeneralSecurityException {
        soapAuthenticationClient.setCredentials(credential);
        if (credential.getUsername().equals("Login")) {
            val splitPassword = Splitter.on("/").splitToList(credential.getPassword());
            if (splitPassword.size() == 2) {
                if (splitPassword.get(0).trim().equals("Login")) {
                    val request = new ObjectFactory().createNumberToWords();
                    request.setUbiNum(splitPassword.get(1).trim());
                    val response = soapAuthenticationClient.sendNumberToWordsRequest(request);
                    if (response.getNumberToWordsResult().trim().equalsIgnoreCase("three hundred and thirty four")) {
                        val principal = principalFactory.createPrincipal(credential.getUsername());
                        return createHandlerResult(credential, principal, new ArrayList<>());
                    }
                }
            }
        }
        throw new FailedLoginException("Unable to authenticate " + credential.getUsername());
    }
}

Puis nous allons effectuer la configuration du coté Java, du côté Spring et du côté XML.

Nous allons maintenant définir le XML pour faire fonctionner notre Java et faire les requêtes au WS Soap. Pour cela, on crée un dossier “ressource/xsd”

mkdir -p resources/xsd

Dans ce dossier on crée un fichier “users.xsd”

touch resources/xsd/users.xsd

Dans ce fichier nous allons définir les objets correspondants à nos classes Java, mais cette  fois-ci en XML. Chaque requête est définie comme un élément du schéma XML.

users.xsd :

<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
           xmlns:tns="http://apereo.org/cas"
           targetNamespace="http://apereo.org/cas"
           elementFormDefault="qualified">
    <xs:element name="NumberToWords">
        <xs:complexType>
            <xs:sequence>
                <xs:element name="ubiNum" type="xs:string"/>
            </xs:sequence>
        </xs:complexType>
    </xs:element>
    <xs:element name="NumberToWordsResponse">
        <xs:complexType>
            <xs:sequence>
                <xs:element name="NumberToWordsResult" type="xs:string"/>
            </xs:sequence>
        </xs:complexType>
    </xs:element>
</xs:schema>

Maintenant nous allons activer les configurations. Pour cela on instancie la classe TestSoapAuthenticationConfiguration.java dans le dossier java/com/server/cas/demo/config/.

touch java/com/server/cas/demo/config/TestSoapAuthenticationConfiguration.java

Dans cette classe nous écrirons la configuration nécessaire pour que CAS reconnaisse les changements que nous avons fait sur le gestionnaire d’authentification.

TestSoapAuthenticationConfiguration.java :

package com.server.cas.demo.config;

import org.apereo.cas.authentication.AuthenticationEventExecutionPlanConfigurer;
import org.apereo.cas.authentication.AuthenticationHandler;
import org.apereo.cas.authentication.principal.PrincipalFactory;
import org.apereo.cas.authentication.principal.PrincipalFactoryUtils;
import org.apereo.cas.authentication.principal.PrincipalNameTransformerUtils;
import org.apereo.cas.authentication.principal.PrincipalResolver;
import org.apereo.cas.authentication.support.password.PasswordEncoderUtils;
import org.apereo.cas.config.SoapAuthenticationConfiguration;
import org.apereo.cas.configuration.CasConfigurationProperties;
import org.apereo.cas.services.ServicesManager;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.oxm.jaxb.Jaxb2Marshaller;
import javax.xml.bind.Marshaller;
import javax.xml.bind.helpers.DefaultValidationEventHandler;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import com.server.cas.demo.authentication.soap.generated.*;
import com.server.cas.demo.authentication.TestSoapAuthenticationClient;
import com.server.cas.demo.authentication.TestSoapAuthenticationHandler;

/**
 * This is {@link TestSoapAuthenticationConfiguration}.
 */

@Configuration("testSoapAuthenticationConfiguration")
@EnableConfigurationProperties(CasConfigurationProperties.class)
@Slf4j
public class TestSoapAuthenticationConfiguration extends SoapAuthenticationConfiguration {

    @Autowired
    private CasConfigurationProperties casProperties;

    @Autowired
    @Qualifier("servicesManager")
    private ObjectProvider<ServicesManager> servicesManager;

    @Autowired
    @Qualifier("defaultPrincipalResolver")
    private ObjectProvider<PrincipalResolver> defaultPrincipalResolver;

    @Override
    public AuthenticationHandler soapAuthenticationAuthenticationHandler() {
        val soap = casProperties.getAuthn().getSoap();
        val handler = new TestSoapAuthenticationHandler(soap.getName(),
            servicesManager.getIfAvailable(),
            soapAuthenticationPrincipalFactory(),
            soap.getOrder(),
            soapAuthenticationClient());
        handler.setPrincipalNameTransformer(PrincipalNameTransformerUtils.newPrincipalNameTransformer(soap.getPrincipalTransformation()));
        handler.setPasswordEncoder(PasswordEncoderUtils.newPasswordEncoder(soap.getPasswordEncoder()));
        return handler;

    }

    @Override
    public AuthenticationEventExecutionPlanConfigurer soapAuthenticationEventExecutionPlanConfigurer() {
        return plan -> plan.registerAuthenticationHandlerWithPrincipalResolver(soapAuthenticationAuthenticationHandler(), defaultPrincipalResolver.getIfAvailable());
    }

    @Override
    public Jaxb2Marshaller soapAuthenticationMarshaller() {
        val marshaller = new Jaxb2Marshaller();
        marshaller.setContextPath(NumberToWords.class.getPackageName());
        val props = new HashMap<String, Object>();
        props.put(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
        props.put(Marshaller.JAXB_ENCODING, StandardCharsets.UTF_8.name());
        marshaller.setMarshallerProperties(props);
        marshaller.setValidationEventHandler(new DefaultValidationEventHandler());
        return marshaller;
    }

    @Override
    public TestSoapAuthenticationClient soapAuthenticationClient() {
        val soap = casProperties.getAuthn().getSoap();
        if (StringUtils.isBlank(soap.getUrl())) {
            throw new BeanCreationException("No SOAP url is defined");
        }

        val client = new TestSoapAuthenticationClient();
        client.setMarshaller(soapAuthenticationMarshaller());
        client.setUnmarshaller(soapAuthenticationMarshaller());
        client.setDefaultUri(soap.getUrl());
        return client;
    }
}

Pour finir nous devons valider les changements de nos classes Java. Pour cela on ouvre le fichier spring.factories (fichier créé dans le précédent guide). Pour rappel ce fichier se situe à l’emplacement suivant : resources/META-INF/spring.factories. Voici notre fichier après avoir ajouté nos fichiers de configuration.

spring.factories :

org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.server.cas.demo.config.TestRegisteredServicesConfiguration,\
com.server.cas.demo.config.TestCasRemoteAuthenticationConfiguration,\
com.server.cas.demo.config.TestSoapAuthenticationConfiguration

Configurer une authentification CAS avec plusieurs WS Soap


Namespace

Pour communiquer avec plusieurs WebService Soap en utilisant le CAS il y a quelques modifications à faire.
Plusieurs WebService signifie plusieurs namespaces. Le namespace présent dans le fichier “package-info.java” est le namespace par défaut, vous pouvez définir un namespace différent pour chaque classe. Le namespace se définit dans l’annotation @XmlRootElement.

Dans la classe NumberToWords.java au lieu de la ligne :

@XmlRootElement(name = "NumberToWords")

Nous aurons la ligne :

@XmlRootElement(name = "NumberToWords", namespace = “<Your namespace>”)

Il est très important que la requête et sa réponse possèdent le même namespace. Le namespace permet au CAS de connaître les adresses autorisées lors de l’envoi et de la réception d’une requête WS Soap. Si vous oubliez d’ajouter le namespace sur la classe réponse, votre programme ne fonctionnera pas.

Uri

Si vous avez plusieurs WS alors vous devez pouvoir gérer l’envoi des requêtes à plusieurs adresses.

Dans le fichier cas.properties vous devez définir une uri de base pour le client Soap du CAS:

cas.authn.soap.url=<default uri>

Il y a deux solutions :

  1. vous déclarez <default uri> comme “”(vide) et vous spécifiez l’adresse à chaque envoi de requête.
  2. vous remplacez <default uri> par l’adresse d’un de vos WS et spécifiez l’adresse d’envoi seulement sur vos autres requêtes.

Dans le fichier TestSoapAuthentificationClient.java nous utilisons la méthode “getWebServiceTemplate().marshalSendAndReceive”. Cette méthode possède deux utilisations intéressantes en fonction de son nombre d'arguments :

(request) : envoi la requête (l’adresse utilisée est celle que vous avez défini si vous avez choisi la seconde solution)
(uri, requête) : envoi la requête à l’adresse indiquée

Conclusion

En définissant un namespace pour chaque couple de requête/réponse et en déclarant l’adresse d’envoi nous pouvons communiquer avec plusieurs Web Services, notamment WS Soap.
 

À propos Clément

Clément est le développeur qu’il vous faut ! Il s’occupe des debugs et résout tout autre problème à traiter en un temps record.
Il élabore des solutions innovantes en respectant les bonnes pratiques de programmation pour répondre à toutes vos attentes.

Son autonomie et sa rigueur

Le montage des meubles en kit

Articles liés

Ajouter un commentaire

This question is for testing whether or not you are a human visitor and to prevent automated spam submissions.