mirror of
				https://github.com/ae-utbm/sith.git
				synced 2025-11-04 11:03:04 +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,16 @@ 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