CAS SSO avec Spring Security

CAS SSO avec Spring Security

1. Vue d'ensemble

Dans cet article, nous allons examinerintegrating the Central Authentication Service (CAS) with Spring Security. CAS est un service d'authentification unique (SSO).

Supposons que nous ayons des applications nécessitant une authentification utilisateur. La méthode la plus courante consiste à implémenter un mécanisme de sécurité pour chaque application. Cependant, il vaut mieux mettre en œuvre l'authentification utilisateur pour toutes les applications en un seul endroit.

C’est précisément ce que fait le système CAS SSO. Cearticle donne plus de détails sur l'architecture. Le diagramme de protocole peut être trouvéhere.

2. Configuration et installation du projet

Il y a au moins deux composants impliqués dans la configuration d'un service d'authentification central. Un composant est un serveur basé sur Spring - appelécas-server. Les autres composants sont constitués d'un ou plusieurs clients.

Un client peut être n'importe quelle application Web utilisant le serveur pour l'authentification.

2.1. Configuration du serveur CAS

Le serveur utilise le style Maven (Gradle) War Overlay pour faciliter la configuration et le déploiement. Il existe un modèle de démarrage rapide qui peut être cloné et utilisé.

Clonons-le:

git clone https://github.com/apereo/cas-overlay-template.git cas-server

Cette commande clone lescas-overlay-template dans le répertoirecas-server sur la machine locale.

Ensuite, ajoutons des dépendances supplémentaires à la racinepom.xml. Ces dépendances permettent l'enregistrement du service via une configuration JSON.

En outre, ils facilitent les connexions à la base de données:


    org.apereo.cas
    cas-server-support-json-service-registry
    ${cas.version}


    org.apereo.cas
    cas-server-support-jdbc
    ${cas.version}


    org.apereo.cas
    cas-server-support-jdbc-drivers
    ${cas.version}

La dernière version des dépendancescas-server-support-json-service-registry,cas-server-support-jdbc etcas-server-support-jdbc-drivers est disponible sur Maven Central. Veuillez noter que le parentpom.xml gère automatiquement les versions d'artefact.

Ensuite, créons le dossiercas-server/src/main/resources et copions le dossiercas-server/etc. dans ça. Nous allons également modifier le port de l'application ainsi que le chemin du magasin de clés SSL.

Nous les configurons en éditant les entrées associées danscas-server/src/main/resources/application.properties:

server.port=6443
server.ssl.key-store=classpath:/etc/cas/thekeystore
standalone.config=classpath:/etc/cas/config

Le chemin du dossier de configuration a également été défini surclasspath:/etc/cas/config. Il pointe vers lescas-server/src/main/resources/etc/cas/config.

L'étape suivante consiste à générer un magasin de clés SSL local. Le magasin de clés est utilisé pour établir des connexions HTTPS. Cette étape est importante et ne peut être ignorée.

Depuis le terminal, changez le répertoire encas-server/src/main/resources/etc/cas. Après cela, exécutez la commande suivante:

keytool -genkey -keyalg RSA -alias thekeystore -keystore thekeystore
-storepass changeit -validity 360 -keysize 2048

It’s important to use localhost when prompted for a first and last name, nom de l'organisation et même unité d'organisation. Ne pas le faire peut entraîner une erreur lors de l'établissement de la liaison SSL. D'autres champs tels que la ville, l'état et le pays peuvent être définis en fonction des besoins.

La commande ci-dessus génère un magasin de clés avec le nomthekeystore et le mot de passechangeit. Il est stocké dans le répertoire courant.

Ensuite, le magasin de clés généré doit être exporté au format.crt pour être utilisé par les applications clientes. Donc, toujours dans le même répertoire, exécutez la commande suivante pour exporter le fichierthekeystore généré versthekeystore.crt. Le mot de passe reste inchangé:

keytool -export -alias thekeystore -file thekeystore.crt
-keystore thekeystore

Maintenant, importons lesthekeystore.crt exportés dans le magasin de clés Javacacerts. L'invite du terminal doit toujours se trouver dans le répertoirecas-server/src/main/resources/etc/cas.

A partir de là, exécutez la commande:

keytool -import -alias thekeystore -storepass changeit -file thekeystore.crt
 -keystore "C:\Program Files\Java\jdk1.8.0_152\jre\lib\security\cacerts"

Pour être tout à fait sûr, nous pouvons également importer le certificat dans un JRE situé en dehors de l'installation de JDK:

keytool -import -alias thekeystore -storepass changeit -file thekeystore.crt
-keystore "C:\Program Files\Java\jre1.8.0_152\lib\security\cacerts"

Notez que l'indicateur-keystore pointe vers l'emplacement du magasin de clés Java sur la machine locale. Cet emplacement peut être différent en fonction de l'installation de Java.

De plus,ensure that the JRE that is referenced as the location of the key store is the same as the one that is used for the client application.

Après avoir ajouté avec succèsthekeystore.crt au magasin de clés Java, nous devons redémarrer le système. De manière équivalente, nous pouvons tuer toutes les instances de la machine virtuelle Java s'exécutant sur la machine locale.

Ensuite, à partir du répertoire racine du projet,cas-server, appelle les commandesbuild package etbuild run depuis le terminal. Le démarrage du serveur peut prendre un certain temps. Lorsqu'il est prêt, il imprime READY dans la console.

À ce stade, la visite dehttps://localhost:6443/cas avec un navigateur génère un formulaire de connexion. Le nom d'utilisateur par défaut estcasuser et le mot de passe estMellon.

2.2. Configuration du client CAS

Utilisons lesSpring Initializr pour générer le projet avec les dépendances suivantes: Web, Sécurité, Freemarker et éventuellement DevTools.

En plus des dépendances générées par Spring Initializr, ajoutons la dépendance pour le module Spring Security CAS:


    org.springframework.security
    spring-security-cas

La dernière version de la dépendance peut être trouvée surMaven Central. Configurons également le port du serveur pour écouter sur le port 9000 en ajoutant l’entrée suivante dansapplication.properties:

server.port=9000

3. Enregistrement des services / clients avec CAS Server

Le serveur n'autorise pas n'importe quel client à y accéder pour l'authentification. Lesclients/services must be registered in the CAS server services registry.

Il existe plusieurs façons d’enregistrer un service auprès du serveur. Il s'agit notamment de YAML, JSON, Mongo, LDAP etothers.

Selon la méthode, il existe des dépendances à inclure dans le fichierpom.xml. Dans cet article, nous utilisons la méthode du registre de service JSON. La dépendance était déjà incluse dans le fichierpom.xml de la section précédente.

Créons un fichier JSON contenant la définition de l'application cliente. Dans le dossiercas-server/src/main/resources, créons un autre dossier -services. C'est ce dossierservices qui contient les fichiers JSON.

Ensuite, nous créons un fichier JSON nommé,casSecuredApp-19991.json dans le répertoirecas-server/src/main/resources/services avec le contenu suivant:

{
    "@class" : "org.apereo.cas.services.RegexRegisteredService",
    "serviceId" : "^http://localhost:9000/login/cas",
    "name" : "CAS Spring Secured App",
    "description": "This is a Spring App that usses the CAS Server for it's authentication",
    "id" : 19991,
    "evaluationOrder" : 1
}

L'attributserviceId définit un modèle d'URL regex pour l'application cliente qui a l'intention d'utiliser le serveur pour l'authentification. Dans ce cas, le modèle correspond à une application exécutée sur localhost et à l'écoute sur le port 9000.

L'attributid doit être unique pour éviter les conflits et le remplacement accidentel des configurations. Le nom du fichier de configuration du service suit la conventionserviceName-id.json. D'autres attributs configurables tels quetheme,proxyPolicy,logo,privacyUrl et d'autres peuvent être trouvéshere.

Pour l'instant, ajoutons simplement deux éléments de configuration supplémentaires pour activer le registre de service JSON. L'une consiste à informer le serveur du répertoire dans lequel se trouvent les fichiers de configuration du service. L'autre consiste à activer l'initialisation du registre de service à partir de fichiers de configuration JSON.

Ces deux éléments de configuration sont placés dans un autre fichier, nommécas.properties. Nous créons ce fichier dans le répertoirecas-server /src/main/resources:

cas.serviceRegistry.initFromJson=true
cas.serviceRegistry.config.location=classpath:/services

Exécutons à nouveau la commandebuild run et notons les lignes telles que «Loaded [3] service(s) from [JsonServiceRegistryDao]» sur la console.

4. Configuration de sécurité de printemps

4.1. Configuration de l'authentification unique

Maintenant que l’application Spring Boot a été enregistrée avec le serveur CAS en tant que service. Soitconfigure Spring Security to work in concert with the server pour l’authentification des utilisateurs. La séquence complète des interactions entre Spring Security et le serveur peut être trouvéehere.

Commençons par configurer les beans associés au module CAS de Spring Security. Cela permet à Spring Security de collaborer avec le service d'authentification central.

Dans cette mesure, nous devons ajouter des beans de configuration à la classeCasSecuredAppApplication le point d'entrée de l'application Spring Boot:

@Bean
public ServiceProperties serviceProperties() {
    ServiceProperties serviceProperties = new ServiceProperties();
    serviceProperties.setService("http://localhost:9000/login/cas");
    serviceProperties.setSendRenew(false);
    return serviceProperties;
}

@Bean
@Primary
public AuthenticationEntryPoint authenticationEntryPoint(
  ServiceProperties sP) {

    CasAuthenticationEntryPoint entryPoint
      = new CasAuthenticationEntryPoint();
    entryPoint.setLoginUrl("https://localhost:6443/cas/login");
    entryPoint.setServiceProperties(sP);
    return entryPoint;
}

@Bean
public TicketValidator ticketValidator() {
    return new Cas30ServiceTicketValidator(
      "https://localhost:6443/cas");
}

@Bean
public CasAuthenticationProvider casAuthenticationProvider() {

    CasAuthenticationProvider provider = new CasAuthenticationProvider();
    provider.setServiceProperties(serviceProperties());
    provider.setTicketValidator(ticketValidator());
    provider.setUserDetailsService(
      s -> new User("casuser", "Mellon", true, true, true, true,
        AuthorityUtils.createAuthorityList("ROLE_ADMIN")));
    provider.setKey("CAS_PROVIDER_LOCALHOST_9000");
    return provider;
}

Nous configurons le beanServiceProperties avec l'URL de connexion au service par défaut vers laquelle lesCasAuthenticationFilter seront mappés en interne. La propriétésendRenew deServiceProperties est définie surfalse. En conséquence, un utilisateur n'a besoin de présenter ses identifiants de connexion qu'une seule fois au serveur.

L'authentification ultérieure sera effectuée automatiquement, c'est-à-dire sans que l'utilisateur ait à nouveau besoin du nom d'utilisateur et du mot de passe. Ce comportement signifie qu'un seul utilisateur ayant accès à plusieurs services utilisant le même serveur pour l'authentification.

Comme nous le verrons plus tard, si un utilisateur se déconnecte complètement du serveur, son ticket est invalidé. En conséquence, l'utilisateur est déconnecté de toutes les applications connectées au serveur en même temps. C'est ce qu'on appelle la déconnexion unique.

Nous configurons le beanAuthenticationEntryPoint avec l'URL de connexion par défaut du serveur. Notez que cette URL est différente de l'URL de connexion au service. Cette URL de connexion au serveur est l'emplacement vers lequel l'utilisateur sera redirigé pour s'authentifier.

LeTicketValidator est le bean que l'application de service utilise pour valider un ticket de service accordé à un utilisateur après une authentification réussie auprès du serveur.

Le flux est:

  1. Un utilisateur tente d'accéder à une page sécurisée

  2. LeAuthenticationEntryPoint est déclenché et amène l'utilisateur vers le serveur. L'adresse de connexion du serveur a été spécifiée dans lesAuthenticationEntryPoint

  3. En cas d'authentification réussie auprès du serveur, la requête est redirigée vers l'URL de service spécifiée, le ticket de service étant ajouté en tant que paramètre de requête.

  4. CasAuthenticationFilter est mappé à une URL qui correspond au modèle et à son tour, déclenche la validation du ticket en interne.

  5. Si le ticket est valide, un utilisateur sera redirigé vers l'URL demandée à l'origine

Maintenant, nous devons configurer Spring Security pour protéger certaines routes et utiliser le beanCasAuthenticationEntryPoint.

CréonsSecurityConfig.java qui étendWebSecurityConfigurerAdapter et écrasent lesconfig():

@EnableWebSecurity
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
      http
        .authorizeRequests()
        .regexMatchers("/secured.*", "/login")
        .authenticated()
        .and()
        .authorizeRequests()
        .regexMatchers("/")
        .permitAll()
        .and()
        .httpBasic()
        .authenticationEntryPoint(authenticationEntryPoint);
    }
    // ...
}

De plus, dans la classeSecurityConfig, nous remplaçons les méthodes suivantes et créons le beanCasAuthenticationFilter en même temps:

@EnableWebSecurity
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private AuthenticationProvider authenticationProvider;
    private AuthenticationEntryPoint authenticationEntryPoint;
    private SingleSignOutFilter singleSignOutFilter;
    private LogoutFilter logoutFilter;

    @Autowired
    public SecurityConfig(CasAuthenticationProvider casAuthenticationProvider, AuthenticationEntryPoint eP,
                          LogoutFilter lF
                          , SingleSignOutFilter ssF
    ) {
        this.authenticationProvider = casAuthenticationProvider;
        this.authenticationEntryPoint = eP;

        this.logoutFilter = lF;
        this.singleSignOutFilter = ssF;

    }

    // ...

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
      auth.authenticationProvider(authenticationProvider);
    }

    @Override
    protected AuthenticationManager authenticationManager() throws Exception {
      return new ProviderManager(Arrays.asList(authenticationProvider));
    }

    @Bean
    public CasAuthenticationFilter casAuthenticationFilter(ServiceProperties sP) throws Exception {
      CasAuthenticationFilter filter = new CasAuthenticationFilter();
      filter.setServiceProperties(sP);
      filter.setAuthenticationManager(authenticationManager());
      return filter;
    }
}

Créons des contrôleurs qui gèrent les requêtes dirigées vers/secured,/login et la page d’accueil.

La page d'accueil est mappée à unIndexController qui a une méthodeindex(). Cette méthode renvoie simplement la vue d'index:

@GetMapping("/")
public String index() {
    return "index";
}

Le chemin/login est mappé à la méthodelogin() de la classeAuthController. Il redirige simplement vers la page de connexion réussie par défaut.

Notez que lors de la configuration desHttpSecurity ci-dessus, nous avons configuré le chemin de/login afin qu'il nécessite une authentification. De cette façon, nous redirigeons l'utilisateur vers le serveur CAS pour l'authentification.

Ce mécanisme est un peu différent de la configuration normale où le chemin/login n'est pas une route protégée et renvoie un formulaire de connexion:

@GetMapping("/login")
public String login() {
    return "redirect:/secured";
}

Le chemin/secured est mappé à la méthodeindex() de la classeSecuredPageController. Il récupère le nom d'utilisateur de l'utilisateur authentifié et l'affiche dans le message de bienvenue:

@GetMapping
public String index(ModelMap modelMap) {
  Authentication auth = SecurityContextHolder.getContext()
    .getAuthentication();
  if(auth != null
    && auth.getPrincipal() != null
    && auth.getPrincipal() instanceof UserDetails) {
      modelMap.put("username", ((UserDetails) auth.getPrincipal()).getUsername());
  }
  return "secure/index";
}

Notez que toutes les vues sont disponibles dans le dossierresources descas-secured-app. À ce stade, lescas-secured-appdevraient pouvoir utiliser le serveur pour l'authentification.

Enfin, nous exécutonsbuild run depuis le terminal et démarrons simultanément l'application Spring Boot. Notez que SSL est la clé de tout ce processus. Par conséquent, l'étape de génération SSL ci-dessus ne doit pas être ignorée!

4.2. Configuration de la déconnexion unique

Continuons avecthe authentication process by logging out a user du système. Un utilisateur peut être déconnecté à deux endroits: l'application client et le serveur.

Déconnecter un utilisateur de l'application / du service client est la première chose à faire. Cela n'affecte pas l'état d'authentification de l'utilisateur dans d'autres applications connectées au même serveur. Bien sûr, la déconnexion d'un utilisateur du serveur le déconnecte également de tous les autres services / clients enregistrés.

Commençons par définir quelques configurations de bean dans la classeCasSecuredAppApplicaiton:

@Bean
public SecurityContextLogoutHandler securityContextLogoutHandler() {
    return new SecurityContextLogoutHandler();
}

@Bean
public LogoutFilter logoutFilter() {
    LogoutFilter logoutFilter = new LogoutFilter(
      "https://localhost:6443/cas/logout",
      securityContextLogoutHandler());
    logoutFilter.setFilterProcessesUrl("/logout/cas");
    return logoutFilter;
}

@Bean
public SingleSignOutFilter singleSignOutFilter() {
    SingleSignOutFilter singleSignOutFilter = new SingleSignOutFilter();
    singleSignOutFilter.setCasServerUrlPrefix("https://localhost:6443/cas");
    singleSignOutFilter.setIgnoreInitConfiguration(true);
    return singleSignOutFilter;
}

@EventListener
public SingleSignOutHttpSessionListener singleSignOutHttpSessionListener(
  HttpSessionEvent event) {
    return new SingleSignOutHttpSessionListener();
}

Nous configurons leslogoutFilter pour intercepter le modèle d'URL/logout/cas et pour rediriger l'application vers le serveur pour une déconnexion à l'échelle du système. Le serveur envoie une seule demande de déconnexion à tous les services concernés. Une telle demande est gérée par leSingleSignOutFilter, qui invalide la session HTTP.

Modifions la configuration deHttpSecurity dans la classeconfig() deSecurityConfig. LesCasAuthenticationFilter etLogoutFilter qui étaient configurés précédemment sont désormais également ajoutés à la chaîne:

http
  .authorizeRequests()
  .regexMatchers("/secured.*", "/login")
  .authenticated()
  .and()
  .authorizeRequests()
  .regexMatchers("/")
  .permitAll()
  .and()
  .httpBasic()
  .authenticationEntryPoint(authenticationEntryPoint)
  .and()
  .logout().logoutSuccessUrl("/logout")
  .and()
  .addFilterBefore(singleSignOutFilter, CasAuthenticationFilter.class)
  .addFilterBefore(logoutFilter, LogoutFilter.class);

Pour que la déconnexion fonctionne correctement, nous devons implémenter une méthodelogout() qui déconnecte d'abord un utilisateur du système localement et affiche une page avec un lien pour éventuellement déconnecter l'utilisateur de tous les autres services connectés au serveur.

Le lien est le même que celui défini comme URL de processus de filtrage desLogoutFilter que nous avons configurés ci-dessus:

@GetMapping("/logout")
public String logout(
  HttpServletRequest request,
  HttpServletResponse response,
  SecurityContextLogoutHandler logoutHandler) {
    Authentication auth = SecurityContextHolder
      .getContext().getAuthentication();
    logoutHandler.logout(request, response, auth );
    new CookieClearingLogoutHandler(
      AbstractRememberMeServices.SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY)
      .logout(request, response, auth);
    return "auth/logout";
}

La vue de déconnexion:



    Cas Secured App - Logout


You have logged out of Cas Secured Spring Boot App Successfully


Log out of all other Services

5. Connexion du serveur CAS à une base de données

Nous utilisons des informations d'identification utilisateur statiques pour l'authentification. Toutefois, dans les environnements de production, les informations d'identification de l'utilisateur sont stockées dans une base de données la plupart du temps. Donc, ensuite,we show how to connect our server to a MySQL database (database name: test) s'exécutant localement.

Nous faisons cela en ajoutant les données suivantes au fichierapplication.properties dans le répertoirecas-server/src/main/resources:

cas.authn.accept.users=
cas.authn.accept.name=

cas.authn.jdbc.query[0].sql=SELECT * FROM users WHERE email = ?
cas.authn.jdbc.query[0].url=jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC
cas.authn.jdbc.query[0].dialect=org.hibernate.dialect.MySQLDialect
cas.authn.jdbc.query[0].user=root
cas.authn.jdbc.query[0].password=root
cas.authn.jdbc.query[0].ddlAuto=none
cas.authn.jdbc.query[0].driverClass=com.mysql.cj.jdbc.Driver
cas.authn.jdbc.query[0].fieldPassword=password
cas.authn.jdbc.query[0].passwordEncoder.type=NONE

N'oubliez pas que le contenu complet deapplication.properties se trouve dans le code source. Leaving the value of cas.authn.accept.users blank deactivates the use of static user repositories by the server.

En outre, nous définissons l'instruction SQL qui extrait les utilisateurs de la base de données. La possibilité de configurer le code SQL lui-même rend le stockage des utilisateurs dans la base de données très flexible.

Selon le SQL ci-dessus, l'enregistrement d'un utilisateur est stocké dans la tableusers. La colonneemail représente le principal (nom d'utilisateur) des utilisateurs. Plus bas dans la configuration, nous définissons le nom du champ de mot de passe,cas.authn.jdbc.query[0].fieldPassword. Nous le définissons sur la valeurpassword pour augmenter encore la flexibilité.

Les autres attributs que nous avons configurés sont l'utilisateur de la base de données (root) et le mot de passe (vide), le dialecte et la connexion JDBCString. La liste des bases de données prises en charge, des pilotes disponibles et des dialectes peut être trouvéehere .

Another essential attribute is the encryption type used for storing the password. Dans ce cas, il est réglé sur AUCUN.

Cependant, le serveur prend en charge davantage de mécanismes de chiffrement, tels que Bcrypt. Ces mécanismes de chiffrement qui peuvent être trouvéshere, ainsi que d'autres propriétés configurables.

L'exécution du serveur (build run) permet désormais l'authentification des utilisateurs avec des informations d'identification présentes dans la base de données configurée. Note again that the principal in the database that the server uses must be the same as that of the client applications.

Dans ce cas, l'application Spring Boot doit avoir la même valeur ([email protected]) pour le principal (username) que celle de la base de données connectée au serveur.

Modifions ensuite lesUserDetails connectés au beanCasAuthenticationProvider configuré dans la classeCasSecuredAppApplication de l'application Spring Boot:

@Bean
public CasAuthenticationProvider casAuthenticationProvider() {
    CasAuthenticationProvider provider = new CasAuthenticationProvider();
    provider.setServiceProperties(serviceProperties());
    provider.setTicketValidator(ticketValidator());
    provider.setUserDetailsService((s) -> new User(
      "[email protected]", "testU",
      true, true, true, true,
    AuthorityUtils.createAuthorityList("ROLE_ADMIN")));
    provider.setKey("CAS_PROVIDER_LOCALHOST_9000");
    return provider;
}

Une autre chose à noter est que même si leUserDetails reçoit un mot de passe, il n’est pas utilisé. Toutefois, si le nom d'utilisateur diffère de celui du serveur, l'authentification échouera.

Pour que l'application puisse s'authentifier avec les informations d'identification stockées dans la base de données, démarrez un serveur MySQL s'exécutant sur 127.0.0.1 et le port 3306 avec le nom d'utilisateur root et le mot de passe root.

Utilisez ensuite le fichier SQL,cas-server\src\main esources\create_test_db_and_users_tbl.sql, qui fait partie dessource code, pour créer la tableusers dans la base de donnéestest.

Par défaut, il contient l'e-mail[email protected] et le mot de passeMellon. N'oubliez pas que nous pouvons toujours modifier les paramètres de connexion à la base de données dansapplication.properties.

Redémarrez le serveur CAS avecbuild run,, accédez àhttps://localhost:6443/cas et utilisez ces informations d'identification pour l'authentification. Les mêmes informations d'identification fonctionneront également pour l'application Spring Boot sécurisée par cas.

6. Conclusion

Nous avons étudié en détail comment utiliser CAS Server SSO avec Spring Security et de nombreux fichiers de configuration impliqués.

Il existe de nombreux autres aspects d'un serveur pouvant être configurés, allant des thèmes et types de protocole aux stratégies d'authentification. Ceux-ci peuvent tous être trouvéshere dans la documentation.

Le code source du serveur dans cet article et ses fichiers de configuration peuvent être trouvéshere, et celui de l'application Spring Boot peut être trouvéhere.