PnX-SI / GeoNature

Application de saisie et de synthèse des observations faune et flore
GNU General Public License v3.0
97 stars 99 forks source link

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

Open jacquesfize opened 2 days ago

jacquesfize commented 2 days ago

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 ou provider. 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 providers sont intégrés dans le module UsersHub-authentification-module :

Fonctionnement

Notions

Provider ? Instance d'un provider ? Un provider désigne un protocole de connexion, tandis qu'une instance d'un provider se réfère à une utilisation spécifique de ce protocole. Par exemple, le système d'authentification de GeoNature est utilisé à la fois par GeoNature du SINP PACA et par SINP AuRA. La principale différence entre ces deux instances réside dans leurs adresses d'accès et la base de données utilisateurs associées.

Avant/Après cette mise à jour

Avant.

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

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 est qu'il 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 protocole de connexion prédéfini dans des Provider.

Si le fournisseur d'identité utilise un des protocoles de connexions existant, il suffit de remplir la configuration comme ci-dessous. Dans ce fichier de configuration, nous avons ajouté la possibilité de se connecter à:

[AUTHENTICATION]
    PROVIDERS=["pypnusershub.auth.providers.openid_provider.OpenIDProvider",
    "pypnusershub.auth.providers.openid_provider.OpenIDConnectProvider","pypnusershub.auth.providers.usershub_provider.ExternalUsersHubAuthProvider"]
    DISPLAY_DEFAULT_LOGIN_FORM = true
    DEFAULT_RECONCILIATION_GROUP_ID = 2
    # ONLY_PROVIDER = "google" #
[[AUTHENTICATION.OPENID_CONNECT_PROVIDER_CONFIG]]
    id_provider = "keycloak"
    label = "KeyCloak"
    ISSUER = "http://<realmKeycloak>"
    CLIENT_ID = "secret"
    CLIENT_SECRET = "secret"
    group_mapping = {"/user"=1,"/admin"=2}
[[AUTHENTICATION.OPENID_PROVIDER_CONFIG]]
    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.EXTERNAL_USERSHUB_PROVIDER_CONFIG]]
    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]

Comment ça marche ?

Pour utiliser un fournisseur d'identité, il faut :

[[AUTHENTICATION.<label_accessible_dans_config**>]]
    param1=value1
    param2=value2

** Voir dans l'attribut name dans la classe décrivant le protocole de connexion

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

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_uh = 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(User(**new_user), provider_instance=self)

    @staticmethod
    def configuration_schema() -> Optional[Tuple[str, ProviderConfigurationSchema]]:
        class GoogleProviderConfiguration(ProviderConfigurationSchema):
            GOOGLE_CLIENT_ID = fields.String(load_default="")
            GOOGLE_CLIENT_SECRET = fields.String(load_default="")

        return GoogleProviderConfiguration

    def configure(self, configuration: Union[dict, Any]):
        super().configure(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 5 méthodes et plusieurs attributs.

Les attributs sont les suivants

Les méthodes sont les suivantes :

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 :

et les attributs suivants :

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):
    name = "NAME_IN_CONFIG"
    is_uh = False

    def authenticate(self, *args, **kwargs) -> Union[Response, models.User]:
        pass # return a User or a Flask redirection to the login portal of the provider

    def authorize(self):
        pass # must return a User

    def revoke(self):
        pass # if specific action have to be made when logout

    @staticmethod
    def configuration_schema() -> Optional[Tuple[str, ProviderConfigurationSchema]]:
        ## return a schema detailing your configuration in the geonature_config.toml
        class SchemaConf(ProviderConfigurationSchema):
            VAR = fields.String(required=True)

        return SchemaConf
    def configure(self, configuration: Union[dict, Any]):
        pass # Si instruction spécifique dans l'utilisation des variables de configuration

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 commented 2 days ago

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

jacquesfize commented 2 days ago

Ç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.