Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Connexion à différents fournisseurs d'identités #3111

Closed
jacquesfize opened this issue Jul 3, 2024 · 5 comments · Fixed by #2976
Closed

Connexion à différents fournisseurs d'identités #3111

jacquesfize opened this issue Jul 3, 2024 · 5 comments · Fixed by #2976

Comments

@jacquesfize
Copy link
Contributor

jacquesfize commented Jul 3, 2024

Bonjour à tous,

Avec @TheoLechemia, nous travaillons sur l'ajout d'une fonctionnalité dans GeoNature permettant de se connecter à l'aide de différents fournisseurs d'identités. L'objectif est de permettre aux structures qui le souhaitent d'utiliser leurs propres fournisseurs d'identités pour se connecter à leur GeoNature. Par défaut, plusieurs protocoles de connexions sont intégrés dans le module UsersHub-authentification-module (v2.3):

  • OpenID et OpenIDConnect (compatible avec Google)
  • Le CAS de l'INPN (déjà présent dans les précédentes versions)
  • GeoNature

Fonctionnement

Avant/Après cette mise à jour

Avant.

Dans la version actuelle de GeoNature, il est possible de se connecter de deux manières :

  • via le système d'authentification propre à GeoNature
  • via le CAS de l'INPN.

Dans le cas par défaut, lors de la connexion, le frontend effectue une requête asynchrone vers la route /auth/login de l'API. L'API retourne les informations de l'utilisateur qui seront stockées dans le localStorage (gn_token, gn_current_user et gn_expires_at). Le token permet à l'API d'identifier l'utilisateur lors des ces requêtes sur cette dernière.

Dans le cas de l'INPN, l'utilisateur est automatiquement redirigé vers le portail de l'INPN lorsqu'il tente d'accéder à GeoNature. Une fois sur le portail, il doit saisir ses informations de connexion. Après une connexion réussie, une redirection est effectuée vers des routes spécifiques au CAS qui connecte l'utilisateur et renvoie son token d'identification au frontend.

Après.

Le fonctionnement initial du login est maintenu. La nouveauté réside dans la possibilité de se connecter à d'autres fournisseurs d'identités (FI) en s'appuyant sur des protocoles de connexions différents de GeoNature (OAuth, OAuth2, CAS, etc...). Côté Frontend, en cliquant pour se connecter à un FI, l'utilisateur sera rediriger vers l'API /auth/login/<id_provider> (id_provider correspond à l'idenfiant unique du FI). Cette API redirigera ensuite vers le portail de connexion de l'instance du fournisseur d'identités (Voir Figure ci-dessus).

google login page

Une fois la connexion réussie, le portail redirige vers la route de l'API auth/authorize/<id_provider> qui se charge de réconcilier les informations utilisateur fournies par le FI avec le schéma de la base de données de GeoNature.

Lors de la déconnexion, il est possible de se déconnecter du fournisseur d'identité ainsi que de GeoNature en utilisant la méthode revoke() définie par le provider.

N.B. Il est possible de se connecter à plusieurs fournisseurs d'identités autres que celui de base !

Schéma.

workflow

Comment utiliser un autre fournisseur d'identités (FI) ?

Comme expliqué précédemment, UsersHub-authenfication-module vient avec un ensemble de protocoles de connexion prédéfini.

Si le fournisseur d'identité utilise un des protocoles de connexions existant, il suffit de remplir la configuration comme dans l'exemple suivant :

[AUTHENTICATION]
    DEFAULT_RECONCILIATION_GROUP_ID = 2
[[AUTHENTICATION.PROVIDERS]]
    module="pypnusershub.auth.providers.default.DefaultConfiguration"
    id_provider="local_provider"
[[AUTHENTICATION.PROVIDERS]]
    module="pypnusershub.auth.providers.openid_provider.OpenIDProvider"
    id_provider = "keycloak"
    label = "KeyCloak"
    ISSUER = "http://<realmKeycloak>"
    CLIENT_ID = "secret"
    CLIENT_SECRET = "secret"
    group_mapping = {"/user"=1,"/admin"=2}
[[AUTHENTICATION.PROVIDERS]]
    module="pypnusershub.auth.providers.openid_provider.OpenIDConnectProvider"
    id_provider = "google"
    logo = "<i class='fa fa-google' aria-hidden='true'></i>"
    label = "Google"
    ISSUER = "https://accounts.google.com/"
    CLIENT_ID = "secret"
    CLIENT_SECRET = "secret"
[[AUTHENTICATION.PROVIDERS]]
    module="pypnusershub.auth.providers.usershub_provider.ExternalUsersHubAuthProvider"
    id_provider = "usershub"
    label ="Geonature Ecrins"
    login_url = "https://geonature.ecrins-parcnational.fr/api/auth/login"
    logout_url = "https://geonature.ecrins-parcnational.fr/api/auth/logout"

[... other providers instances definitions]

Cette configuration permet de se connecter à :

  • Google (OpenID)
  • Un KeyCloak (protocole OpenIDConnect)
  • Un autre GeoNature, ici l'instance de GeoNature du Parc National des Ecrins

Comment ça marche ?

Pour ajouter un fournisseur d'identité utilisant un protocole, if faut ajouter une section AUTHENTICATION.PROVIDERS dans la configuration.

[[AUTHENTICATION.PROVIDERS]]
    param1=value1
    param2=value2

Dans chaque section décrivant un fournisseur d'identité, il faut déclaré :

  • le chemin vers la classe Python module déclarant le protocole de connexion
  • son identifiant unique id_provider (dans cette instance de GeoNature)
  • (optionel) le logo et le label qui seront affichés sur la page de login.
  • login_url, logout_url si le provider en a besoin
  • (optionel)groupe_mapping pour la réconciliation entre les groupes du fournisseurs d'identité et celui dans GeoNature.
  • Autre variables de configuration propre au protocole de connexion (clé d'API, etc..)

Une fois la configuration mise à jour, vous devriez voir l'interface suivante.

image

Déclaration d'un protocole de connexion

La "brique" permettant de faire la connexion et la réconciliation (i.e synchronisation des données du fournisseurs et celle présente en local) sur différents fournisseurs d'identités.

Chaque protocole de connexion ou provider est défini par une classe comme celle-ci :

from typing import Any, Optional, Tuple, Union

from authlib.integrations.flask_client import OAuth
from flask import (
    Response,
    current_app,
    url_for,
)
from marshmallow import Schema, fields

from pypnusershub.auth import Authentication, ProviderConfigurationSchema, oauth
from pypnusershub.db import models, db
from pypnusershub.db.models import User
from pypnusershub.routes import insert_or_update_role
import sqlalchemy as sa

CONF_URL = "https://accounts.google.com/.well-known/openid-configuration"
oauth.register(
    name="google",
    server_metadata_url=CONF_URL,
    client_kwargs={"scope": "openid email profile"},
)

class GoogleAuthProvider(Authentication):
    name = "GOOGLE_PROVIDER_CONFIG"
    id_provider = "google"
    label = "Google"
    is_external = False
    group_claim_name = "groups"
    logo = '<i class="fa fa-google"></i>'

    def authenticate(self, *args, **kwargs) -> Union[Response, models.User]:
        redirect_uri = url_for(
            "auth.authorize", provider=self.id_provider, _external=True
        )
        return oauth.google.authorize_redirect(redirect_uri)

    def authorize(self):
        token = oauth.google.authorize_access_token()
        user_info = token["userinfo"]
        new_user = {
            "identifiant": f"{user_info['given_name'].lower()}{user_info['family_name'].lower()}",
            "email": user_info["email"],
            "prenom_role": user_info["given_name"],
            "nom_role": user_info["family_name"],
            "active": True,
        }
        return insert_or_update_role(new_user, provider_instance=self)

    def configure(self, configuration: Union[dict, Any]):
        super().configure(configuration)
        class GoogleProviderConfiguration(ProviderConfigurationSchema):
            GOOGLE_CLIENT_ID = fields.String(load_default="")
            GOOGLE_CLIENT_SECRET = fields.String(load_default="")
        configuration = GoogleProviderConfiguration().load(configuration)

        current_app.config["GOOGLE_CLIENT_ID"] = configuration["GOOGLE_CLIENT_ID"]
        current_app.config["GOOGLE_CLIENT_SECRET"] = configuration[
            "GOOGLE_CLIENT_SECRET"
        ]

Un protocole de connexion est défini par 4 méthodes et plusieurs attributs.

Les attributs sont les suivants

  • L'attribut id_provider indique l'identifiant de l'instance du provider.
  • Les attributs logo et label sont destinés à l'interface utilisateur.
  • L'attribut is_external spécifie si le provider permet de se connecter à une autre application Flask utilisant UsersHub-authentification-module ou à un fournisseur d'identité qui requiert une redirection vers une page de login.
  • L'attribut login_url et logout_url, si le protocole de connexion nécessite une redirection
  • L'attribut group_mapping contient le mapping entre les groupes du fournisseurs d'identités et celui de votre instance de GeoNature.

Les méthodes sont les suivantes :

  • authenticate: Lancée sur la route /auth/login, elle récupère les informations du formulaire de login et retourne un objet User. Si le protocole de connexion doit rediriger l'utilisateur vers un portail, alors authenticate retourne une flask.Response qui redirige vers ce dernier.
  • authorize: Cette méthode est lancée par la route /auth/authorize qui récupère les informations renvoyés par le fournisseur d'identités après la connexions sur le portail.
  • configure(self, configuration: Union[dict, Any]): Permet de récupérer et d'utiliser les variables présentes dans le fichier de configuration. Il est possible aussi de valider les résultats à l'aide d'un schéma marshmallow
  • revoke(): Permet de spécifier un fonctionnement spécifique lors de la déconnexion d'un utilisateur.

Ajouter son propre provider

Si les protocoles de connexions fournis dans le module d'authentification ne répondent pas à vos besoins, vous pouvez créer les vôtres !

Pour ce faire, il suffit de créer une classe qui hérite de Authentication et qui implémente les méthodes suivantes :

  • authenticate()
  • configure() (pour indiquer comment les variables de configuration sont utilisées pour configurer le provider)

et les attributs suivants :

  • is_external

D'autres méthodes et attributs sont disponibles, voir la classe Authentication.

from marshmallow import Schema, fields
from typing import Any, Optional, Tuple, Union

from pypnusershub.auth import Authentication, ProviderConfigurationSchema
from pypnusershub.db import models, db
from flask import Response

class NEW_PROVIDER(Authentication):
    is_external = True # si redirection vers un portail de connexion externe

    def authenticate(self, *args, **kwargs) -> Union[Response, models.User]:
        pass # doit retourner un utilisateur (User) ou rediriger (flask.Redirect) vers le portail de connexion du fournisseur d'identités

    def authorize(self):
        # appeler par /auth/authorize si redirection d'un portail de connexion externe
        pass # doit retourner un utilisateur

    def revoke(self):
        pass # si une action spécifique doit être faite lors de la déconnexion

    def configure(self, configuration: Union[dict, Any]):
        class SchemaConf(ProviderConfigurationSchema):
            VAR = fields.String(required=True)
        configuration = SchemaConf().load(configuration) # Si besoin d'un processus de validation
        ...# Configuration du fournisseur d'identités

Comme les autres protocoles de connexions, il suffit d'indiquer le chemin vers votre classe Python et sa configuration pour le fournisseur d'identité utilisant ce dernier !

@maximetoma
Copy link
Contributor

Peut-on envisager l'authentification avec Microsoft 365 ? Je ne sais pas du tout si c'est possible en revanche....

@jacquesfize
Copy link
Contributor Author

Ça semble possible: https://learn.microsoft.com/fr-fr/entra/identity-platform/v2-app-types#web-apps

Leur plateforme utilise le protocole OAuth2. Avec @TheoLechemia, on a implémenté un provider pour ce type de protocole.

@camillemonchicourt
Copy link
Member

Présentation du sujet lors du Cotech du 9 juillet 2024 : https://geonature.fr/documents/comite-technique/2024-07-09-COTECH-AUTHENTIFICATION.pdf

@camillemonchicourt
Copy link
Member

Voir les évolutions liées dans le sous-module d'authentification : PnX-SI/UsersHub-authentification-module#93

@camillemonchicourt
Copy link
Member

Intégré dans la 2.15

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants