From ceb9f50ef320a5f606e35394e5303cfa0fa3bc5f Mon Sep 17 00:00:00 2001
From: imperosol
- {% trans trimmed app=third_party_app, cgu_link=third_party_cgu, sith_cgu_link=sith_cgu %}
- The privacy policies of {{ app }}
+ {% trans trimmed app=third_party_app, privacy_link=third_party_cgu, sith_cgu_link=sith_cgu %}
+ The privacy policies of {{ app }}
and of the Students' Association
applies as soon as the form is submitted.
{% endtrans %}
diff --git a/api/tests/test_third_party_auth.py b/api/tests/test_third_party_auth.py
index 1d69ef0f..2a95267b 100644
--- a/api/tests/test_third_party_auth.py
+++ b/api/tests/test_third_party_auth.py
@@ -9,6 +9,7 @@ from pytest_django.asserts import assertRedirects
from api.models import ApiClient, get_hmac_key
from core.baker_recipes import subscriber_user
+from core.schemas import UserProfileSchema
from core.utils import hmac_hexdigest
@@ -34,14 +35,14 @@ class TestThirdPartyAuth(TestCase):
self.query = {
"client_id": self.api_client.id,
"third_party_app": "app",
- "cgu_link": "https://foobar.fr/",
+ "privacy_link": "https://foobar.fr/",
"username": "bibou",
"callback_url": "https://callback.fr/",
}
self.query["signature"] = hmac_hexdigest(self.api_client.hmac_key, self.query)
- self.callback_data = {"user_id": self.user.id}
+ self.callback_data = {"user": UserProfileSchema.from_orm(self.user).model_dump()}
self.callback_data["signature"] = hmac_hexdigest(
- self.api_client.hmac_key, self.callback_data
+ self.api_client.hmac_key, self.callback_data["user"]
)
def test_auth_ok(self):
diff --git a/api/views.py b/api/views.py
index 34e52732..82e481cb 100644
--- a/api/views.py
+++ b/api/views.py
@@ -10,26 +10,16 @@ from django.core.exceptions import PermissionDenied
from django.urls import reverse, reverse_lazy
from django.utils.translation import gettext as _
from django.views.generic import FormView, TemplateView
-from ninja import Schema
from ninja_extra.shortcuts import get_object_or_none
-from pydantic import HttpUrl
from api.forms import ThirdPartyAuthForm
from api.models import ApiClient
+from api.schemas import ThirdPartyAuthParamsSchema
from core.models import SithFile
from core.schemas import UserProfileSchema
from core.utils import hmac_hexdigest
-class ThirdPartyAuthParamsSchema(Schema):
- client_id: int
- third_party_app: str
- cgu_link: HttpUrl
- username: str
- callback_url: HttpUrl
- signature: str
-
-
class ThirdPartyAuthView(LoginRequiredMixin, FormView):
form_class = ThirdPartyAuthForm
template_name = "api/third_party/auth.jinja"
@@ -93,7 +83,7 @@ class ThirdPartyAuthView(LoginRequiredMixin, FormView):
def get_context_data(self, **kwargs):
return super().get_context_data(**kwargs) | {
"third_party_app": self.params.third_party_app,
- "third_party_cgu": self.params.cgu_link,
+ "third_party_cgu": self.params.privacy_link,
"sith_cgu": SithFile.objects.get(id=settings.SITH_CGU_FILE_ID),
}
diff --git a/core/utils.py b/core/utils.py
index 5d5a0738..6320f8e6 100644
--- a/core/utils.py
+++ b/core/utils.py
@@ -210,14 +210,14 @@ def get_client_ip(request: HttpRequest) -> str | None:
def hmac_hexdigest(
key: str | bytes,
data: Mapping[str, Any] | Sequence[tuple[str, Any]],
- digest: str | Callable[[Buffer], HASH] = "sha256",
+ digest: str | Callable[[Buffer], HASH] = "sha512",
) -> str:
"""Return the hexdigest of the signature of the given data.
Args:
key: the HMAC key used for the signature
data: the data to sign
- digest: a PEP247 hashing algorithm
+ digest: a PEP247 hashing algorithm (by default, sha512)
Examples:
```python
@@ -226,7 +226,7 @@ def hmac_hexdigest(
"bar": "somevalue",
}
hmac_key = secrets.token_hex(64)
- signature = hmac_hexdigest(hmac_key, data, "sha512")
+ signature = hmac_hexdigest(hmac_key, data, "sha256")
```
"""
if isinstance(key, str):
diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md
new file mode 100644
index 00000000..c0108439
--- /dev/null
+++ b/docs/reference/api/schemas.md
@@ -0,0 +1 @@
+::: api.schemas
\ No newline at end of file
diff --git a/docs/reference/api/views.md b/docs/reference/api/views.md
new file mode 100644
index 00000000..2a0daef1
--- /dev/null
+++ b/docs/reference/api/views.md
@@ -0,0 +1 @@
+::: api.views
\ No newline at end of file
diff --git a/docs/tutorial/api/account-link.md b/docs/tutorial/api/account-link.md
new file mode 100644
index 00000000..2a125824
--- /dev/null
+++ b/docs/tutorial/api/account-link.md
@@ -0,0 +1,353 @@
+Le site AE offre des mécanismes permettant aux applications tierces
+de récupérer les informations sur un utilisateur du site AE.
+De cette manière, il devient possible de synchroniser les informations
+qu possède l'application tierce sur l'utilisateur, directement depuis
+le site AE.
+
+## Fonctionnement général
+
+Pour authentifier vos utilisateurs, vous aurez besoin d'un serveur web
+et d'un client d'API (celui auquel est liée votre
+[clef d'API](./connect.md#obtenir-une-clef-dapi)).
+Deux informations vous sont nécessaires, en plus de votre clef d'API :
+
+- l'id du client : vous pouvez l'obtenir soit en le demandant à l'équipe info,
+ soit en appelant la route `GET /client/me` avec votre clef d'API
+ renseignée dans le header [X-APIKey](./connect.md#x-apikey)
+- la clef HMAC du client : vous devez la demander à l'équipe info.
+
+Grâce à ces informations, vous allez pouvoir fournir le contexte nécessaire
+au site AE pour qu'il authentifie vos utilisateurs.
+
+En effet, la démarche d'authentification s'effectue presque entièrement
+sur le site : le travail de l'application tierce consiste uniquement
+à fournir à l'utilisateur une url avec les bons paramètres, puis
+à recevoir la réponse du serveur si tout s'est bien passé.
+
+Comme un dessin vaut parfois mieux que mille mots,
+voici les diagrammes décrivant le processus.
+L'un montre l'entièreté de la démarche ;
+l'autre dans un souci de simplicité, ne montre que ce qui est visible
+directement par l'application tierce.
+
+=== "Intégralité du processus"
+
+ ```mermaid
+ sequenceDiagram
+ actor User
+ participant App
+ User->>+App: Authentifie-moi, stp
+ App-->>-User: url de connexion
avec signature
+ User->>+Sith: GET url
+ opt Utilisateur non-connecté
+ Sith->>+User: Formulaire de connexion
+ User-->>-Sith: Connexion
+ end
+ Sith->>Sith: vérification de la signature
+ Sith->>+User: Formulaire
des conditions
d'utilisation
+ User-->>-Sith: Validation
+ Sith->>+App: URL de retour
avec données utilisateur
+ App->>App: Traitement des
données utilisateur
+ App-->>-Sith: 204 OK, No content
+ Sith-->>-User: Message de succès
+ App--)User: Message de succès
+ ```
+
+=== "Point de vue de l'application tierce"
+
+ ```mermaid
+ sequenceDiagram
+ actor User
+ participant App
+ User->>+App: Authentifie-moi, stp
+ App-->>-User: url de connexion
avec signature
+ opt
+ Sith->>+App: URL de retour
avec données utilisateur
+ App->>App: Traitement des
données utilisateur
+ App-->>-Sith: 204 OK, No content
+ App--)User: Message de succès
+ end
+ ```
+
+## Données attendues
+
+### URL de connexion
+
+L'URL de connexion que vous allez fournir à l'utilisateur doit
+être `https://ae.utbm.fr/api-link/auth/`
+et doit contenir les données décrites dans
+[`ThirdPartyAuthParamsSchema`][api.schemas.ThirdPartyAuthParamsSchema] :
+
+- `client_id` (integer) : l'id de votre client, que vous pouvez obtenir
+ de la manière décrite plus haut
+- `third_party_app`(string) : le nom de la plateforme pour laquelle
+ l'authentification va être réalisée (si votre application est un bot
+ discord, mettez la valeur "discord")
+- `privacy_link`(URL) : l'URL vers la page de politique de confidentialité
+ qui s'appliquera dans le cadre de l'application
+ (s'il s'agit d'un bot discord, donnez le lien vers celles de Discord)
+- `username`(string) : le pseudonyme que l'utilisateur possède sur
+ votre application
+- `callback_url`(URL) : l'URL que le site AE appellera si l'authentification
+ réussit
+- `signature`(string) : la signature des données de la requête.
+
+Ces données doivent être url-encodées et passées dans les paramètres GET.
+
+!!!tip "URL de retour"
+
+ Notre système n'impose aucune contrainte quant à la manière
+ de construire votre URL (hormis le fait que ce doit être une URL HTTPS valide),
+ mais il est tout de même conseillé d'utiliser l'identifiant de votre
+ utilisateur comme paramètre dans l'URL
+ (par exemple `GET /callback/{int:user_id}/`).
+
+???Example
+
+ Supposons que votre client d'API soit utilisé dans le cadre d'un bot Discord,
+ avec les données suivantes :
+
+ - l'id du client est 15
+ - sa clef HMAC est "beb99dd53"
+ (c'est pour l'exemple, une vraie clef sera beaucoup plus longue)
+ - le pseudonyme discord de votre utilisateur est Brian
+ - son id sur discord est 123456789
+ - votre route de callback est `GET /callback/{int:user_id}/`,
+ accessible au domaine `https://bot.ae.utbm.fr`
+
+ Alors les paramètres de votre URL seront :
+
+ | Paramètre | valeur |
+ |-----------------|-----------------------------------------------------------------------|
+ | client_id | 15 |
+ | third_party_app | discord |
+ | privacy_link | `https://discord.com/privacy` |
+ | username | Brian |
+ | callback_url | `https://bot.ae.utbm.fr/callback/123456789/` |
+ | signature | 1a383c51060be64f07772aa42e07
18ae096b8f21f2cdb4061c0834a416d12101 |
+
+ Et l'url fournie à l'utilisateur sera :
+
+ `https://ae.utbm.fr/api-link/auth/?client_id=15&third_party_app=discord
+ &privacy_link=https%3A%2F%2Fdiscord.com%2Fprivacy&username=Brian
+ &callback_url=https%3A%2F%2Fbot.ae.utbm.fr%2Fcallback%2F123456789%2F
+ &signature=1a383c51060be64f07772aa42e0718ae096b8f21f2cdb4061c0834a416d12101`
+
+### Données de retour
+
+Si l'authentification réussit, le site AE enverra une requête HTTP POST
+à l'URL de retour fournie dans l'URL de connexion.
+
+Le corps de la requête de callback et au format JSON
+et contient deux paires clef-valeur :
+
+- `user` : les données utilisateur, telles que décrites
+ par [UserProfileSchema][core.schemas.UserProfileSchema]
+- `signature` : la signature des données utilisateur
+
+???Example
+
+ En reprenant les mêmes paramètres que dans l'exemple précédent,
+ le site AE pourra renvoyer à l'application la requête suivante :
+
+ ```http
+ POST https://bot.ae.utbm.fr/callback/123456789/
+ content-type: application/json
+ body: {
+ "user": {
+ "id": 144131,
+ "nick_name": "inzekitchen",
+ "first_name": "Brian",
+ ...
+ },
+ "signature": "f16955bab6b805f6e1abbb98a86dfee53fed0bf812aa6513ca46cfd461b70020"
+ }
+ ```
+
+L'application doit répondre avec un des codes HTTP suivants :
+
+| Code | Raison |
+|------|--------------------------------------------------------------------------------|
+| 204 | Tout s'est bien passé |
+| 403 | Les données de retour ne sont
pas signées ou sont mal signées |
+| 404 | L'URL de retour ne permet pas
d'identifier un utilisateur de l'application |
+
+!!!note "Code d'erreur par défaut"
+
+ Si l'appel de la route fait face à plusieurs problèmes en même temps
+ (par exemple, l'URL ne permet pas de retrouver votre utilisateur,
+ et en plus les données sont mal signées),
+ le 403 prime et doit être retourné par défaut.
+
+## Signature des données
+
+Les données de l'URL de connexion doivent être signées,
+et la signature de l'URL de retour doit être vérifiée.
+
+Dans le deux cas, la signature est le digest HMAC-SHA512
+des données url-encodées, en utilisant la clef HMAC du client d'API.
+
+???Example "Signature de l'URL de connexion"
+
+ En reprenant le même exemple que les fois précédentes,
+ l'url-encodage des données est :
+
+ `client_id=15&third_party_app=discord
+ &privacy_link=https%3A%2F%2Fdiscord.com%2Fprivacy%2F&username=Brian
+ &callback_url=https%3A%2F%2Fbot.ae.utbm.fr%2Fcallback%2F123456789%2F`
+
+ Notez que la signature n'est pas (encore) dedans.
+ Cette dernière peut-être obtenue avec le code suivant :
+
+ === ":simple-python: Python"
+
+ Dépendances :
+
+ - `environs` (>=14.1)
+
+ ```python
+ import hmac
+ from urllib.parse import urlencode
+
+ from environs import Env
+
+ env = Env()
+ env.read_env()
+
+ key = env.str("HMAC_KEY").encode()
+ data = {
+ "client_id": 15,
+ "third_party_app": "discord",
+ "privacy_link": "https://discord.com/privacy/",
+ "username": "Brian",
+ "callback_url": "https://bot.ae.utbm.fr/callback/123456789/",
+ }
+ urlencoded = urlencode(data)
+ data["signature"] = hmac.digest(key, urlencoded.encode(), "sha512").hex()
+
+ # URL a fournir à l'utilisateur pour son authentification
+ user_url = f"https://ae.ubtm.fr/api-link/auth/?{urlencode(data)}"
+ ```
+
+ === ":simple-rust: Rust"
+
+ Dépendances :
+
+ - `hmac` (>=0.12.1)
+ - `url` (>=2.5.7, features `serde`)
+ - `serde` (>=1.0.228, features `derive`)
+ - `serde_urlencoded` (>="0.7.1)
+ - `sha2` (>=0.10.9)
+ - `dotenvy` (>= 0.15)
+
+ ```rust
+ use hmac::{Mac, SimpleHmac};
+ use serde::Serialize;
+ use sha2::Sha512;
+ use url::Url;
+
+ #[derive(Serialize, Debug)]
+ struct UrlData<'a> {
+ client_id: u32,
+ third_party_app: &'a str,
+ privacy_link: Url,
+ username: &'a str,
+ callback_url: Url,
+ }
+
+ impl<'a> UrlData<'a> {
+ pub fn signature(&self, key: &[u8]) -> CtOutput