OAuth2
======

L'authentification `OAuth2 <https://fr.wikipedia.org/wiki/OAuth>`_ est supportée par la
Note Kfet. Elle offre l'avantage non seulement d'identifier les utilisateur⋅rices, mais
aussi de transmettre des informations à un service tiers tels que des informations
personnelles, le solde de la note ou encore les adhésions de l'utilisateur⋅rice, en
l'avertissant sur quelles données sont effectivement collectées. Ainsi, il est possible
de développer des appplications tierces qui peuvent se baser sur les données de la Note
Kfet ou encore faire des transactions.


Configuration du serveur
------------------------

On utilise ``django-oauth-toolkit``, qui peut être installé grâce à PIP ou bien via APT,
via le paquet ``python3-django-oauth-toolkit``.

On commence par ajouter ``oauth2_provider`` aux applications Django installées. On
n'oublie pas ni d'appliquer les migrations (``./manage.py migrate``) ni de collecter
les fichiers statiques (``./manage.py collectstatic``).

On souhaite que l'API gérée par ``django-rest-framework`` puisse être accessible via
l'authentification OAuth2. On adapte alors la configuration pour permettre cela :

.. code:: python

   REST_FRAMEWORK = {
       'DEFAULT_AUTHENTICATION_CLASSES': [
           'rest_framework.authentication.SessionAuthentication',
           'rest_framework.authentication.TokenAuthentication',
           'oauth2_provider.contrib.rest_framework.OAuth2Authentication',
           ...
       ],
       ...
   }

On a ensuite besoin de définir nos propres scopes afin d'avoir des permissions fines :

.. code:: python

   OAUTH2_PROVIDER = {
       'SCOPES_BACKEND_CLASS': 'permission.scopes.PermissionScopes',
       'OAUTH2_VALIDATOR_CLASS': "permission.scopes.PermissionOAuth2Validator",
       'REFRESH_TOKEN_EXPIRE_SECONDS': timedelta(days=14),
       'PKCE_REQUIRED': False,
       'OIDC_ENABLED': True,
       'OIDC_RSA_PRIVATE_KEY':
           os.getenv('OIDC_RSA_PRIVATE_KEY', '/var/secrets/oidc.key'),
   }

Cela a pour effet d'avoir des scopes sous la forme ``PERMISSION_CLUB``,
et de demander des scopes facultatives (voir plus bas).
Un jeton de rafraîchissement expire de plus au bout de 14 jours, si non-renouvelé.

On ajoute enfin les routes dans ``urls.py`` :

.. code:: python

   urlpatterns.append(
        path('o/', include('oauth2_provider.urls', namespace='oauth2_provider'))
    )

Enfin pour utiliser OIDC, il faut générer une clé privé que l'on va, par défaut,
mettre dans `/var/secrets/oidc.key` :

.. code:: bash

   cd /var/secrets/
   openssl genrsa -out oidc.key 4096

L'OAuth2 est désormais prêt à être utilisé.


Configuration client
--------------------

Contrairement au `CAS <cas>`_, n'importe qui peut créer une application OAuth2.

Pour créer une application, il faut se rendre à la page
`/o/applications/ <https://note.crans.org/o/applications/>`_. Dans ``client type``,
rentrez ``public`` (ou ``confidential`` selon vos choix), et vous rentrerez
généralement ``authorization-code`` dans ``Authorization Grant Type``.
Le champ ``Redirect Uris`` contient une liste d'adresses URL autorisées pour des
redirections post-connexion.

Il vous suffit de donner à votre application :

* L'identifiant client (client-ID)
* La clé secrète
* Les scopes, qui peuvent être récupérées sur cette page : `<https://note.crans.org/permission/scopes/>`_
* L'URL d'autorisation : `<https://note.crans.org/o/authorize/>`_
* L'URL d'obtention de jeton : `<https://note.crans.org/o/token/>`_
* Si besoin, l'URL de récupération des informations de l'utilisateur⋅rice : `<https://note.crans.org/api/me/>`_

N'hésitez pas à consulter la page `<https://note.crans.org/api/me/>`_ pour s'imprégner
du format renvoyé.

.. warning::

   Un petit mot sur les scopes : tel qu'implémenté, un scope est une permission unitaire
   (telle que décrite dans le modèle ``Permission``) associée à un club. Ainsi, un jeton
   a accès à une scope si et seulement si læ propriétaire du jeton dispose d'une adhésion
   courante dans le club lié à la scope qui lui octroie cette permission.

   Par exemple, un jeton pourra avoir accès à la permission de créer des transactions en lien
   avec un club si et seulement si læ propriétaire du jeton est trésorièr⋅e du club.

   La vérification des droits de læ propriétaire est faite systématiquement, afin de ne pas
   faire confiance au jeton en cas de droits révoqués à saon propriétaire.

   Vous pouvez donc contrôler le plus finement possible les permissions octroyées à vos
   jetons.

   Deux scopes sont un peu particulier, le scope "0_0" qui ne donne aucune permission
   et le scope "openid" pour l'OIDC.

.. danger::

   Demander des scopes n'implique pas de les avoir.

   Lorsque des scopes sont demandées par un client, la Note
   va considérer l'ensemble des permissions accessibles parmi
   ce qui est demandé. Dans vos programmes, vous devrez donc
   vérifier les permissions acquises (communiquées lors de la
   récupération du jeton d'accès à partir du grant code),
   et prévoir un comportement dans le cas où des permissions
   sont manquantes.

   Cela offre un intérêt supérieur par rapport au protocole
   OAuth2 classique, consistant à demander trop de permissions
   et agir en conséquence.

   Par exemple, vous pourriez demander la permission d'accéder
   aux membres d'un club ou de faire des transactions, et agir
   uniquement dans le cas où l'utilisateur⋅rice connecté⋅e
   possède la permission problématique.

   Dans le cas extrême ou aucun scope demandé n'est obtenus, vous
   obtiendriez le scope "0_0" qui ne permet l'accès à rien.
   Cela permet de générer un token pour toute les requêtes valides.
   
   
Avec Django-allauth
###################

Si vous utilisez Django-allauth pour votre propre application, vous pouvez utiliser
le module pré-configuré disponible ici :
`<https://gitlab.crans.org/bde/allauth-note-kfet>`_. Pour l'installer, vous
pouvez simplement faire :

.. warning::
   À cette heure (11/2025), ce paquet est déprécié et il est plutôt conseillé de créer
   sa propre application.

.. code:: bash

   $ pip3 install git+https://gitlab.crans.org/bde/allauth-note-kfet.git

L'installation du module se fera automatiquement.

Il vous suffit ensuite d'inclure l'application ``allauth_note_kfet`` à vos applications
installées (sur votre propre client), puis de bien ajouter l'application sociale :

.. code:: python

   SOCIALACCOUNT_PROVIDERS = {
       'notekfet': {
           # 'DOMAIN': 'note.crans.org',
           'SCOPE': ['1_1', '2_1'],
       },
       ...
   }

Le paramètre ``DOMAIN`` permet de changer d'instance de Note Kfet. Par défaut, il
se connectera à ``note.crans.org`` si vous ne renseignez rien.

Le paramètre ``SCOPE`` permet de définir les scopes à demander.
Dans l'exemple ci-dessous, les permissions d'accéder à l'utilisateur⋅rice
et au profil sont demandées.

En créant l'application sur la note, vous pouvez renseigner
``https://monsite.example.com/accounts/notekfet/login/callback/`` en URL de redirection,
à adapter selon votre configuration.

Vous devrez ensuite enregistrer l'application sociale dans la base de données.
Vous pouvez passer par Django-admin, mais cela peut nécessiter d'avoir déjà un compte,
alors autant le faire via un shell python :

.. code:: python

   from allauth.socialaccount.models import SocialApp
   SocialApp.objects.create(
           name="Note Kfet",
           provider="notekfet",
           client_id="VOTRECLIENTID",
           secret="VOTRESECRET",
           key="",
   )

Si vous avez bien configuré ``django-allauth``, vous êtes désormais prêts par à vous
connecter via la note :) Par défaut, nom, prénom, pseudo et adresse e-mail sont
récupérés. Les autres données sont stockées mais inutilisées.


Application personnalisée
#########################

.. note::

   Tout les flow (c'est-à-dire les différentes suites de requête possible pour obtenir
   un token d'accès) de l'OAuth2 sont reproduits dans les
   `tests <https://gitlab.crans.org/bde/nk20/-/tree/main/apps/permission/tests/test_oauth2_flow.py>`_
   de l'application permission de la Note. L'OIDC n'étant qu'une extension du protocole
   OAuth2 vous pouvez facilement reproduire les requêtes en vous inspirant de
   l'Authorization Code de OAuth2.

.. danger::
   
   Pour des raisons de rétrocompatibilité, PKCE (Proof Key for Code Exchange) n'est pas requis,
   son utilisation est néanmoins très vivement conseillé.

Ce modèle vous permet de créer vos propres applications à interfacer avec la Note Kfet.

Commencez par créer une application : `<https://note.crans.org/o/applications/register>`_.
Dans ``Client type``, choisissez ``Confidential`` si des informations confidentielles sont
amenées à transiter, sinon ``public``. Choisissez ``Authorization code`` dans
``Authorization grant type``.

Dans ``Redirect uris``, vous devez insérer l'ensemble des URL autorisées à être redirigées
à la suite d'une autorisation OAuth2. La première URL entrée sera l'URL par défaut dans le
cas où elle n'est pas explicitement indiquée lors de l'autorisation.

.. note::

   À des fins de tests, il est possible de laisser `<http://localhost/>`_ pour faire des
   appels à la main en récupérant le jeton d'autorisation.

Lorsqu'un client veut s'authentifier via la Note Kfet, il va devoir accéder à une page
d'authentification. La page d'autorisation est `<https://note.crans.org/o/authorize/>`_,
c'est sur cette page qu'il faut rediriger les utilisateur⋅rices. Il faut mettre en paramètre GET :

* ``client_id`` : l'identifiant client de l'application (public) ;
* ``response_type`` : mettre ``code`` ;
* ``scope`` : l'ensemble des scopes demandés, séparés par des espaces. Ces scopes peuvent
  être récupérés sur la page `<https://note.crans.org/permission/scopes/>`_.
* ``redirect_uri`` : l'URL sur laquelle rediriger qui récupérera le code d'accès. Doit être
  autorisée par l'application. À des fins de test, peut être `<http://localhost/>`_.
* ``state`` : optionnel, peut être utilisé pour permettre au client de détecter des requêtes
  provenant d'autres sites.
* ``code_challenge``: PKCE, le hash d'une chaine d'entre 43 et 128  caractères.
* ``code_challenge_method``: PKCE, ``S256`` si le hasher est sha256.

Sur cette page, les permissions demandées seront listées, et l'utilisateur⋅rice aura le
choix d'accepter ou non. Dans les deux cas, l'utilisateur⋅rice sera redirigée vers
``redirect_uri``, avec pour paramètre GET soit le message d'erreur, soit un paramètre
``code`` correspondant au code d'autorisation.

Une fois ce code d'autorisation récupéré, il faut désormais récupérer le jeton d'accès.
Il faut pour cela aller sur l'URL `<https://note.crans.org/o/token/>`_, effectuer une
requête POST avec pour arguments :

* ``client_id`` ;
* ``client_secret`` ;
* ``grant_type`` : mettre ``authorization_code`` ;
* ``code`` : le code généré.

À noter que le code fourni n'est disponible que pendant quelques secondes.

À des fins de tests, on peut envoyer la requête avec ``curl`` :

.. code:: bash

   curl -X POST https://note.crans.org/o/token/ -d "client_id=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX&client_secret=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX&grant_type=authorization_code&code=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"

Le serveur renverra si tout se passe bien une réponse JSON :

.. code:: json

   {
       "access_token": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
       "expires_in": 36000,
       "token_type": "Bearer",
       "scope": "1_1 1_2",
       "refresh_token": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
   }

On note donc 2 jetons différents : un d'accès et un de rafraîchissement. Le jeton d'accès
est celui qui sera donné à l'API pour s'authentifier, et qui expire au bout de quelques
heures.

Il suffit désormais d'ajouter l'en-tête ``Authorization: Bearer ACCESS_TOKEN`` pour se
connecter à la note grâce à ce jeton d'accès.

Pour tester :

.. code:: bash

   curl https://note.crans.org/api/me -H "Authorization: Bearer XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"

En cas d'expiration de ce jeton d'accès, il est possible de le renouveler grâce au jeton
de rafraichissement à usage unique. Il suffit pour cela de refaire une requête sur la page
`<https://note.crans.org/o/token/>`_ avec pour paramètres :

* ``client_id`` ;
* ``client_secret`` ;
* ``grant_type`` : mettre ``refresh_token`` ;
* ``refresh_token`` : le jeton de rafraîchissement.

Le serveur vous fournira alors une nouvelle paire de jetons, comme précédemment.
À noter qu'un jeton de rafraîchissement est à usage unique.

N'hésitez pas à vous renseigner sur `OAuth2 <https://www.rfc-editor.org/rfc/rfc6749.html>`_ ou sur le protocole `OIDC <https://openid.net/specs/openid-connect-core-1_0.html>`_ pour plus d'informations.
