mirror of
https://github.com/ae-utbm/sith.git
synced 2025-11-03 10:33:06 +00:00
doc: third-party auth
This commit is contained in:
@@ -23,7 +23,7 @@ class ThirdPartyAuthForm(forms.Form):
|
|||||||
)
|
)
|
||||||
client_id = forms.IntegerField(widget=HiddenInput())
|
client_id = forms.IntegerField(widget=HiddenInput())
|
||||||
third_party_app = forms.CharField(widget=HiddenInput())
|
third_party_app = forms.CharField(widget=HiddenInput())
|
||||||
cgu_link = forms.URLField(widget=HiddenInput())
|
privacy_link = forms.URLField(widget=HiddenInput())
|
||||||
username = forms.CharField(widget=HiddenInput())
|
username = forms.CharField(widget=HiddenInput())
|
||||||
callback_url = forms.URLField(widget=HiddenInput())
|
callback_url = forms.URLField(widget=HiddenInput())
|
||||||
signature = forms.CharField(widget=HiddenInput())
|
signature = forms.CharField(widget=HiddenInput())
|
||||||
|
|||||||
@@ -66,7 +66,11 @@ class ApiClient(models.Model):
|
|||||||
return all(self.has_perm(perm) for perm in perm_list)
|
return all(self.has_perm(perm) for perm in perm_list)
|
||||||
|
|
||||||
def reset_hmac(self, *, commit: bool = True) -> str:
|
def reset_hmac(self, *, commit: bool = True) -> str:
|
||||||
"""Reset and return the HMAC key for this client."""
|
"""Reset and return the HMAC key for this client.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
commit: if True (the default), persist the new hmac in db.
|
||||||
|
"""
|
||||||
self.hmac_key = get_hmac_key()
|
self.hmac_key = get_hmac_key()
|
||||||
if commit:
|
if commit:
|
||||||
self.save()
|
self.save()
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from ninja import ModelSchema
|
from ninja import ModelSchema, Schema
|
||||||
from pydantic import Field
|
from pydantic import Field, HttpUrl
|
||||||
|
|
||||||
from api.models import ApiClient
|
from api.models import ApiClient
|
||||||
from core.schemas import SimpleUserSchema
|
from core.schemas import SimpleUserSchema
|
||||||
@@ -12,3 +12,12 @@ class ApiClientSchema(ModelSchema):
|
|||||||
|
|
||||||
owner: SimpleUserSchema
|
owner: SimpleUserSchema
|
||||||
permissions: list[str] = Field(alias="all_permissions")
|
permissions: list[str] = Field(alias="all_permissions")
|
||||||
|
|
||||||
|
|
||||||
|
class ThirdPartyAuthParamsSchema(Schema):
|
||||||
|
client_id: int
|
||||||
|
third_party_app: str
|
||||||
|
privacy_link: HttpUrl
|
||||||
|
username: str
|
||||||
|
callback_url: HttpUrl
|
||||||
|
signature: str
|
||||||
|
|||||||
4
api/templates/api/third_party/auth.jinja
vendored
4
api/templates/api/third_party/auth.jinja
vendored
@@ -14,8 +14,8 @@
|
|||||||
{% endtrans %}
|
{% endtrans %}
|
||||||
</p>
|
</p>
|
||||||
<p class="margin-bottom">
|
<p class="margin-bottom">
|
||||||
{% trans trimmed app=third_party_app, cgu_link=third_party_cgu, sith_cgu_link=sith_cgu %}
|
{% trans trimmed app=third_party_app, privacy_link=third_party_cgu, sith_cgu_link=sith_cgu %}
|
||||||
The privacy policies of <a href="{{ cgu_link }}">{{ app }}</a>
|
The privacy policies of <a href="{{ privacy_link }}">{{ app }}</a>
|
||||||
and of <a href="{{ sith_cgu_link }}">the Students' Association</a>
|
and of <a href="{{ sith_cgu_link }}">the Students' Association</a>
|
||||||
applies as soon as the form is submitted.
|
applies as soon as the form is submitted.
|
||||||
{% endtrans %}
|
{% endtrans %}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from pytest_django.asserts import assertRedirects
|
|||||||
|
|
||||||
from api.models import ApiClient, get_hmac_key
|
from api.models import ApiClient, get_hmac_key
|
||||||
from core.baker_recipes import subscriber_user
|
from core.baker_recipes import subscriber_user
|
||||||
|
from core.schemas import UserProfileSchema
|
||||||
from core.utils import hmac_hexdigest
|
from core.utils import hmac_hexdigest
|
||||||
|
|
||||||
|
|
||||||
@@ -34,14 +35,14 @@ class TestThirdPartyAuth(TestCase):
|
|||||||
self.query = {
|
self.query = {
|
||||||
"client_id": self.api_client.id,
|
"client_id": self.api_client.id,
|
||||||
"third_party_app": "app",
|
"third_party_app": "app",
|
||||||
"cgu_link": "https://foobar.fr/",
|
"privacy_link": "https://foobar.fr/",
|
||||||
"username": "bibou",
|
"username": "bibou",
|
||||||
"callback_url": "https://callback.fr/",
|
"callback_url": "https://callback.fr/",
|
||||||
}
|
}
|
||||||
self.query["signature"] = hmac_hexdigest(self.api_client.hmac_key, self.query)
|
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.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):
|
def test_auth_ok(self):
|
||||||
|
|||||||
14
api/views.py
14
api/views.py
@@ -10,26 +10,16 @@ from django.core.exceptions import PermissionDenied
|
|||||||
from django.urls import reverse, reverse_lazy
|
from django.urls import reverse, reverse_lazy
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from django.views.generic import FormView, TemplateView
|
from django.views.generic import FormView, TemplateView
|
||||||
from ninja import Schema
|
|
||||||
from ninja_extra.shortcuts import get_object_or_none
|
from ninja_extra.shortcuts import get_object_or_none
|
||||||
from pydantic import HttpUrl
|
|
||||||
|
|
||||||
from api.forms import ThirdPartyAuthForm
|
from api.forms import ThirdPartyAuthForm
|
||||||
from api.models import ApiClient
|
from api.models import ApiClient
|
||||||
|
from api.schemas import ThirdPartyAuthParamsSchema
|
||||||
from core.models import SithFile
|
from core.models import SithFile
|
||||||
from core.schemas import UserProfileSchema
|
from core.schemas import UserProfileSchema
|
||||||
from core.utils import hmac_hexdigest
|
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):
|
class ThirdPartyAuthView(LoginRequiredMixin, FormView):
|
||||||
form_class = ThirdPartyAuthForm
|
form_class = ThirdPartyAuthForm
|
||||||
template_name = "api/third_party/auth.jinja"
|
template_name = "api/third_party/auth.jinja"
|
||||||
@@ -93,7 +83,7 @@ class ThirdPartyAuthView(LoginRequiredMixin, FormView):
|
|||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
return super().get_context_data(**kwargs) | {
|
return super().get_context_data(**kwargs) | {
|
||||||
"third_party_app": self.params.third_party_app,
|
"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),
|
"sith_cgu": SithFile.objects.get(id=settings.SITH_CGU_FILE_ID),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -210,14 +210,14 @@ def get_client_ip(request: HttpRequest) -> str | None:
|
|||||||
def hmac_hexdigest(
|
def hmac_hexdigest(
|
||||||
key: str | bytes,
|
key: str | bytes,
|
||||||
data: Mapping[str, Any] | Sequence[tuple[str, Any]],
|
data: Mapping[str, Any] | Sequence[tuple[str, Any]],
|
||||||
digest: str | Callable[[Buffer], HASH] = "sha256",
|
digest: str | Callable[[Buffer], HASH] = "sha512",
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Return the hexdigest of the signature of the given data.
|
"""Return the hexdigest of the signature of the given data.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
key: the HMAC key used for the signature
|
key: the HMAC key used for the signature
|
||||||
data: the data to sign
|
data: the data to sign
|
||||||
digest: a PEP247 hashing algorithm
|
digest: a PEP247 hashing algorithm (by default, sha512)
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
```python
|
```python
|
||||||
@@ -226,7 +226,7 @@ def hmac_hexdigest(
|
|||||||
"bar": "somevalue",
|
"bar": "somevalue",
|
||||||
}
|
}
|
||||||
hmac_key = secrets.token_hex(64)
|
hmac_key = secrets.token_hex(64)
|
||||||
signature = hmac_hexdigest(hmac_key, data, "sha512")
|
signature = hmac_hexdigest(hmac_key, data, "sha256")
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
if isinstance(key, str):
|
if isinstance(key, str):
|
||||||
|
|||||||
1
docs/reference/api/schemas.md
Normal file
1
docs/reference/api/schemas.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
::: api.schemas
|
||||||
1
docs/reference/api/views.md
Normal file
1
docs/reference/api/views.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
::: api.views
|
||||||
353
docs/tutorial/api/account-link.md
Normal file
353
docs/tutorial/api/account-link.md
Normal file
@@ -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<br/>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<br/>des conditions<br/>d'utilisation
|
||||||
|
User-->>-Sith: Validation
|
||||||
|
Sith->>+App: URL de retour<br/>avec données utilisateur
|
||||||
|
App->>App: Traitement des <br/>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<br/>avec signature
|
||||||
|
opt
|
||||||
|
Sith->>+App: URL de retour<br/>avec données utilisateur
|
||||||
|
App->>App: Traitement des <br/>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<br/>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 <br>pas signées ou sont mal signées |
|
||||||
|
| 404 | L'URL de retour ne permet pas <br>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<SimpleHmac<Sha512>> {
|
||||||
|
let urlencoded = serde_urlencoded::to_string(self).unwrap();
|
||||||
|
SimpleHmac::<Sha512>::new_from_slice(key)
|
||||||
|
.unwrap()
|
||||||
|
.chain_update(urlencoded.as_bytes())
|
||||||
|
.finalize()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Into<Url> for UrlData<'_> {
|
||||||
|
fn into(self) -> Url {
|
||||||
|
let key = std::env::var("HMAC_KEY").unwrap();
|
||||||
|
let mut url = Url::parse("http://ae.utbm.fr/api-link/auth/").unwrap();
|
||||||
|
url.set_query(Some(
|
||||||
|
format!(
|
||||||
|
"{}&signature={:x}",
|
||||||
|
serde_urlencoded::to_string(&self).unwrap(),
|
||||||
|
self.signature(key.as_bytes()).into_bytes()
|
||||||
|
)
|
||||||
|
.as_str(),
|
||||||
|
));
|
||||||
|
url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
dotenvy::dotenv().expect("Couldn't load env");
|
||||||
|
let data = UrlData {
|
||||||
|
client_id: 1,
|
||||||
|
third_party_app: "discord",
|
||||||
|
privacy_link: "https://discord.com/privacy/".parse().unwrap(),
|
||||||
|
username: "Brian",
|
||||||
|
callback_url: "https://bot.ae.utbm.fr/callback/123456789/"
|
||||||
|
.parse()
|
||||||
|
.unwrap(),
|
||||||
|
};
|
||||||
|
let url: Url = data.into();
|
||||||
|
println!("{:?}", url);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
???Example "Vérification de la signature de la réponse"
|
||||||
|
|
||||||
|
Les données utilisateur peuvent ressembler à :
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"user": {
|
||||||
|
"display_name": "Matthieu Vincent",
|
||||||
|
"profile_url": "/user/380/",
|
||||||
|
"profile_pict": "/static/core/img/unknown.jpg",
|
||||||
|
"id": 380,
|
||||||
|
"nick_name": None,
|
||||||
|
"first_name": "Matthieu",
|
||||||
|
"last_name": "Vincent",
|
||||||
|
},
|
||||||
|
"signature": "3802a280fbb01bd9fetc."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Vous pouvez vérifier la signature ainsi :
|
||||||
|
|
||||||
|
```python
|
||||||
|
import hmac
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
|
from environs import Env
|
||||||
|
|
||||||
|
env = Env()
|
||||||
|
env.read_env()
|
||||||
|
|
||||||
|
def is_signature_valid(user_data: dict, signature: str) -> bool:
|
||||||
|
key = env.str("HMAC_KEY").encode()
|
||||||
|
urlencoded = urlencode(user_data)
|
||||||
|
return hmac.compare_digest(
|
||||||
|
hmac.digest(key, urlencoded.encode(), "sha512").hex(),
|
||||||
|
signature,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
post_data = <récupération des données POST>
|
||||||
|
print(
|
||||||
|
"signature valide :",
|
||||||
|
is_signature_valid(post_data["user"], post_data["signature"]
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
!!!Warning
|
||||||
|
|
||||||
|
Vous devez impérativement vérifier la signature
|
||||||
|
des données de la requête de callback !
|
||||||
|
|
||||||
|
Si l'équipe informatique se rend compte que vous ne le faites pas,
|
||||||
|
elle se réserve le droit de suspendre votre application,
|
||||||
|
immédiatement et sans préavis.
|
||||||
@@ -112,7 +112,7 @@ cf. [HTTP persistant connection (wikipedia)](https://en.wikipedia.org/wiki/HTTP_
|
|||||||
|
|
||||||
Voici quelques exemples :
|
Voici quelques exemples :
|
||||||
|
|
||||||
=== "Python (requests)"
|
=== ":simple-python: Python (requests)"
|
||||||
|
|
||||||
Dépendances :
|
Dépendances :
|
||||||
|
|
||||||
@@ -132,7 +132,7 @@ Voici quelques exemples :
|
|||||||
print(response.json())
|
print(response.json())
|
||||||
```
|
```
|
||||||
|
|
||||||
=== "Python (aiohttp)"
|
=== ":simple-python: Python (aiohttp)"
|
||||||
|
|
||||||
Dépendances :
|
Dépendances :
|
||||||
|
|
||||||
@@ -158,7 +158,7 @@ Voici quelques exemples :
|
|||||||
asyncio.run(main())
|
asyncio.run(main())
|
||||||
```
|
```
|
||||||
|
|
||||||
=== "Javascript (axios)"
|
=== ":simple-javascript: Javascript (axios)"
|
||||||
|
|
||||||
Dépendances :
|
Dépendances :
|
||||||
|
|
||||||
@@ -178,7 +178,7 @@ Voici quelques exemples :
|
|||||||
console.log(await instance.get("club/1").json());
|
console.log(await instance.get("club/1").json());
|
||||||
```
|
```
|
||||||
|
|
||||||
=== "Rust (reqwest)"
|
=== ":simple-rust: Rust (reqwest)"
|
||||||
|
|
||||||
Dépendances :
|
Dépendances :
|
||||||
|
|
||||||
|
|||||||
@@ -144,11 +144,11 @@ msgstr ""
|
|||||||
#: api/templates/api/third_party/auth.jinja
|
#: api/templates/api/third_party/auth.jinja
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"The privacy policies of <a href=\"%(cgu_link)s\">%(app)s</a> and of <a "
|
"The privacy policies of <a href=\"%(privacy_link)s\">%(app)s</a> and of <a "
|
||||||
"href=\"%(sith_cgu_link)s\">the Students' Association</a> applies as soon as "
|
"href=\"%(sith_cgu_link)s\">the Students' Association</a> applies as soon as "
|
||||||
"the form is submitted."
|
"the form is submitted."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Les politiques de confidentialité de <a href=\"%(cgu_link)s\">%(app)s</a> et de <a "
|
"Les politiques de confidentialité de <a href=\"%(privacy_link)s\">%(app)s</a> et de <a "
|
||||||
"href=\"%(sith_cgu_link)s\">l'Association des Etudiants</a> s'appliquent dès la soumission "
|
"href=\"%(sith_cgu_link)s\">l'Association des Etudiants</a> s'appliquent dès la soumission "
|
||||||
"du formulaire."
|
"du formulaire."
|
||||||
|
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ nav:
|
|||||||
- API:
|
- API:
|
||||||
- Développement: tutorial/api/dev.md
|
- Développement: tutorial/api/dev.md
|
||||||
- Connexion à l'API: tutorial/api/connect.md
|
- Connexion à l'API: tutorial/api/connect.md
|
||||||
|
- Liaison avec le compte AE: tutorial/api/account-link.md
|
||||||
- Etransactions: tutorial/etransaction.md
|
- Etransactions: tutorial/etransaction.md
|
||||||
- How-to:
|
- How-to:
|
||||||
- L'ORM de Django: howto/querysets.md
|
- L'ORM de Django: howto/querysets.md
|
||||||
@@ -91,6 +92,8 @@ nav:
|
|||||||
- reference/api/hashers.md
|
- reference/api/hashers.md
|
||||||
- reference/api/models.md
|
- reference/api/models.md
|
||||||
- reference/api/perms.md
|
- reference/api/perms.md
|
||||||
|
- reference/api/schemas.md
|
||||||
|
- reference/api/views.md
|
||||||
- club:
|
- club:
|
||||||
- reference/club/models.md
|
- reference/club/models.md
|
||||||
- reference/club/views.md
|
- reference/club/views.md
|
||||||
|
|||||||
Reference in New Issue
Block a user