diff --git a/apps/activity/forms.py b/apps/activity/forms.py
index 60c1831147919c11f1a053432140cc4bd2d13209..b40463c0ec9553992b83d911e650916154f6a626 100644
--- a/apps/activity/forms.py
+++ b/apps/activity/forms.py
@@ -11,7 +11,7 @@ from django.utils.translation import gettext_lazy as _
 from member.models import Club
 from note.models import Note, NoteUser
 from note_kfet.inputs import Autocomplete, DateTimePickerInput
-from note_kfet.middlewares import get_current_authenticated_user
+from note_kfet.middlewares import get_current_request
 from permission.backends import PermissionBackend
 
 from .models import Activity, Guest
@@ -24,7 +24,7 @@ class ActivityForm(forms.ModelForm):
         self.fields["attendees_club"].initial = Club.objects.get(name="Kfet")
         self.fields["attendees_club"].widget.attrs["placeholder"] = "Kfet"
         clubs = list(Club.objects.filter(PermissionBackend
-                                         .filter_queryset(get_current_authenticated_user(), Club, "view")).all())
+                                         .filter_queryset(get_current_request(), Club, "view")).all())
         shuffle(clubs)
         self.fields["organizer"].widget.attrs["placeholder"] = ", ".join(club.name for club in clubs[:4]) + ", ..."
 
diff --git a/apps/activity/views.py b/apps/activity/views.py
index 86914caf95e6063c562a6edb6dc10372350b4bb0..a2ae59abcb8035acf033ac298bad446dd2a8ed98 100644
--- a/apps/activity/views.py
+++ b/apps/activity/views.py
@@ -74,12 +74,12 @@ class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView
 
         upcoming_activities = Activity.objects.filter(date_end__gt=timezone.now())
         context['upcoming'] = ActivityTable(
-            data=upcoming_activities.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view")),
+            data=upcoming_activities.filter(PermissionBackend.filter_queryset(self.request, Activity, "view")),
             prefix='upcoming-',
         )
 
         started_activities = Activity.objects\
-            .filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view"))\
+            .filter(PermissionBackend.filter_queryset(self.request, Activity, "view"))\
             .filter(open=True, valid=True).all()
         context["started_activities"] = started_activities
 
@@ -98,7 +98,7 @@ class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
         context = super().get_context_data()
 
         table = GuestTable(data=Guest.objects.filter(activity=self.object)
-                           .filter(PermissionBackend.filter_queryset(self.request.user, Guest, "view")))
+                           .filter(PermissionBackend.filter_queryset(self.request, Guest, "view")))
         context["guests"] = table
 
         context["activity_started"] = timezone.now() > timezone.localtime(self.object.date_start)
@@ -144,7 +144,7 @@ class ActivityInviteView(ProtectQuerysetMixin, ProtectedCreateView):
 
     def get_form(self, form_class=None):
         form = super().get_form(form_class)
-        form.activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view"))\
+        form.activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request, Activity, "view"))\
             .get(pk=self.kwargs["pk"])
         form.fields["inviter"].initial = self.request.user.note
         return form
@@ -152,7 +152,7 @@ class ActivityInviteView(ProtectQuerysetMixin, ProtectedCreateView):
     @transaction.atomic
     def form_valid(self, form):
         form.instance.activity = Activity.objects\
-            .filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view")).get(pk=self.kwargs["pk"])
+            .filter(PermissionBackend.filter_queryset(self.request, Activity, "view")).get(pk=self.kwargs["pk"])
         return super().form_valid(form)
 
     def get_success_url(self, **kwargs):
@@ -173,7 +173,7 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
         activity = Activity.objects.get(pk=self.kwargs["pk"])
 
         sample_entry = Entry(activity=activity, note=self.request.user.note)
-        if not PermissionBackend.check_perm(self.request.user, "activity.add_entry", sample_entry):
+        if not PermissionBackend.check_perm(self.request, "activity.add_entry", sample_entry):
             raise PermissionDenied(_("You are not allowed to display the entry interface for this activity."))
 
         if not activity.activity_type.manage_entries:
@@ -191,7 +191,7 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
         guest_qs = Guest.objects\
             .annotate(balance=F("inviter__balance"), note_name=F("inviter__user__username"))\
             .filter(activity=activity)\
-            .filter(PermissionBackend.filter_queryset(self.request.user, Guest, "view"))\
+            .filter(PermissionBackend.filter_queryset(self.request, Guest, "view"))\
             .order_by('last_name', 'first_name').distinct()
 
         if "search" in self.request.GET and self.request.GET["search"]:
@@ -230,7 +230,7 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
         )
 
         # Filter with permission backend
-        note_qs = note_qs.filter(PermissionBackend.filter_queryset(self.request.user, Alias, "view"))
+        note_qs = note_qs.filter(PermissionBackend.filter_queryset(self.request, Alias, "view"))
 
         if "search" in self.request.GET and self.request.GET["search"]:
             pattern = self.request.GET["search"]
@@ -256,7 +256,7 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
         """
         context = super().get_context_data(**kwargs)
 
-        activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view"))\
+        activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request, Activity, "view"))\
             .distinct().get(pk=self.kwargs["pk"])
         context["activity"] = activity
 
@@ -281,9 +281,9 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
         context["notespecial_ctype"] = ContentType.objects.get_for_model(NoteSpecial).pk
 
         activities_open = Activity.objects.filter(open=True).filter(
-            PermissionBackend.filter_queryset(self.request.user, Activity, "view")).distinct().all()
+            PermissionBackend.filter_queryset(self.request, Activity, "view")).distinct().all()
         context["activities_open"] = [a for a in activities_open
-                                      if PermissionBackend.check_perm(self.request.user,
+                                      if PermissionBackend.check_perm(self.request,
                                                                       "activity.add_entry",
                                                                       Entry(activity=a, note=self.request.user.note,))]
 
diff --git a/apps/api/viewsets.py b/apps/api/viewsets.py
index 25221cfcee4dcfd234c6089d97a716709181e4e5..faeadee1f5f9b15cf646c600fed36f514328fa09 100644
--- a/apps/api/viewsets.py
+++ b/apps/api/viewsets.py
@@ -9,7 +9,6 @@ from django.contrib.auth.models import User
 from rest_framework.filters import SearchFilter
 from rest_framework.viewsets import ReadOnlyModelViewSet, ModelViewSet
 from permission.backends import PermissionBackend
-from note_kfet.middlewares import get_current_session
 from note.models import Alias
 
 from .serializers import UserSerializer, ContentTypeSerializer
@@ -25,9 +24,7 @@ class ReadProtectedModelViewSet(ModelViewSet):
         self.model = ContentType.objects.get_for_model(self.serializer_class.Meta.model).model_class()
 
     def get_queryset(self):
-        user = self.request.user
-        get_current_session().setdefault("permission_mask", 42)
-        return self.queryset.filter(PermissionBackend.filter_queryset(user, self.model, "view")).distinct()
+        return self.queryset.filter(PermissionBackend.filter_queryset(self.request, self.model, "view")).distinct()
 
 
 class ReadOnlyProtectedModelViewSet(ReadOnlyModelViewSet):
@@ -40,9 +37,7 @@ class ReadOnlyProtectedModelViewSet(ReadOnlyModelViewSet):
         self.model = ContentType.objects.get_for_model(self.serializer_class.Meta.model).model_class()
 
     def get_queryset(self):
-        user = self.request.user
-        get_current_session().setdefault("permission_mask", 42)
-        return self.queryset.filter(PermissionBackend.filter_queryset(user, self.model, "view")).distinct()
+        return self.queryset.filter(PermissionBackend.filter_queryset(self.request, self.model, "view")).distinct()
 
 
 class UserViewSet(ReadProtectedModelViewSet):
diff --git a/apps/logs/signals.py b/apps/logs/signals.py
index 862dbd751c9e5ca32394148b29c01482c194eeb3..a3166eed00a3445f8f75044cda105597566640e3 100644
--- a/apps/logs/signals.py
+++ b/apps/logs/signals.py
@@ -5,7 +5,7 @@ from django.contrib.contenttypes.models import ContentType
 from rest_framework.renderers import JSONRenderer
 from rest_framework.serializers import ModelSerializer
 from note.models import NoteUser, Alias
-from note_kfet.middlewares import get_current_authenticated_user, get_current_ip
+from note_kfet.middlewares import get_current_request
 
 from .models import Changelog
 
@@ -57,9 +57,9 @@ def save_object(sender, instance, **kwargs):
     previous = instance._previous
 
     # Si un utilisateur est connecté, on récupère l'utilisateur courant ainsi que son adresse IP
-    user, ip = get_current_authenticated_user(), get_current_ip()
+    request = get_current_request()
 
-    if user is None:
+    if request is None:
         # Si la modification n'a pas été faite via le client Web, on suppose que c'est du à `manage.py`
         # On récupère alors l'utilisateur·trice connecté·e à la VM, et on récupère la note associée
         # IMPORTANT : l'utilisateur dans la VM doit être un des alias note du respo info
@@ -71,9 +71,23 @@ def save_object(sender, instance, **kwargs):
         # else:
         if note.exists():
             user = note.get().user
+        else:
+            user = None
+    else:
+        user = request.user
+        if 'HTTP_X_REAL_IP' in request.META:
+            ip = request.META.get('HTTP_X_REAL_IP')
+        elif 'HTTP_X_FORWARDED_FOR' in request.META:
+            ip = request.META.get('HTTP_X_FORWARDED_FOR').split(', ')[0]
+        else:
+            ip = request.META.get('REMOTE_ADDR')
+
+        if not user.is_authenticated:
+            # For registration and OAuth2 purposes
+            user = None
 
     # noinspection PyProtectedMember
-    if user is not None and instance._meta.label_lower == "auth.user" and previous:
+    if request is not None and instance._meta.label_lower == "auth.user" and previous:
         # On n'enregistre pas les connexions
         if instance.last_login != previous.last_login:
             return
@@ -121,9 +135,9 @@ def delete_object(sender, instance, **kwargs):
         return
 
     # Si un utilisateur est connecté, on récupère l'utilisateur courant ainsi que son adresse IP
-    user, ip = get_current_authenticated_user(), get_current_ip()
+    request = get_current_request()
 
-    if user is None:
+    if request is None:
         # Si la modification n'a pas été faite via le client Web, on suppose que c'est du à `manage.py`
         # On récupère alors l'utilisateur·trice connecté·e à la VM, et on récupère la note associée
         # IMPORTANT : l'utilisateur dans la VM doit être un des alias note du respo info
@@ -135,6 +149,20 @@ def delete_object(sender, instance, **kwargs):
         # else:
         if note.exists():
             user = note.get().user
+        else:
+            user = None
+    else:
+        user = request.user
+        if 'HTTP_X_REAL_IP' in request.META:
+            ip = request.META.get('HTTP_X_REAL_IP')
+        elif 'HTTP_X_FORWARDED_FOR' in request.META:
+            ip = request.META.get('HTTP_X_FORWARDED_FOR').split(', ')[0]
+        else:
+            ip = request.META.get('REMOTE_ADDR')
+
+        if not user.is_authenticated:
+            # For registration and OAuth2 purposes
+            user = None
 
     # On crée notre propre sérialiseur JSON pour pouvoir sauvegarder les modèles
     class CustomSerializer(ModelSerializer):
diff --git a/apps/member/hashers.py b/apps/member/hashers.py
index 99b2c30ee2771061e4ae3717c6134a95c1f233e2..69db24b0523c600c3a5e2682d104303b93beec73 100644
--- a/apps/member/hashers.py
+++ b/apps/member/hashers.py
@@ -6,7 +6,7 @@ import hashlib
 from django.conf import settings
 from django.contrib.auth.hashers import PBKDF2PasswordHasher
 from django.utils.crypto import constant_time_compare
-from note_kfet.middlewares import get_current_authenticated_user, get_current_session
+from note_kfet.middlewares import get_current_request
 
 
 class CustomNK15Hasher(PBKDF2PasswordHasher):
@@ -24,16 +24,22 @@ class CustomNK15Hasher(PBKDF2PasswordHasher):
 
     def must_update(self, encoded):
         if settings.DEBUG:
-            current_user = get_current_authenticated_user()
+            # Small hack to let superusers to impersonate people.
+            # Don't change their password.
+            request = get_current_request()
+            current_user = request.user
             if current_user is not None and current_user.is_superuser:
                 return False
         return True
 
     def verify(self, password, encoded):
         if settings.DEBUG:
-            current_user = get_current_authenticated_user()
+            # Small hack to let superusers to impersonate people.
+            # If a superuser is already connected, let him/her log in as another person.
+            request = get_current_request()
+            current_user = request.user
             if current_user is not None and current_user.is_superuser\
-                    and get_current_session().get("permission_mask", -1) >= 42:
+                    and request.session.get("permission_mask", -1) >= 42:
                 return True
 
         if '|' in encoded:
@@ -51,8 +57,11 @@ class DebugSuperuserBackdoor(PBKDF2PasswordHasher):
 
     def verify(self, password, encoded):
         if settings.DEBUG:
-            current_user = get_current_authenticated_user()
+            # Small hack to let superusers to impersonate people.
+            # If a superuser is already connected, let him/her log in as another person.
+            request = get_current_request()
+            current_user = request.user
             if current_user is not None and current_user.is_superuser\
-                    and get_current_session().get("permission_mask", -1) >= 42:
+                    and request.session.get("permission_mask", -1) >= 42:
                 return True
         return super().verify(password, encoded)
diff --git a/apps/member/tables.py b/apps/member/tables.py
index d97da7caaf020ed3a9e75dadb0defcc2fb7a9835..1c152526f7534b691dcb3d2406b8a7ff295248c4 100644
--- a/apps/member/tables.py
+++ b/apps/member/tables.py
@@ -9,7 +9,7 @@ from django.utils.translation import gettext_lazy as _
 from django.urls import reverse_lazy
 from django.utils.html import format_html
 from note.templatetags.pretty_money import pretty_money
-from note_kfet.middlewares import get_current_authenticated_user
+from note_kfet.middlewares import get_current_request
 from permission.backends import PermissionBackend
 
 from .models import Club, Membership
@@ -51,19 +51,19 @@ class UserTable(tables.Table):
     def render_email(self, record, value):
         # Replace the email by a dash if the user can't see the profile detail
         # Replace also the URL
-        if not PermissionBackend.check_perm(get_current_authenticated_user(), "member.view_profile", record.profile):
+        if not PermissionBackend.check_perm(get_current_request(), "member.view_profile", record.profile):
             value = "—"
             record.email = value
         return value
 
     def render_section(self, record, value):
         return value \
-            if PermissionBackend.check_perm(get_current_authenticated_user(), "member.view_profile", record.profile) \
+            if PermissionBackend.check_perm(get_current_request(), "member.view_profile", record.profile) \
             else "—"
 
     def render_balance(self, record, value):
         return pretty_money(value)\
-            if PermissionBackend.check_perm(get_current_authenticated_user(), "note.view_note", record.note) else "—"
+            if PermissionBackend.check_perm(get_current_request(), "note.view_note", record.note) else "—"
 
     class Meta:
         attrs = {
@@ -93,7 +93,7 @@ class MembershipTable(tables.Table):
     def render_user(self, value):
         # If the user has the right, link the displayed user with the page of its detail.
         s = value.username
-        if PermissionBackend.check_perm(get_current_authenticated_user(), "auth.view_user", value):
+        if PermissionBackend.check_perm(get_current_request(), "auth.view_user", value):
             s = format_html("<a href={url}>{name}</a>",
                             url=reverse_lazy('member:user_detail', kwargs={"pk": value.pk}), name=s)
 
@@ -102,7 +102,7 @@ class MembershipTable(tables.Table):
     def render_club(self, value):
         # If the user has the right, link the displayed club with the page of its detail.
         s = value.name
-        if PermissionBackend.check_perm(get_current_authenticated_user(), "member.view_club", value):
+        if PermissionBackend.check_perm(get_current_request(), "member.view_club", value):
             s = format_html("<a href={url}>{name}</a>",
                             url=reverse_lazy('member:club_detail', kwargs={"pk": value.pk}), name=s)
 
@@ -127,7 +127,7 @@ class MembershipTable(tables.Table):
                     date_end=date.today(),
                     fee=0,
                 )
-                if PermissionBackend.check_perm(get_current_authenticated_user(),
+                if PermissionBackend.check_perm(get_current_request(),
                                                 "member.add_membership", empty_membership):  # If the user has right
                     renew_url = reverse_lazy('member:club_renew_membership',
                                              kwargs={"pk": record.pk})
@@ -142,7 +142,7 @@ class MembershipTable(tables.Table):
         # If the user has the right to manage the roles, display the link to manage them
         roles = record.roles.all()
         s = ", ".join(str(role) for role in roles)
-        if PermissionBackend.check_perm(get_current_authenticated_user(), "member.change_membership_roles", record):
+        if PermissionBackend.check_perm(get_current_request(), "member.change_membership_roles", record):
             s = format_html("<a href='" + str(reverse_lazy("member:club_manage_roles", kwargs={"pk": record.pk}))
                             + "'>" + s + "</a>")
         return s
@@ -165,7 +165,7 @@ class ClubManagerTable(tables.Table):
     def render_user(self, value):
         # If the user has the right, link the displayed user with the page of its detail.
         s = value.username
-        if PermissionBackend.check_perm(get_current_authenticated_user(), "auth.view_user", value):
+        if PermissionBackend.check_perm(get_current_request(), "auth.view_user", value):
             s = format_html("<a href={url}>{name}</a>",
                             url=reverse_lazy('member:user_detail', kwargs={"pk": value.pk}), name=s)
 
diff --git a/apps/member/templates/member/manage_auth_tokens.html b/apps/member/templates/member/manage_auth_tokens.html
index 014686f1e6a20284435a7debff5d4ced9750b1f1..0f6a64eccb5b7f7122dd6b4db788926b4eadcfab 100644
--- a/apps/member/templates/member/manage_auth_tokens.html
+++ b/apps/member/templates/member/manage_auth_tokens.html
@@ -5,32 +5,98 @@ SPDX-License-Identifier: GPL-3.0-or-later
 {% load i18n %}
 
 {% block content %}
-<div class="alert alert-info">
-    <h4>À quoi sert un jeton d'authentification ?</h4>
+<div class="row mt-4">
+    <div class="col-xl-6">
+        <div class="card">
+            <div class="card-header text-center">
+                <h3>{% trans "Token authentication" %}</h3>
+            </div>
+            <div class="card-body">
+                <div class="alert alert-info">
+                    <h4>À quoi sert un jeton d'authentification ?</h4>
 
-    Un jeton vous permet de vous connecter à <a href="/api/">l'API de la Note Kfet</a>.<br />
-    Il suffit pour cela d'ajouter en en-tête de vos requêtes <code>Authorization: Token &lt;TOKEN&gt;</code>
-    pour pouvoir vous identifier.<br /><br />
+                    Un jeton vous permet de vous connecter à <a href="/api/">l'API de la Note Kfet</a> via votre propre compte
+                    depuis un client externe.<br />
+                    Il suffit pour cela d'ajouter en en-tête de vos requêtes <code>Authorization: Token &lt;TOKEN&gt;</code>
+                    pour pouvoir vous identifier.<br /><br />
 
-    Une documentation de l'API arrivera ultérieurement.
-</div>
+                    La documentation de l'API est disponible ici :
+                    <a href="/doc/api/">{{ request.scheme }}://{{ request.get_host }}/doc/api/</a>.
+                </div>
 
-<div class="alert alert-info">
-    <strong>{%trans  'Token' %} :</strong>
-    {% if 'show' in request.GET %}
-    {{ token.key }} (<a href="?">cacher</a>)
-    {% else %}
-    <em>caché</em> (<a href="?show">montrer</a>)
-    {% endif %}
-    <br />
-    <strong>{%trans  'Created' %} :</strong> {{ token.created }}
-</div>
+                <div class="alert alert-info">
+                    <strong>{%trans  'Token' %} :</strong>
+                    {% if 'show' in request.GET %}
+                    {{ token.key }} (<a href="?">cacher</a>)
+                    {% else %}
+                    <em>caché</em> (<a href="?show">montrer</a>)
+                    {% endif %}
+                    <br />
+                    <strong>{%trans  'Created' %} :</strong> {{ token.created }}
+                </div>
 
-<div class="alert alert-warning">
-    <strong>Attention :</strong> regénérer le jeton va révoquer tout accès autorisé à l'API via ce jeton !
-</div>
+                <div class="alert alert-warning">
+                    <strong>{% trans "Warning" %} :</strong> regénérer le jeton va révoquer tout accès autorisé à l'API via ce jeton !
+                </div>
+            </div>
+            <div class="card-footer text-center">
+                <a href="?regenerate">
+                    <button class="btn btn-primary">{% trans 'Regenerate token' %}</button>
+                </a>
+            </div>
+        </div>
+    </div>
+
+    <div class="col-xl-6">
+        <div class="card">
+            <div class="card-header text-center">
+                <h3>{% trans "OAuth2 authentication" %}</h3>
+            </div>
+            <div class="card-header">
+                <div class="alert alert-info">
+                    <p>
+                        La Note Kfet implémente également le protocole <a href="https://oauth.net/2/">OAuth2</a>, afin de
+                        permettre à des applications tierces d'interagir avec la Note en récoltant des informations
+                        (de connexion par exemple) voir en permettant des modifications à distance, par exemple lorsqu'il
+                        s'agit d'avoir un site marchand sur lequel faire des transactions via la Note Kfet.
+                    </p>
 
-<a href="?regenerate">
-    <button class="btn btn-primary">{% trans 'Regenerate token' %}</button>
-</a>
+                    <p>
+                        L'usage de ce protocole est recommandé pour tout usage non personnel, car permet de mieux cibler
+                        les droits dont on a besoin, en restreignant leur usage par jeton généré.
+                    </p>
+
+                    <p>
+                        La documentation vis-à-vis de l'usage de ce protocole est disponible ici :
+                        <a href="/doc/external_services/oauth2/">{{ request.scheme }}://{{ request.get_host }}/doc/external_services/oauth2/</a>.
+                    </p>
+                </div>
+
+                Liste des URL à communiquer à votre application :
+
+                <ul>
+                    <li>
+                        {% trans "Authorization:" %}
+                        <a href="{% url 'oauth2_provider:authorize' %}">{{ request.scheme }}://{{ request.get_host }}{% url 'oauth2_provider:authorize' %}</a>
+                    </li>
+                    <li>
+                        {% trans "Token:" %}
+                        <a href="{% url 'oauth2_provider:authorize' %}">{{ request.scheme }}://{{ request.get_host }}{% url 'oauth2_provider:token' %}</a>
+                    </li>
+                    <li>
+                        {% trans "Revoke Token:" %}
+                        <a href="{% url 'oauth2_provider:authorize' %}">{{ request.scheme }}://{{ request.get_host }}{% url 'oauth2_provider:revoke-token' %}</a>
+                    </li>
+                    <li>
+                        {% trans "Introspect Token:" %}
+                        <a href="{% url 'oauth2_provider:authorize' %}">{{ request.scheme }}://{{ request.get_host }}{% url 'oauth2_provider:introspect' %}</a>
+                    </li>
+                </ul>
+            </div>
+            <div class="card-footer text-center">
+                <a class="btn btn-primary" href="{% url 'oauth2_provider:list' %}">{% trans "Show my applications" %}</a>
+            </div>
+        </div>
+    </div>
+</div>
 {% endblock %}
\ No newline at end of file
diff --git a/apps/member/views.py b/apps/member/views.py
index 514c0644acc4e141872c24b69a787b488536f65d..39edcc0b9cac312aedd8938c306a091e9877bfaf 100644
--- a/apps/member/views.py
+++ b/apps/member/views.py
@@ -21,7 +21,7 @@ from rest_framework.authtoken.models import Token
 from note.models import Alias, NoteUser
 from note.models.transactions import Transaction, SpecialTransaction
 from note.tables import HistoryTable, AliasTable
-from note_kfet.middlewares import _set_current_user_and_ip
+from note_kfet.middlewares import _set_current_request
 from permission.backends import PermissionBackend
 from permission.models import Role
 from permission.views import ProtectQuerysetMixin, ProtectedCreateView
@@ -41,7 +41,8 @@ class CustomLoginView(LoginView):
     @transaction.atomic
     def form_valid(self, form):
         logout(self.request)
-        _set_current_user_and_ip(form.get_user(), self.request.session, None)
+        self.request.user = form.get_user()
+        _set_current_request(self.request)
         self.request.session['permission_mask'] = form.cleaned_data['permission_mask'].rank
         return super().form_valid(form)
 
@@ -70,7 +71,7 @@ class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
         form.fields['email'].required = True
         form.fields['email'].help_text = _("This address must be valid.")
 
-        if PermissionBackend.check_perm(self.request.user, "member.change_profile", context['user_object'].profile):
+        if PermissionBackend.check_perm(self.request, "member.change_profile", context['user_object'].profile):
             context['profile_form'] = self.profile_form(instance=context['user_object'].profile,
                                                         data=self.request.POST if self.request.POST else None)
             if not self.object.profile.report_frequency:
@@ -153,13 +154,13 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
         history_list = \
             Transaction.objects.all().filter(Q(source=user.note) | Q(destination=user.note))\
             .order_by("-created_at")\
-            .filter(PermissionBackend.filter_queryset(self.request.user, Transaction, "view"))
+            .filter(PermissionBackend.filter_queryset(self.request, Transaction, "view"))
         history_table = HistoryTable(history_list, prefix='transaction-')
         history_table.paginate(per_page=20, page=self.request.GET.get("transaction-page", 1))
         context['history_list'] = history_table
 
         club_list = Membership.objects.filter(user=user, date_end__gte=date.today() - timedelta(days=15))\
-            .filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view"))\
+            .filter(PermissionBackend.filter_queryset(self.request, Membership, "view"))\
             .order_by("club__name", "-date_start")
         # Display only the most recent membership
         club_list = club_list.distinct("club__name")\
@@ -176,21 +177,20 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
             modified_note.is_active = True
             modified_note.inactivity_reason = 'manual'
             context["can_lock_note"] = user.note.is_active and PermissionBackend\
-                                           .check_perm(self.request.user, "note.change_noteuser_is_active",
-                                                       modified_note)
+                                           .check_perm(self.request, "note.change_noteuser_is_active", modified_note)
             old_note = NoteUser.objects.select_for_update().get(pk=user.note.pk)
             modified_note.inactivity_reason = 'forced'
             modified_note._force_save = True
             modified_note.save()
             context["can_force_lock"] = user.note.is_active and PermissionBackend\
-                .check_perm(self.request.user, "note.change_note_is_active", modified_note)
+                .check_perm(self.request, "note.change_note_is_active", modified_note)
             old_note._force_save = True
             old_note._no_signal = True
             old_note.save()
             modified_note.refresh_from_db()
             modified_note.is_active = True
             context["can_unlock_note"] = not user.note.is_active and PermissionBackend\
-                .check_perm(self.request.user, "note.change_note_is_active", modified_note)
+                .check_perm(self.request, "note.change_note_is_active", modified_note)
 
         return context
 
@@ -237,7 +237,7 @@ class UserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
 
     def get_context_data(self, **kwargs):
         context = super().get_context_data(**kwargs)
-        pre_registered_users = User.objects.filter(PermissionBackend.filter_queryset(self.request.user, User, "view"))\
+        pre_registered_users = User.objects.filter(PermissionBackend.filter_queryset(self.request, User, "view"))\
             .filter(profile__registration_valid=False)
         context["can_manage_registrations"] = pre_registered_users.exists()
         return context
@@ -256,8 +256,8 @@ class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
         context = super().get_context_data(**kwargs)
         note = context['object'].note
         context["aliases"] = AliasTable(
-            note.alias.filter(PermissionBackend.filter_queryset(self.request.user, Alias, "view")).distinct().all())
-        context["can_create"] = PermissionBackend.check_perm(self.request.user, "note.add_alias", Alias(
+            note.alias.filter(PermissionBackend.filter_queryset(self.request, Alias, "view")).distinct().all())
+        context["can_create"] = PermissionBackend.check_perm(self.request, "note.add_alias", Alias(
             note=context["object"].note,
             name="",
             normalized_name="",
@@ -382,7 +382,7 @@ class ClubListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
 
     def get_context_data(self, **kwargs):
         context = super().get_context_data(**kwargs)
-        context["can_add_club"] = PermissionBackend.check_perm(self.request.user, "member.add_club", Club(
+        context["can_add_club"] = PermissionBackend.check_perm(self.request, "member.add_club", Club(
             name="",
             email="club@example.com",
         ))
@@ -404,7 +404,7 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
         context = super().get_context_data(**kwargs)
 
         club = context["club"]
-        if PermissionBackend.check_perm(self.request.user, "member.change_club_membership_start", club):
+        if PermissionBackend.check_perm(self.request, "member.change_club_membership_start", club):
             club.update_membership_dates()
         # managers list
         managers = Membership.objects.filter(club=self.object, roles__name="Bureau de club",
@@ -413,7 +413,7 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
         context["managers"] = ClubManagerTable(data=managers, prefix="managers-")
         # transaction history
         club_transactions = Transaction.objects.all().filter(Q(source=club.note) | Q(destination=club.note))\
-            .filter(PermissionBackend.filter_queryset(self.request.user, Transaction, "view"))\
+            .filter(PermissionBackend.filter_queryset(self.request, Transaction, "view"))\
             .order_by('-created_at')
         history_table = HistoryTable(club_transactions, prefix="history-")
         history_table.paginate(per_page=20, page=self.request.GET.get('history-page', 1))
@@ -422,7 +422,7 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
         club_member = Membership.objects.filter(
             club=club,
             date_end__gte=date.today() - timedelta(days=15),
-        ).filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view"))\
+        ).filter(PermissionBackend.filter_queryset(self.request, Membership, "view"))\
             .order_by("user__username", "-date_start")
         # Display only the most recent membership
         club_member = club_member.distinct("user__username")\
@@ -459,8 +459,8 @@ class ClubAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
         context = super().get_context_data(**kwargs)
         note = context['object'].note
         context["aliases"] = AliasTable(note.alias.filter(
-            PermissionBackend.filter_queryset(self.request.user, Alias, "view")).distinct().all())
-        context["can_create"] = PermissionBackend.check_perm(self.request.user, "note.add_alias", Alias(
+            PermissionBackend.filter_queryset(self.request, Alias, "view")).distinct().all())
+        context["can_create"] = PermissionBackend.check_perm(self.request, "note.add_alias", Alias(
             note=context["object"].note,
             name="",
             normalized_name="",
@@ -535,7 +535,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
         form = context['form']
 
         if "club_pk" in self.kwargs:  # We create a new membership.
-            club = Club.objects.filter(PermissionBackend.filter_queryset(self.request.user, Club, "view"))\
+            club = Club.objects.filter(PermissionBackend.filter_queryset(self.request, Club, "view"))\
                 .get(pk=self.kwargs["club_pk"], weiclub=None)
             form.fields['credit_amount'].initial = club.membership_fee_paid
             # Ensure that the user is member of the parent club and all its the family tree.
@@ -683,7 +683,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
         """
         # Get the club that is concerned by the membership
         if "club_pk" in self.kwargs:  # get from url of new membership
-            club = Club.objects.filter(PermissionBackend.filter_queryset(self.request.user, Club, "view")) \
+            club = Club.objects.filter(PermissionBackend.filter_queryset(self.request, Club, "view")) \
                 .get(pk=self.kwargs["club_pk"])
             user = form.instance.user
             old_membership = None
@@ -867,7 +867,7 @@ class ClubMembersListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableV
     def get_context_data(self, **kwargs):
         context = super().get_context_data(**kwargs)
         club = Club.objects.filter(
-            PermissionBackend.filter_queryset(self.request.user, Club, "view")
+            PermissionBackend.filter_queryset(self.request, Club, "view")
         ).get(pk=self.kwargs["pk"])
         context["club"] = club
 
diff --git a/apps/note/api/serializers.py b/apps/note/api/serializers.py
index f4905103aee94a8f02cfd9aaf7a370dd7bcec4fc..7dda6dba278e8547d23420efdec6435f0dae206e 100644
--- a/apps/note/api/serializers.py
+++ b/apps/note/api/serializers.py
@@ -8,7 +8,7 @@ from rest_framework.exceptions import ValidationError
 from rest_polymorphic.serializers import PolymorphicSerializer
 from member.api.serializers import MembershipSerializer
 from member.models import Membership
-from note_kfet.middlewares import get_current_authenticated_user
+from note_kfet.middlewares import get_current_request
 from permission.backends import PermissionBackend
 from rest_framework.utils import model_meta
 
@@ -126,7 +126,7 @@ class ConsumerSerializer(serializers.ModelSerializer):
         """
         # If the user has no right to see the note, then we only display the note identifier
         return NotePolymorphicSerializer().to_representation(obj.note)\
-            if PermissionBackend.check_perm(get_current_authenticated_user(), "note.view_note", obj.note)\
+            if PermissionBackend.check_perm(get_current_request(), "note.view_note", obj.note)\
             else dict(
             id=obj.note.id,
             name=str(obj.note),
@@ -142,7 +142,7 @@ class ConsumerSerializer(serializers.ModelSerializer):
     def get_membership(self, obj):
         if isinstance(obj.note, NoteUser):
             memberships = Membership.objects.filter(
-                PermissionBackend.filter_queryset(get_current_authenticated_user(), Membership, "view")).filter(
+                PermissionBackend.filter_queryset(get_current_request(), Membership, "view")).filter(
                 user=obj.note.user,
                 club=2,  # Kfet
             ).order_by("-date_start")
diff --git a/apps/note/api/views.py b/apps/note/api/views.py
index 594b2b9c54238f0adcf8685601da3d65a2cc2618..d4021210cfaa3eae0ad203646ede332c510dc66a 100644
--- a/apps/note/api/views.py
+++ b/apps/note/api/views.py
@@ -10,7 +10,6 @@ from rest_framework import viewsets
 from rest_framework.response import Response
 from rest_framework import status
 from api.viewsets import ReadProtectedModelViewSet, ReadOnlyProtectedModelViewSet
-from note_kfet.middlewares import get_current_session
 from permission.backends import PermissionBackend
 
 from .serializers import NotePolymorphicSerializer, AliasSerializer, ConsumerSerializer,\
@@ -40,12 +39,11 @@ class NotePolymorphicViewSet(ReadProtectedModelViewSet):
         Parse query and apply filters.
         :return: The filtered set of requested notes
         """
-        user = self.request.user
-        get_current_session().setdefault("permission_mask", 42)
-        queryset = self.queryset.filter(PermissionBackend.filter_queryset(user, Note, "view")
-                                        | PermissionBackend.filter_queryset(user, NoteUser, "view")
-                                        | PermissionBackend.filter_queryset(user, NoteClub, "view")
-                                        | PermissionBackend.filter_queryset(user, NoteSpecial, "view")).distinct()
+        queryset = self.queryset.filter(PermissionBackend.filter_queryset(self.request, Note, "view")
+                                        | PermissionBackend.filter_queryset(self.request, NoteUser, "view")
+                                        | PermissionBackend.filter_queryset(self.request, NoteClub, "view")
+                                        | PermissionBackend.filter_queryset(self.request, NoteSpecial, "view"))\
+            .distinct()
 
         alias = self.request.query_params.get("alias", ".*")
         queryset = queryset.filter(
@@ -67,7 +65,8 @@ class AliasViewSet(ReadProtectedModelViewSet):
     serializer_class = AliasSerializer
     filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter]
     search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ]
-    filterset_fields = ['note', 'note__noteuser__user', 'note__noteclub__club', 'note__polymorphic_ctype__model', ]
+    filterset_fields = ['name', 'normalized_name', 'note', 'note__noteuser__user',
+                        'note__noteclub__club', 'note__polymorphic_ctype__model', ]
     ordering_fields = ['name', 'normalized_name', ]
 
     def get_serializer_class(self):
@@ -118,7 +117,8 @@ class ConsumerViewSet(ReadOnlyProtectedModelViewSet):
     serializer_class = ConsumerSerializer
     filter_backends = [SearchFilter, OrderingFilter, DjangoFilterBackend]
     search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ]
-    filterset_fields = ['note', 'note__noteuser__user', 'note__noteclub__club', 'note__polymorphic_ctype__model', ]
+    filterset_fields = ['name', 'normalized_name', 'note', 'note__noteuser__user',
+                        'note__noteclub__club', 'note__polymorphic_ctype__model', ]
     ordering_fields = ['name', 'normalized_name', ]
 
     def get_queryset(self):
@@ -205,7 +205,5 @@ class TransactionViewSet(ReadProtectedModelViewSet):
     ordering_fields = ['created_at', 'amount', ]
 
     def get_queryset(self):
-        user = self.request.user
-        get_current_session().setdefault("permission_mask", 42)
-        return self.model.objects.filter(PermissionBackend.filter_queryset(user, self.model, "view"))\
+        return self.model.objects.filter(PermissionBackend.filter_queryset(self.request, self.model, "view"))\
             .order_by("created_at", "id")
diff --git a/apps/note/tables.py b/apps/note/tables.py
index e98a9b0b1fb7f36fec15c074bd3301ca6449ae24..518173c6ecc47e3ab588ec313ad1174fa5e564ab 100644
--- a/apps/note/tables.py
+++ b/apps/note/tables.py
@@ -7,7 +7,7 @@ import django_tables2 as tables
 from django.utils.html import format_html
 from django_tables2.utils import A
 from django.utils.translation import gettext_lazy as _
-from note_kfet.middlewares import get_current_authenticated_user
+from note_kfet.middlewares import get_current_request
 from permission.backends import PermissionBackend
 
 from .models.notes import Alias
@@ -88,16 +88,16 @@ class HistoryTable(tables.Table):
                 "class": lambda record:
                 str(record.valid).lower()
                 + (' validate' if record.source.is_active and record.destination.is_active and PermissionBackend
-                   .check_perm(get_current_authenticated_user(), "note.change_transaction_invalidity_reason", record)
+                   .check_perm(get_current_request(), "note.change_transaction_invalidity_reason", record)
                    else ''),
                 "data-toggle": "tooltip",
                 "title": lambda record: (_("Click to invalidate") if record.valid else _("Click to validate"))
-                if PermissionBackend.check_perm(get_current_authenticated_user(),
+                if PermissionBackend.check_perm(get_current_request(),
                                                 "note.change_transaction_invalidity_reason", record)
                 and record.source.is_active and record.destination.is_active else None,
                 "onclick": lambda record: 'de_validate(' + str(record.id) + ', ' + str(record.valid).lower()
                                           + ', "' + str(record.__class__.__name__) + '")'
-                if PermissionBackend.check_perm(get_current_authenticated_user(),
+                if PermissionBackend.check_perm(get_current_request(),
                                                 "note.change_transaction_invalidity_reason", record)
                 and record.source.is_active and record.destination.is_active else None,
                 "onmouseover": lambda record: '$("#invalidity_reason_'
@@ -126,7 +126,7 @@ class HistoryTable(tables.Table):
         When the validation status is hovered, an input field is displayed to let the user specify an invalidity reason
         """
         has_perm = PermissionBackend \
-            .check_perm(get_current_authenticated_user(), "note.change_transaction_invalidity_reason", record)
+            .check_perm(get_current_request(), "note.change_transaction_invalidity_reason", record)
 
         val = "✔" if value else "✖"
 
@@ -165,7 +165,7 @@ class AliasTable(tables.Table):
                                        extra_context={"delete_trans": _('delete')},
                                        attrs={'td': {'class': lambda record: 'col-sm-1' + (
                                            ' d-none' if not PermissionBackend.check_perm(
-                                               get_current_authenticated_user(), "note.delete_alias",
+                                               get_current_request(), "note.delete_alias",
                                                record) else '')}}, verbose_name=_("Delete"), )
 
 
diff --git a/apps/note/views.py b/apps/note/views.py
index 73e5a0842f5f26a53d8f67818c8e9057a79a740b..279eadc66c2ed97dfb1fdeeabb8e5d4b2758c3a8 100644
--- a/apps/note/views.py
+++ b/apps/note/views.py
@@ -38,7 +38,7 @@ class TransactionCreateView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTabl
     def get_queryset(self, **kwargs):
         # retrieves only Transaction that user has the right to see.
         return Transaction.objects.filter(
-            PermissionBackend.filter_queryset(self.request.user, Transaction, "view")
+            PermissionBackend.filter_queryset(self.request, Transaction, "view")
         ).order_by("-created_at").all()[:20]
 
     def get_context_data(self, **kwargs):
@@ -47,16 +47,16 @@ class TransactionCreateView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTabl
         context['polymorphic_ctype'] = ContentType.objects.get_for_model(Transaction).pk
         context['special_polymorphic_ctype'] = ContentType.objects.get_for_model(SpecialTransaction).pk
         context['special_types'] = NoteSpecial.objects\
-            .filter(PermissionBackend.filter_queryset(self.request.user, NoteSpecial, "view"))\
+            .filter(PermissionBackend.filter_queryset(self.request, NoteSpecial, "view"))\
             .order_by("special_type").all()
 
         # Add a shortcut for entry page for open activities
         if "activity" in settings.INSTALLED_APPS:
             from activity.models import Activity
             activities_open = Activity.objects.filter(open=True).filter(
-                PermissionBackend.filter_queryset(self.request.user, Activity, "view")).distinct().all()
+                PermissionBackend.filter_queryset(self.request, Activity, "view")).distinct().all()
             context["activities_open"] = [a for a in activities_open
-                                          if PermissionBackend.check_perm(self.request.user,
+                                          if PermissionBackend.check_perm(self.request,
                                                                           "activity.add_entry",
                                                                           Entry(activity=a,
                                                                                 note=self.request.user.note, ))]
@@ -159,7 +159,7 @@ class ConsoView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
             return self.handle_no_permission()
 
         templates = TransactionTemplate.objects.filter(
-            PermissionBackend().filter_queryset(self.request.user, TransactionTemplate, "view")
+            PermissionBackend().filter_queryset(self.request, TransactionTemplate, "view")
         )
         if not templates.exists():
             raise PermissionDenied(_("You can't see any button."))
@@ -170,7 +170,7 @@ class ConsoView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
         restrict to the transaction history the user can see.
         """
         return Transaction.objects.filter(
-            PermissionBackend.filter_queryset(self.request.user, Transaction, "view")
+            PermissionBackend.filter_queryset(self.request, Transaction, "view")
         ).order_by("-created_at").all()[:20]
 
     def get_context_data(self, **kwargs):
@@ -180,13 +180,13 @@ class ConsoView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
         # for each category, find which transaction templates the user can see.
         for category in categories:
             category.templates_filtered = category.templates.filter(
-                PermissionBackend().filter_queryset(self.request.user, TransactionTemplate, "view")
+                PermissionBackend().filter_queryset(self.request, TransactionTemplate, "view")
             ).filter(display=True).order_by('name').all()
 
         context['categories'] = [cat for cat in categories if cat.templates_filtered]
         # some transactiontemplate are put forward to find them easily
         context['highlighted'] = TransactionTemplate.objects.filter(highlighted=True).filter(
-            PermissionBackend().filter_queryset(self.request.user, TransactionTemplate, "view")
+            PermissionBackend().filter_queryset(self.request, TransactionTemplate, "view")
         ).order_by('name').all()
         context['polymorphic_ctype'] = ContentType.objects.get_for_model(RecurrentTransaction).pk
 
@@ -209,7 +209,7 @@ class TransactionSearchView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView
         data = form.cleaned_data if form.is_valid() else {}
 
         transactions = Transaction.objects.annotate(total_amount=F("quantity") * F("amount")).filter(
-            PermissionBackend.filter_queryset(self.request.user, Transaction, "view"))\
+            PermissionBackend.filter_queryset(self.request, Transaction, "view"))\
             .filter(Q(source=self.object) | Q(destination=self.object)).order_by('-created_at')
 
         if "source" in data and data["source"]:
diff --git a/apps/permission/backends.py b/apps/permission/backends.py
index b43340f07ce3477133cb3721a8d44d04fbd9ac30..af071455e3904ac6762de2c9513cea0be301ad46 100644
--- a/apps/permission/backends.py
+++ b/apps/permission/backends.py
@@ -4,12 +4,12 @@
 from datetime import date
 
 from django.contrib.auth.backends import ModelBackend
-from django.contrib.auth.models import User, AnonymousUser
+from django.contrib.auth.models import User
 from django.contrib.contenttypes.models import ContentType
 from django.db.models import Q, F
 from django.utils import timezone
 from note.models import Note, NoteUser, NoteClub, NoteSpecial
-from note_kfet.middlewares import get_current_session
+from note_kfet.middlewares import get_current_request
 from member.models import Membership, Club
 
 from .decorators import memoize
@@ -26,14 +26,31 @@ class PermissionBackend(ModelBackend):
 
     @staticmethod
     @memoize
-    def get_raw_permissions(user, t):
+    def get_raw_permissions(request, t):
         """
         Query permissions of a certain type for a user, then memoize it.
-        :param user: The owner of the permissions
+        :param request: The current request
         :param t: The type of the permissions: view, change, add or delete
         :return: The queryset of the permissions of the user (memoized) grouped by clubs
         """
-        if isinstance(user, AnonymousUser):
+        if hasattr(request, 'auth') and request.auth is not None and hasattr(request.auth, 'scope'):
+            # OAuth2 Authentication
+            user = request.auth.user
+
+            def permission_filter(membership_obj):
+                query = Q(pk=-1)
+                for scope in request.auth.scope.split(' '):
+                    permission_id, club_id = scope.split('_')
+                    if int(club_id) == membership_obj.club_id:
+                        query |= Q(pk=permission_id)
+                return query
+        else:
+            user = request.user
+
+            def permission_filter(membership_obj):
+                return Q(mask__rank__lte=request.session.get("permission_mask", 42))
+
+        if user.is_anonymous:
             # Unauthenticated users have no permissions
             return Permission.objects.none()
 
@@ -43,7 +60,7 @@ class PermissionBackend(ModelBackend):
 
         for membership in memberships:
             for role in membership.roles.all():
-                for perm in role.permissions.filter(type=t, mask__rank__lte=get_current_session().get("permission_mask", -1)).all():
+                for perm in role.permissions.filter(permission_filter(membership), type=t).all():
                     if not perm.permanent:
                         if membership.date_start > date.today() or membership.date_end < date.today():
                             continue
@@ -52,16 +69,22 @@ class PermissionBackend(ModelBackend):
         return perms
 
     @staticmethod
-    def permissions(user, model, type):
+    def permissions(request, model, type):
         """
         List all permissions of the given user that applies to a given model and a give type
-        :param user: The owner of the permissions
+        :param request: The current request
         :param model: The model that the permissions shoud apply
         :param type: The type of the permissions: view, change, add or delete
         :return: A generator of the requested permissions
         """
 
-        for permission in PermissionBackend.get_raw_permissions(user, type):
+        if hasattr(request, 'auth') and request.auth is not None and hasattr(request.auth, 'scope'):
+            # OAuth2 Authentication
+            user = request.auth.user
+        else:
+            user = request.user
+
+        for permission in PermissionBackend.get_raw_permissions(request, type):
             if not isinstance(model.model_class()(), permission.model.model_class()) or not permission.membership:
                 continue
 
@@ -88,20 +111,26 @@ class PermissionBackend(ModelBackend):
 
     @staticmethod
     @memoize
-    def filter_queryset(user, model, t, field=None):
+    def filter_queryset(request, model, t, field=None):
         """
         Filter a queryset by considering the permissions of a given user.
-        :param user: The owner of the permissions that are fetched
+        :param request: The current request
         :param model: The concerned model of the queryset
         :param t: The type of modification (view, add, change, delete)
         :param field: The field of the model to test, if concerned
         :return: A query that corresponds to the filter to give to a queryset
         """
-        if user is None or isinstance(user, AnonymousUser):
+        if hasattr(request, 'auth') and request.auth is not None and hasattr(request.auth, 'scope'):
+            # OAuth2 Authentication
+            user = request.auth.user
+        else:
+            user = request.user
+
+        if user is None or user.is_anonymous:
             # Anonymous users can't do anything
             return Q(pk=-1)
 
-        if user.is_superuser and get_current_session().get("permission_mask", -1) >= 42:
+        if user.is_superuser and request.session.get("permission_mask", -1) >= 42:
             # Superusers have all rights
             return Q()
 
@@ -110,7 +139,7 @@ class PermissionBackend(ModelBackend):
 
         # Never satisfied
         query = Q(pk=-1)
-        perms = PermissionBackend.permissions(user, model, t)
+        perms = PermissionBackend.permissions(request, model, t)
         for perm in perms:
             if perm.field and field != perm.field:
                 continue
@@ -122,7 +151,7 @@ class PermissionBackend(ModelBackend):
 
     @staticmethod
     @memoize
-    def check_perm(user_obj, perm, obj=None):
+    def check_perm(request, perm, obj=None):
         """
         Check is the given user has the permission over a given object.
         The result is then memoized.
@@ -130,10 +159,15 @@ class PermissionBackend(ModelBackend):
         primary key, the result is not memoized. Moreover, the right could change
         (e.g. for a transaction, the balance of the user could change)
         """
-        if user_obj is None or isinstance(user_obj, AnonymousUser):
-            return False
+        user_obj = request.user
+        sess = request.session
 
-        sess = get_current_session()
+        if hasattr(request, 'auth') and request.auth is not None and hasattr(request.auth, 'scope'):
+            # OAuth2 Authentication
+            user_obj = request.auth.user
+
+        if user_obj is None or user_obj.is_anonymous:
+            return False
 
         if user_obj.is_superuser and sess.get("permission_mask", -1) >= 42:
             return True
@@ -147,16 +181,19 @@ class PermissionBackend(ModelBackend):
 
         ct = ContentType.objects.get_for_model(obj)
         if any(permission.applies(obj, perm_type, perm_field)
-               for permission in PermissionBackend.permissions(user_obj, ct, perm_type)):
+               for permission in PermissionBackend.permissions(request, ct, perm_type)):
             return True
         return False
 
     def has_perm(self, user_obj, perm, obj=None):
-        return PermissionBackend.check_perm(user_obj, perm, obj)
+        # Warning: this does not check that user_obj has the permission,
+        # but if the current request has the permission.
+        # This function is implemented for backward compatibility, and should not be used.
+        return PermissionBackend.check_perm(get_current_request(), perm, obj)
 
     def has_module_perms(self, user_obj, app_label):
         return False
 
     def get_all_permissions(self, user_obj, obj=None):
         ct = ContentType.objects.get_for_model(obj)
-        return list(self.permissions(user_obj, ct, "view"))
+        return list(self.permissions(get_current_request(), ct, "view"))
diff --git a/apps/permission/decorators.py b/apps/permission/decorators.py
index 7f5b48b0936b04c54ef2db890a9c39ea7ad8f1ad..0e79df909e608f983f1c194fc2c436ee3bd3bdca 100644
--- a/apps/permission/decorators.py
+++ b/apps/permission/decorators.py
@@ -5,7 +5,7 @@ from functools import lru_cache
 from time import time
 
 from django.contrib.sessions.models import Session
-from note_kfet.middlewares import get_current_session
+from note_kfet.middlewares import get_current_request
 
 
 def memoize(f):
@@ -48,11 +48,11 @@ def memoize(f):
             last_collect = time()
 
         # If there is no session, then we don't memoize anything.
-        sess = get_current_session()
-        if sess is None or sess.session_key is None:
+        request = get_current_request()
+        if request is None or request.session is None or request.session.session_key is None:
             return f(*args, **kwargs)
 
-        sess_key = sess.session_key
+        sess_key = request.session.session_key
         if sess_key not in sess_funs:
             # lru_cache makes the job of memoization
             # We store only the 512 latest data per session. It has to be enough.
diff --git a/apps/permission/permissions.py b/apps/permission/permissions.py
index 9a0c1e12e334a36a831aac0b2a5d349ab1da4cce..1a5f51e598c2f01600e0657955477ce1bf47e7c2 100644
--- a/apps/permission/permissions.py
+++ b/apps/permission/permissions.py
@@ -45,7 +45,7 @@ class StrongDjangoObjectPermissions(DjangoObjectPermissions):
 
         perms = self.get_required_object_permissions(request.method, model_cls)
         # if not user.has_perms(perms, obj):
-        if not all(PermissionBackend.check_perm(user, perm, obj) for perm in perms):
+        if not all(PermissionBackend.check_perm(request, perm, obj) for perm in perms):
             # If the user does not have permissions we need to determine if
             # they have read permissions to see 403, or not, and simply see
             # a 404 response.
diff --git a/apps/permission/scopes.py b/apps/permission/scopes.py
new file mode 100644
index 0000000000000000000000000000000000000000..bf74cb817ac264d5613d1496c29acddb8916b8c0
--- /dev/null
+++ b/apps/permission/scopes.py
@@ -0,0 +1,34 @@
+# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from oauth2_provider.scopes import BaseScopes
+from member.models import Club
+from note_kfet.middlewares import get_current_request
+
+from .backends import PermissionBackend
+from .models import Permission
+
+
+class PermissionScopes(BaseScopes):
+    """
+    An OAuth2 scope is defined by a permission object and a club.
+    A token will have a subset of permissions from the owner of the application,
+    and can be useful to make queries through the API with limited privileges.
+    """
+
+    def get_all_scopes(self):
+        return {f"{p.id}_{club.id}": f"{p.description} (club {club.name})"
+                for p in Permission.objects.all() for club in Club.objects.all()}
+
+    def get_available_scopes(self, application=None, request=None, *args, **kwargs):
+        if not application:
+            return []
+        return [f"{p.id}_{p.membership.club.id}"
+                for t in Permission.PERMISSION_TYPES
+                for p in PermissionBackend.get_raw_permissions(get_current_request(), t[0])]
+
+    def get_default_scopes(self, application=None, request=None, *args, **kwargs):
+        if not application:
+            return []
+        return [f"{p.id}_{p.membership.club.id}"
+                for p in PermissionBackend.get_raw_permissions(get_current_request(), 'view')]
diff --git a/apps/permission/signals.py b/apps/permission/signals.py
index b419ce090d5b4adf646301f47b0c761e1641bf3c..78d0b8f98a52c9017c85fc176cc73d3beb614718 100644
--- a/apps/permission/signals.py
+++ b/apps/permission/signals.py
@@ -3,7 +3,7 @@
 
 from django.core.exceptions import PermissionDenied
 from django.utils.translation import gettext_lazy as _
-from note_kfet.middlewares import get_current_authenticated_user
+from note_kfet.middlewares import get_current_request
 from permission.backends import PermissionBackend
 
 
@@ -16,6 +16,9 @@ EXCLUDED = [
     'contenttypes.contenttype',
     'logs.changelog',
     'migrations.migration',
+    'oauth2_provider.accesstoken',
+    'oauth2_provider.grant',
+    'oauth2_provider.refreshtoken',
     'sessions.session',
 ]
 
@@ -31,8 +34,8 @@ def pre_save_object(sender, instance, **kwargs):
     if hasattr(instance, "_force_save") or hasattr(instance, "_no_signal"):
         return
 
-    user = get_current_authenticated_user()
-    if user is None:
+    request = get_current_request()
+    if request is None:
         # Action performed on shell is always granted
         return
 
@@ -45,7 +48,7 @@ def pre_save_object(sender, instance, **kwargs):
         # We check if the user can change the model
 
         # If the user has all right on a model, then OK
-        if PermissionBackend.check_perm(user, app_label + ".change_" + model_name, instance):
+        if PermissionBackend.check_perm(request, app_label + ".change_" + model_name, instance):
             return
 
         # In the other case, we check if he/she has the right to change one field
@@ -58,7 +61,8 @@ def pre_save_object(sender, instance, **kwargs):
             # If the field wasn't modified, no need to check the permissions
             if old_value == new_value:
                 continue
-            if not PermissionBackend.check_perm(user, app_label + ".change_" + model_name + "_" + field_name, instance):
+            if not PermissionBackend.check_perm(request, app_label + ".change_" + model_name + "_" + field_name,
+                                                instance):
                 raise PermissionDenied(
                     _("You don't have the permission to change the field {field} on this instance of model"
                       " {app_label}.{model_name}.")
@@ -66,7 +70,7 @@ def pre_save_object(sender, instance, **kwargs):
                 )
     else:
         # We check if the user has right to add the object
-        has_perm = PermissionBackend.check_perm(user, app_label + ".add_" + model_name, instance)
+        has_perm = PermissionBackend.check_perm(request, app_label + ".add_" + model_name, instance)
 
         if not has_perm:
             raise PermissionDenied(
@@ -87,8 +91,8 @@ def pre_delete_object(instance, **kwargs):
         # Don't check permissions on force-deleted objects
         return
 
-    user = get_current_authenticated_user()
-    if user is None:
+    request = get_current_request()
+    if request is None:
         # Action performed on shell is always granted
         return
 
@@ -97,7 +101,7 @@ def pre_delete_object(instance, **kwargs):
     model_name = model_name_full[1]
 
     # We check if the user has rights to delete the object
-    if not PermissionBackend.check_perm(user, app_label + ".delete_" + model_name, instance):
+    if not PermissionBackend.check_perm(request, app_label + ".delete_" + model_name, instance):
         raise PermissionDenied(
             _("You don't have the permission to delete this instance of model {app_label}.{model_name}.")
             .format(app_label=app_label, model_name=model_name))
diff --git a/apps/permission/tables.py b/apps/permission/tables.py
index 9e82fa8e58a46e54dc1a635c9d62c7056f6eed3b..eaec51388ee5dce14c32c2630371c19a8070db6d 100644
--- a/apps/permission/tables.py
+++ b/apps/permission/tables.py
@@ -8,7 +8,7 @@ from django.urls import reverse_lazy
 from django.utils.html import format_html
 from django_tables2 import A
 from member.models import Membership
-from note_kfet.middlewares import get_current_authenticated_user
+from note_kfet.middlewares import get_current_request
 from permission.backends import PermissionBackend
 
 
@@ -20,7 +20,7 @@ class RightsTable(tables.Table):
     def render_user(self, value):
         # If the user has the right, link the displayed user with the page of its detail.
         s = value.username
-        if PermissionBackend.check_perm(get_current_authenticated_user(), "auth.view_user", value):
+        if PermissionBackend.check_perm(get_current_request(), "auth.view_user", value):
             s = format_html("<a href={url}>{name}</a>",
                             url=reverse_lazy('member:user_detail', kwargs={"pk": value.pk}), name=s)
         return s
@@ -28,7 +28,7 @@ class RightsTable(tables.Table):
     def render_club(self, value):
         # If the user has the right, link the displayed user with the page of its detail.
         s = value.name
-        if PermissionBackend.check_perm(get_current_authenticated_user(), "member.view_club", value):
+        if PermissionBackend.check_perm(get_current_request(), "member.view_club", value):
             s = format_html("<a href={url}>{name}</a>",
                             url=reverse_lazy('member:club_detail', kwargs={"pk": value.pk}), name=s)
 
@@ -42,7 +42,7 @@ class RightsTable(tables.Table):
                                      | Q(name="Bureau de club"))
                                      & Q(weirole__isnull=True))).all()
         s = ", ".join(str(role) for role in roles)
-        if PermissionBackend.check_perm(get_current_authenticated_user(), "member.change_membership_roles", record):
+        if PermissionBackend.check_perm(get_current_request(), "member.change_membership_roles", record):
             s = format_html("<a href='" + str(reverse_lazy("member:club_manage_roles", kwargs={"pk": record.pk}))
                             + "'>" + s + "</a>")
         return s
diff --git a/apps/permission/templates/permission/scopes.html b/apps/permission/templates/permission/scopes.html
new file mode 100644
index 0000000000000000000000000000000000000000..31a4395e6d48d78affda0c639b73a3e36eda7a73
--- /dev/null
+++ b/apps/permission/templates/permission/scopes.html
@@ -0,0 +1,74 @@
+{% extends "base.html" %}
+
+{% load i18n %}
+
+{% block content %}
+    <div class="card">
+        <div class="card-header text-center">
+            <h2>{% trans "Available scopes" %}</h2>
+        </div>
+        <div class="card-body">
+            <div class="accordion" id="accordionApps">
+                {% for app, app_scopes in scopes.items %}
+                    <div class="card">
+                        <div class="card-header" id="app-{{ app.name.lower }}-title">
+                            <a class="text-decoration-none collapsed" href="#" data-toggle="collapse"
+                               data-target="#app-{{ app.name.lower }}" aria-expanded="false"
+                               aria-controls="app-{{ app.name.lower }}">
+                                {{ app.name }}
+                            </a>
+                        </div>
+                        <div class="collapse" id="app-{{ app.name.lower }}" aria-labelledby="app-{{ app.name.lower }}" data-target="#accordionApps">
+                            <div class="card-body">
+                                {% for scope_id, scope_desc in app_scopes.items %}
+                                    <div class="form-group">
+                                        <label class="form-check-label" for="scope-{{ app.name.lower }}-{{ scope_id }}">
+                                            <input type="checkbox" id="scope-{{ app.name.lower }}-{{ scope_id }}"
+                                                   name="scope-{{ app.name.lower }}" class="checkboxinput form-check-input" value="{{ scope_id }}">
+                                            {{ scope_desc }}
+                                        </label>
+                                    </div>
+                                {% endfor %}
+                                <p id="url-{{ app.name.lower }}">
+                                    <a href="{% url 'oauth2_provider:authorize' %}?client_id={{ app.client_id }}&response_type=code" target="_blank">
+                                        {{ request.scheme }}://{{ request.get_host }}{% url 'oauth2_provider:authorize' %}?client_id={{ app.client_id }}&response_type=code
+                                    </a>
+                                </p>
+                            </div>
+                        </div>
+                    </div>
+                {% empty %}
+                    <p>
+                        {% trans "No applications defined" %}.
+                        <a href="{% url 'oauth2_provider:register' %}">{% trans "Click here" %}</a> {% trans "if you want to register a new one" %}.
+                    </p>
+                {% endfor %}
+            </div>
+        </div>
+    </div>
+{% endblock %}
+
+{% block extrajavascript %}
+    <script>
+        {% for app in scopes.keys %}
+            let elements = document.getElementsByName("scope-{{ app.name.lower }}");
+            for (let element of elements) {
+                element.onchange = function (event) {
+                    let scope = ""
+                    for (let element of elements) {
+                        if (element.checked) {
+                            scope += element.value + " "
+                        }
+                    }
+
+                    scope = scope.substr(0, scope.length - 1)
+
+                    document.getElementById("url-{{ app.name.lower }}").innerHTML = 'Scopes : ' + scope
+                        + '<br><a href="{% url 'oauth2_provider:authorize' %}?client_id={{ app.client_id }}&response_type=code&scope='+ scope.replaceAll(' ', '%20')
+                        + '" target="_blank">{{ request.scheme }}://{{ request.get_host }}{% url 'oauth2_provider:authorize' %}?client_id={{ app.client_id }}&response_type=code&scope='
+                        + scope.replaceAll(' ', '%20') + '</a>'
+                }
+            }
+        {% endfor %}
+    </script>
+{% endblock %}
diff --git a/apps/permission/templatetags/perms.py b/apps/permission/templatetags/perms.py
index 2fb376d4e36f8e36d4051a8060bf6bcd344d9d46..17f336f4b4a4e342aef55f23bc65f3d1b8598d5b 100644
--- a/apps/permission/templatetags/perms.py
+++ b/apps/permission/templatetags/perms.py
@@ -1,12 +1,12 @@
 # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
 # SPDX-License-Identifier: GPL-3.0-or-later
 
-from django.contrib.auth.models import AnonymousUser
 from django.contrib.contenttypes.models import ContentType
 from django.template.defaultfilters import stringfilter
 from django import template
-from note_kfet.middlewares import get_current_authenticated_user, get_current_session
-from permission.backends import PermissionBackend
+from note_kfet.middlewares import get_current_request
+
+from ..backends import PermissionBackend
 
 
 @stringfilter
@@ -14,9 +14,10 @@ def not_empty_model_list(model_name):
     """
     Return True if and only if the current user has right to see any object of the given model.
     """
-    user = get_current_authenticated_user()
-    session = get_current_session()
-    if user is None or isinstance(user, AnonymousUser):
+    request = get_current_request()
+    user = request.user
+    session = request.session
+    if user is None or not user.is_authenticated:
         return False
     elif user.is_superuser and session.get("permission_mask", -1) >= 42:
         return True
@@ -29,11 +30,12 @@ def model_list(model_name, t="view", fetch=True):
     """
     Return the queryset of all visible instances of the given model.
     """
-    user = get_current_authenticated_user()
+    request = get_current_request()
+    user = request.user
     spl = model_name.split(".")
     ct = ContentType.objects.get(app_label=spl[0], model=spl[1])
-    qs = ct.model_class().objects.filter(PermissionBackend.filter_queryset(user, ct, t))
-    if user is None or isinstance(user, AnonymousUser):
+    qs = ct.model_class().objects.filter(PermissionBackend.filter_queryset(request, ct, t))
+    if user is None or not user.is_authenticated:
         return qs.none()
     if fetch:
         qs = qs.all()
@@ -49,7 +51,7 @@ def model_list_length(model_name, t="view"):
 
 
 def has_perm(perm, obj):
-    return PermissionBackend.check_perm(get_current_authenticated_user(), perm, obj)
+    return PermissionBackend.check_perm(get_current_request(), perm, obj)
 
 
 register = template.Library()
diff --git a/apps/permission/tests/test_oauth2.py b/apps/permission/tests/test_oauth2.py
new file mode 100644
index 0000000000000000000000000000000000000000..4593b35a1869467895b5a2b7392d63df935bf459
--- /dev/null
+++ b/apps/permission/tests/test_oauth2.py
@@ -0,0 +1,94 @@
+# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from datetime import timedelta
+
+from django.contrib.auth.models import User
+from django.test import TestCase
+from django.urls import reverse
+from django.utils import timezone
+from django.utils.crypto import get_random_string
+from member.models import Membership, Club
+from note.models import NoteUser
+from oauth2_provider.models import Application, AccessToken
+
+from ..models import Role, Permission
+
+
+class OAuth2TestCase(TestCase):
+    fixtures = ('initial', )
+
+    def setUp(self):
+        self.user = User.objects.create(
+            username="toto",
+        )
+        self.application = Application.objects.create(
+            name="Test",
+            client_type=Application.CLIENT_PUBLIC,
+            authorization_grant_type=Application.GRANT_AUTHORIZATION_CODE,
+            user=self.user,
+        )
+
+    def test_oauth2_access(self):
+        """
+        Create a simple OAuth2 access token that only has the right to see data of the current user
+        and check that this token has required access, and nothing more.
+        """
+
+        bde = Club.objects.get(name="BDE")
+        view_user_perm = Permission.objects.get(pk=1)  # View own user detail
+
+        # Create access token that has access to our own user detail
+        token = AccessToken.objects.create(
+            user=self.user,
+            application=self.application,
+            scope=f"{view_user_perm.pk}_{bde.pk}",
+            token=get_random_string(64),
+            expires=timezone.now() + timedelta(days=365),
+        )
+
+        # No access without token
+        resp = self.client.get(f'/api/user/{self.user.pk}/')
+        self.assertEqual(resp.status_code, 403)
+
+        # Valid token but user has no membership, so the query is not returning the user object
+        resp = self.client.get(f'/api/user/{self.user.pk}/', **{'Authorization': f'Bearer {token.token}'})
+        self.assertEqual(resp.status_code, 404)
+
+        # Create membership to validate permissions
+        NoteUser.objects.create(user=self.user)
+        membership = Membership.objects.create(user=self.user, club_id=bde.pk)
+        membership.roles.add(Role.objects.get(name="Adhérent BDE"))
+        membership.save()
+
+        # User is now a member and can now see its own user detail
+        resp = self.client.get(f'/api/user/{self.user.pk}/', **{'Authorization': f'Bearer {token.token}'})
+        self.assertEqual(resp.status_code, 200)
+
+        # Token is not granted to see profile detail
+        resp = self.client.get(f'/api/members/profile/{self.user.profile.pk}/',
+                               **{'Authorization': f'Bearer {token.token}'})
+        self.assertEqual(resp.status_code, 404)
+
+    def test_scopes(self):
+        """
+        Ensure that the scopes page is loading.
+        """
+        self.client.force_login(self.user)
+
+        resp = self.client.get(reverse('permission:scopes'))
+        self.assertEqual(resp.status_code, 200)
+        self.assertIn(self.application, resp.context['scopes'])
+        self.assertNotIn('1_1', resp.context['scopes'][self.application])  # The user has not this permission
+
+        # Create membership to validate permissions
+        bde = Club.objects.get(name="BDE")
+        NoteUser.objects.create(user=self.user)
+        membership = Membership.objects.create(user=self.user, club_id=bde.pk)
+        membership.roles.add(Role.objects.get(name="Adhérent BDE"))
+        membership.save()
+
+        resp = self.client.get(reverse('permission:scopes'))
+        self.assertEqual(resp.status_code, 200)
+        self.assertIn(self.application, resp.context['scopes'])
+        self.assertIn('1_1', resp.context['scopes'][self.application])  # Now the user has this permission
diff --git a/apps/permission/urls.py b/apps/permission/urls.py
index 0894ecf007122b25d59b7b632cb205ce7d4a0465..43eec1ef7022042338c2b923fdf2849ccbf6213b 100644
--- a/apps/permission/urls.py
+++ b/apps/permission/urls.py
@@ -1,10 +1,17 @@
 # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
 # SPDX-License-Identifier: GPL-3.0-or-later
 
+from django.conf import settings
 from django.urls import path
-from permission.views import RightsView
+
+from .views import RightsView, ScopesView
 
 app_name = 'permission'
 urlpatterns = [
-    path('rights', RightsView.as_view(), name="rights"),
+    path('rights/', RightsView.as_view(), name="rights"),
 ]
+
+if "oauth2_provider" in settings.INSTALLED_APPS:
+    urlpatterns += [
+        path('scopes/', ScopesView.as_view(), name="scopes"),
+    ]
diff --git a/apps/permission/views.py b/apps/permission/views.py
index c48215a02702fbbbdaffb9237fd5b8c549a3e0cb..8f498478d280073a5d8b3dc16b8e8c6362d67df1 100644
--- a/apps/permission/views.py
+++ b/apps/permission/views.py
@@ -1,6 +1,6 @@
 # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
 # SPDX-License-Identifier: GPL-3.0-or-later
-
+from collections import OrderedDict
 from datetime import date
 
 from django.contrib.auth.mixins import LoginRequiredMixin
@@ -28,7 +28,7 @@ class ProtectQuerysetMixin:
     """
     def get_queryset(self, filter_permissions=True, **kwargs):
         qs = super().get_queryset(**kwargs)
-        return qs.filter(PermissionBackend.filter_queryset(self.request.user, qs.model, "view")).distinct()\
+        return qs.filter(PermissionBackend.filter_queryset(self.request, qs.model, "view")).distinct()\
             if filter_permissions else qs
 
     def get_object(self, queryset=None):
@@ -53,7 +53,7 @@ class ProtectQuerysetMixin:
         # We could also delete the field, but some views might be affected.
         meta = form.instance._meta
         for key in form.base_fields:
-            if not PermissionBackend.check_perm(self.request.user,
+            if not PermissionBackend.check_perm(self.request,
                                                 f"{meta.app_label}.change_{meta.model_name}_" + key, self.object):
                 form.fields[key].widget = HiddenInput()
 
@@ -101,7 +101,7 @@ class ProtectedCreateView(LoginRequiredMixin, CreateView):
         # noinspection PyProtectedMember
         app_label, model_name = model_class._meta.app_label, model_class._meta.model_name.lower()
         perm = app_label + ".add_" + model_name
-        if not PermissionBackend.check_perm(request.user, perm, self.get_sample_object()):
+        if not PermissionBackend.check_perm(request, perm, self.get_sample_object()):
             raise PermissionDenied(_("You don't have the permission to add an instance of model "
                                      "{app_label}.{model_name}.").format(app_label=app_label, model_name=model_name))
         return super().dispatch(request, *args, **kwargs)
@@ -143,3 +143,26 @@ class RightsView(TemplateView):
                                                    prefix="superusers-")
 
         return context
+
+
+class ScopesView(LoginRequiredMixin, TemplateView):
+    template_name = "permission/scopes.html"
+
+    def get_context_data(self, **kwargs):
+        context = super().get_context_data(**kwargs)
+
+        from oauth2_provider.models import Application
+        from .scopes import PermissionScopes
+
+        scopes = PermissionScopes()
+        context["scopes"] = {}
+        all_scopes = scopes.get_all_scopes()
+        for app in Application.objects.filter(user=self.request.user).all():
+            available_scopes = scopes.get_available_scopes(app)
+            context["scopes"][app] = OrderedDict()
+            items = [(k, v) for (k, v) in all_scopes.items() if k in available_scopes]
+            items.sort(key=lambda x: (int(x[0].split("_")[1]), int(x[0].split("_")[0])))
+            for k, v in items:
+                context["scopes"][app][k] = v
+
+        return context
diff --git a/apps/registration/views.py b/apps/registration/views.py
index a680996d900ff06ede3674416b04f8ccc94c03d1..9b385324d1165c9a69491d65f46dddd148d4fd1e 100644
--- a/apps/registration/views.py
+++ b/apps/registration/views.py
@@ -66,9 +66,11 @@ class UserCreateView(CreateView):
         profile_form.instance.user = user
         profile = profile_form.save(commit=False)
         user.profile = profile
+        user._force_save = True
         user.save()
         user.refresh_from_db()
         profile.user = user
+        profile._force_save = True
         profile.save()
 
         user.profile.send_email_validation_link()
@@ -110,7 +112,9 @@ class UserValidateView(TemplateView):
             self.validlink = True
             user.is_active = user.profile.registration_valid or user.is_superuser
             user.profile.email_confirmed = True
+            user._force_save = True
             user.save()
+            user.profile._force_save = True
             user.profile.save()
         return self.render_to_response(self.get_context_data(), status=200 if self.validlink else 400)
 
@@ -384,7 +388,7 @@ class FutureUserInvalidateView(ProtectQuerysetMixin, LoginRequiredMixin, View):
         Delete the pre-registered user which id is given in the URL.
         """
         user = User.objects.filter(profile__registration_valid=False)\
-            .filter(PermissionBackend.filter_queryset(request.user, User, "change", "is_valid"))\
+            .filter(PermissionBackend.filter_queryset(request, User, "change", "is_valid"))\
             .get(pk=self.kwargs["pk"])
         # Delete associated soge credits before
         SogeCredit.objects.filter(user=user).delete()
diff --git a/apps/treasury/views.py b/apps/treasury/views.py
index 47544cc1ae8eb6c0623b9bd5bfc5fb0685fef43d..b9a7fe7c5cbb8e96e559caf7addde49e9a0ad652 100644
--- a/apps/treasury/views.py
+++ b/apps/treasury/views.py
@@ -107,7 +107,7 @@ class InvoiceListView(LoginRequiredMixin, SingleTableView):
             name="",
             address="",
         )
-        if not PermissionBackend.check_perm(self.request.user, "treasury.add_invoice", sample_invoice):
+        if not PermissionBackend.check_perm(self.request, "treasury.add_invoice", sample_invoice):
             raise PermissionDenied(_("You are not able to see the treasury interface."))
         return super().dispatch(request, *args, **kwargs)
 
@@ -194,7 +194,7 @@ class InvoiceRenderView(LoginRequiredMixin, View):
 
     def get(self, request, **kwargs):
         pk = kwargs["pk"]
-        invoice = Invoice.objects.filter(PermissionBackend.filter_queryset(request.user, Invoice, "view")).get(pk=pk)
+        invoice = Invoice.objects.filter(PermissionBackend.filter_queryset(request, Invoice, "view")).get(pk=pk)
         tex = invoice.tex
 
         try:
@@ -259,7 +259,7 @@ class RemittanceCreateView(ProtectQuerysetMixin, ProtectedCreateView):
 
         context["table"] = RemittanceTable(
             data=Remittance.objects.filter(
-                PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all())
+                PermissionBackend.filter_queryset(self.request, Remittance, "view")).all())
         context["special_transactions"] = SpecialTransactionTable(data=SpecialTransaction.objects.none())
 
         return context
@@ -281,7 +281,7 @@ class RemittanceListView(LoginRequiredMixin, TemplateView):
             remittance_type_id=1,
             comment="",
         )
-        if not PermissionBackend.check_perm(self.request.user, "treasury.add_remittance", sample_remittance):
+        if not PermissionBackend.check_perm(self.request, "treasury.add_remittance", sample_remittance):
             raise PermissionDenied(_("You are not able to see the treasury interface."))
         return super().dispatch(request, *args, **kwargs)
 
@@ -290,7 +290,7 @@ class RemittanceListView(LoginRequiredMixin, TemplateView):
 
         opened_remittances = RemittanceTable(
             data=Remittance.objects.filter(closed=False).filter(
-                PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all(),
+                PermissionBackend.filter_queryset(self.request, Remittance, "view")).all(),
             prefix="opened-remittances-",
         )
         opened_remittances.paginate(page=self.request.GET.get("opened-remittances-page", 1), per_page=10)
@@ -298,7 +298,7 @@ class RemittanceListView(LoginRequiredMixin, TemplateView):
 
         closed_remittances = RemittanceTable(
             data=Remittance.objects.filter(closed=True).filter(
-                PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all(),
+                PermissionBackend.filter_queryset(self.request, Remittance, "view")).all(),
             prefix="closed-remittances-",
         )
         closed_remittances.paginate(page=self.request.GET.get("closed-remittances-page", 1), per_page=10)
@@ -307,7 +307,7 @@ class RemittanceListView(LoginRequiredMixin, TemplateView):
         no_remittance_tr = SpecialTransactionTable(
             data=SpecialTransaction.objects.filter(source__in=NoteSpecial.objects.filter(~Q(remittancetype=None)),
                                                    specialtransactionproxy__remittance=None).filter(
-                PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all(),
+                PermissionBackend.filter_queryset(self.request, Remittance, "view")).all(),
             exclude=('remittance_remove', ),
             prefix="no-remittance-",
         )
@@ -317,7 +317,7 @@ class RemittanceListView(LoginRequiredMixin, TemplateView):
         with_remittance_tr = SpecialTransactionTable(
             data=SpecialTransaction.objects.filter(source__in=NoteSpecial.objects.filter(~Q(remittancetype=None)),
                                                    specialtransactionproxy__remittance__closed=False).filter(
-                PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all(),
+                PermissionBackend.filter_queryset(self.request, Remittance, "view")).all(),
             exclude=('remittance_add', ),
             prefix="with-remittance-",
         )
@@ -342,7 +342,7 @@ class RemittanceUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView)
         context = super().get_context_data(**kwargs)
 
         data = SpecialTransaction.objects.filter(specialtransactionproxy__remittance=self.object).filter(
-            PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all()
+            PermissionBackend.filter_queryset(self.request, Remittance, "view")).all()
         context["special_transactions"] = SpecialTransactionTable(
             data=data,
             exclude=('remittance_add', 'remittance_remove', ) if self.object.closed else ('remittance_add', ))
diff --git a/apps/wei/tables.py b/apps/wei/tables.py
index 274e8dbd9eb01d40a39e6f0b8fa1de294bc0e86e..b2e55508fe3e825e588b4f19e0cbd98aa3cf1ff7 100644
--- a/apps/wei/tables.py
+++ b/apps/wei/tables.py
@@ -8,7 +8,7 @@ from django.urls import reverse_lazy
 from django.utils.html import format_html
 from django.utils.translation import gettext_lazy as _
 from django_tables2 import A
-from note_kfet.middlewares import get_current_authenticated_user
+from note_kfet.middlewares import get_current_request
 from permission.backends import PermissionBackend
 
 from .models import WEIClub, WEIRegistration, Bus, BusTeam, WEIMembership
@@ -85,7 +85,7 @@ class WEIRegistrationTable(tables.Table):
 
     def render_validate(self, record):
         hasperm = PermissionBackend.check_perm(
-            get_current_authenticated_user(), "wei.add_weimembership", WEIMembership(
+            get_current_request(), "wei.add_weimembership", WEIMembership(
                 club=record.wei,
                 user=record.user,
                 date_start=date.today(),
@@ -110,7 +110,7 @@ class WEIRegistrationTable(tables.Table):
                            f"title=\"{tooltip}\" href=\"{url}\">{text}</a>")
 
     def render_delete(self, record):
-        hasperm = PermissionBackend.check_perm(get_current_authenticated_user(), "wei.delete_weimembership", record)
+        hasperm = PermissionBackend.check_perm(get_current_request(), "wei.delete_weimembership", record)
         return _("Delete") if hasperm else format_html("<span class='no-perm'></span>")
 
     class Meta:
diff --git a/apps/wei/views.py b/apps/wei/views.py
index 04efe95452347ebb022c227c1ff051e36d7f5ccf..cf7c391132685cc9a62bae6ce58f1d506a25b38b 100644
--- a/apps/wei/views.py
+++ b/apps/wei/views.py
@@ -57,7 +57,7 @@ class WEIListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
 
     def get_context_data(self, **kwargs):
         context = super().get_context_data(**kwargs)
-        context["can_create_wei"] = PermissionBackend.check_perm(self.request.user, "wei.add_weiclub", WEIClub(
+        context["can_create_wei"] = PermissionBackend.check_perm(self.request, "wei.add_weiclub", WEIClub(
             name="",
             email="weiclub@example.com",
             year=0,
@@ -112,7 +112,7 @@ class WEIDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
         club = context["club"]
 
         club_transactions = Transaction.objects.all().filter(Q(source=club.note) | Q(destination=club.note)) \
-            .filter(PermissionBackend.filter_queryset(self.request.user, Transaction, "view")) \
+            .filter(PermissionBackend.filter_queryset(self.request, Transaction, "view")) \
             .order_by('-created_at', '-id')
         history_table = HistoryTable(club_transactions, prefix="history-")
         history_table.paginate(per_page=20, page=self.request.GET.get('history-page', 1))
@@ -121,13 +121,13 @@ class WEIDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
         club_member = WEIMembership.objects.filter(
             club=club,
             date_end__gte=date.today(),
-        ).filter(PermissionBackend.filter_queryset(self.request.user, WEIMembership, "view"))
+        ).filter(PermissionBackend.filter_queryset(self.request, WEIMembership, "view"))
         membership_table = WEIMembershipTable(data=club_member, prefix="membership-")
         membership_table.paginate(per_page=20, page=self.request.GET.get('membership-page', 1))
         context['member_list'] = membership_table
 
         pre_registrations = WEIRegistration.objects.filter(
-            PermissionBackend.filter_queryset(self.request.user, WEIRegistration, "view")).filter(
+            PermissionBackend.filter_queryset(self.request, WEIRegistration, "view")).filter(
             membership=None,
             wei=club
         )
@@ -142,7 +142,7 @@ class WEIDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
             my_registration = None
         context["my_registration"] = my_registration
 
-        buses = Bus.objects.filter(PermissionBackend.filter_queryset(self.request.user, Bus, "view")) \
+        buses = Bus.objects.filter(PermissionBackend.filter_queryset(self.request, Bus, "view")) \
             .filter(wei=self.object).annotate(count=Count("memberships")).order_by("name")
         bus_table = BusTable(data=buses, prefix="bus-")
         context['buses'] = bus_table
@@ -167,7 +167,7 @@ class WEIDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
                 emergency_contact_phone="No",
             )
             context["can_add_first_year_member"] = PermissionBackend \
-                .check_perm(self.request.user, "wei.add_weiregistration", empty_fy_registration)
+                .check_perm(self.request, "wei.add_weiregistration", empty_fy_registration)
 
             # Check if the user has the right to create a registration of a random old member.
             empty_old_registration = WEIRegistration(
@@ -180,13 +180,13 @@ class WEIDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
                 emergency_contact_phone="No",
             )
             context["can_add_any_member"] = PermissionBackend \
-                .check_perm(self.request.user, "wei.add_weiregistration", empty_old_registration)
+                .check_perm(self.request, "wei.add_weiregistration", empty_old_registration)
 
         empty_bus = Bus(
             wei=club,
             name="",
         )
-        context["can_add_bus"] = PermissionBackend.check_perm(self.request.user, "wei.add_bus", empty_bus)
+        context["can_add_bus"] = PermissionBackend.check_perm(self.request, "wei.add_bus", empty_bus)
 
         context["not_first_year"] = WEIMembership.objects.filter(user=self.request.user).exists()
 
@@ -370,13 +370,13 @@ class BusManageView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
         context["club"] = self.object.wei
 
         bus = self.object
-        teams = BusTeam.objects.filter(PermissionBackend.filter_queryset(self.request.user, BusTeam, "view")) \
+        teams = BusTeam.objects.filter(PermissionBackend.filter_queryset(self.request, BusTeam, "view")) \
             .filter(bus=bus).annotate(count=Count("memberships")).order_by("name")
         teams_table = BusTeamTable(data=teams, prefix="team-")
         context["teams"] = teams_table
 
         memberships = WEIMembership.objects.filter(PermissionBackend.filter_queryset(
-            self.request.user, WEIMembership, "view")).filter(bus=bus)
+            self.request, WEIMembership, "view")).filter(bus=bus)
         memberships_table = WEIMembershipTable(data=memberships, prefix="membership-")
         memberships_table.paginate(per_page=20, page=self.request.GET.get("membership-page", 1))
         context["memberships"] = memberships_table
@@ -469,7 +469,7 @@ class BusTeamManageView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
         context["club"] = self.object.bus.wei
 
         memberships = WEIMembership.objects.filter(PermissionBackend.filter_queryset(
-            self.request.user, WEIMembership, "view")).filter(team=self.object)
+            self.request, WEIMembership, "view")).filter(team=self.object)
         memberships_table = WEIMembershipTable(data=memberships, prefix="membership-")
         memberships_table.paginate(per_page=20, page=self.request.GET.get("membership-page", 1))
         context["memberships"] = memberships_table
@@ -659,7 +659,7 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update
                                                 data=self.request.POST if self.request.POST else None)
             for field_name, field in membership_form.fields.items():
                 if not PermissionBackend.check_perm(
-                        self.request.user, "wei.change_membership_" + field_name, self.object.membership):
+                        self.request, "wei.change_membership_" + field_name, self.object.membership):
                     field.widget = HiddenInput()
             del membership_form.fields["credit_type"]
             del membership_form.fields["credit_amount"]
@@ -668,7 +668,7 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update
             del membership_form.fields["bank"]
             context["membership_form"] = membership_form
         elif not self.object.first_year and PermissionBackend.check_perm(
-                self.request.user, "wei.change_weiregistration_information_json", self.object):
+                self.request, "wei.change_weiregistration_information_json", self.object):
             choose_bus_form = WEIChooseBusForm(
                 self.request.POST if self.request.POST else dict(
                     bus=Bus.objects.filter(pk__in=self.object.information["preferred_bus_pk"]).all(),
@@ -704,7 +704,7 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update
             membership_form.save()
         # If it is not validated and if this is an old member, then we update the choices
         elif not form.instance.first_year and PermissionBackend.check_perm(
-                self.request.user, "wei.change_weiregistration_information_json", self.object):
+                self.request, "wei.change_weiregistration_information_json", self.object):
             choose_bus_form = WEIChooseBusForm(self.request.POST)
             if not choose_bus_form.is_valid():
                 return self.form_invalid(form)
@@ -726,7 +726,7 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update
             survey = CurrentSurvey(self.object)
             if not survey.is_complete():
                 return reverse_lazy("wei:wei_survey", kwargs={"pk": self.object.pk})
-        if PermissionBackend.check_perm(self.request.user, "wei.add_weimembership", WEIMembership(
+        if PermissionBackend.check_perm(self.request, "wei.add_weimembership", WEIMembership(
             club=self.object.wei,
             user=self.object.user,
             date_start=date.today(),
@@ -753,7 +753,7 @@ class WEIDeleteRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Delete
         if today > wei.membership_end:
             return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,)))
 
-        if not PermissionBackend.check_perm(self.request.user, "wei.delete_weiregistration", object):
+        if not PermissionBackend.check_perm(self.request, "wei.delete_weiregistration", object):
             raise PermissionDenied(_("You don't have the right to delete this WEI registration."))
 
         return super().dispatch(request, *args, **kwargs)
@@ -1049,7 +1049,7 @@ class MemberListRenderView(LoginRequiredMixin, View):
     """
 
     def get_queryset(self, **kwargs):
-        qs = WEIMembership.objects.filter(PermissionBackend.filter_queryset(self.request.user, WEIMembership, "view"))
+        qs = WEIMembership.objects.filter(PermissionBackend.filter_queryset(self.request, WEIMembership, "view"))
         qs = qs.filter(club__pk=self.kwargs["wei_pk"]).order_by(
             Lower('bus__name'),
             Lower('team__name'),
diff --git a/docs/external_services/oauth2.rst b/docs/external_services/oauth2.rst
index 3f1eee2cce317fcec8d3607a093122c66060a4f0..1033030eda232945b8e8f88122289b54eafd03d7 100644
--- a/docs/external_services/oauth2.rst
+++ b/docs/external_services/oauth2.rst
@@ -5,19 +5,10 @@ L'authentification `OAuth2 <https://fr.wikipedia.org/wiki/OAuth>`_ est supporté
 Note Kfet. Elle offre l'avantage non seulement d'identifier les utilisateurs, 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, en l'avertissant sur
-quelles données sont effectivement collectées.
+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.
 
-.. danger::
-   L'implémentation actuelle ne permet pas de choisir quels droits on offre. Se connecter
-   par OAuth2 offre actuellement exactement les mêmes permissions que l'on n'aurait
-   normalement, avec le masque le plus haut, y compris en écriture.
-
-   Faites alors très attention lorsque vous vous connectez à un service tiers via OAuth2,
-   et contrôlez bien exactement ce que l'application fait de vos données, à savoir si
-   elle ignore bien tout ce dont elle n'a pas besoin.
-
-   À l'avenir, la fenêtre d'authentification pourra vous indiquer clairement quels
-   paramètres sont collectés.
 
 Configuration du serveur
 ------------------------
@@ -44,7 +35,15 @@ l'authentification OAuth2. On adapte alors la configuration pour permettre cela
        ...
    }
 
-On ajoute les routes dans ``urls.py`` :
+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',
+   }
+
+On ajoute enfin les routes dans ``urls.py`` :
 
 .. code:: python
 
@@ -58,8 +57,7 @@ L'OAuth2 est désormais prêt à être utilisé.
 Configuration client
 --------------------
 
-Contrairement au `CAS <cas>`_, n'importe qui peut en théorie créer une application OAuth2.
-En théorie, car pour l'instant les permissions ne leur permettent pas.
+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``,
@@ -72,14 +70,30 @@ Il vous suffit de donner à votre application :
 
 * L'identifiant client (client-ID)
 * La clé secrète
-* Les scopes : sous-ensemble de ``[read, write]`` (ignoré pour l'instant, cf premier paragraphe)
+* 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/>`_
-* L'URL de récupération des informations de l'utilisateur : `<https://note.crans.org/api/me/>`_
+* Si besoin, l'URL de récupération des informations de l'utilisateur : `<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é, une 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 le/la 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 le propriétaire du jeton est trésorier du club.
+
+   La vérification des droits du propriétaire est faite systématiquement, afin de ne pas
+   faire confiance au jeton en cas de droits révoqués à son propriétaire.
+
+   Vous pouvez donc contrôler le plus finement possible les permissions octroyées à vos
+   jetons.
+
 Avec Django-allauth
 ###################
 
@@ -131,3 +145,97 @@ alors autant le faire via un shell python :
 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
+#########################
+
+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 utilisateurs. 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.
+
+Sur cette page, les permissions demandées seront listées, et l'utilisateur aura le choix
+d'accepter ou non. Dans les deux cas, l'utilisateur 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 pour plus d'informations.
diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po
index f832c1484795ce54eaae586adf0d60542c86e507..80ab332a4ce75c2d214669e044e9dddb8c537056 100644
--- a/locale/fr/LC_MESSAGES/django.po
+++ b/locale/fr/LC_MESSAGES/django.po
@@ -7,7 +7,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: \n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2021-08-29 14:06+0200\n"
+"POT-Creation-Date: 2021-06-15 21:17+0200\n"
 "PO-Revision-Date: 2020-11-16 20:02+0000\n"
 "Last-Translator: Yohann D'ANELLO <ynerant@crans.org>\n"
 "Language-Team: French <http://translate.ynerant.fr/projects/nk20/nk20/fr/>\n"
@@ -540,8 +540,8 @@ msgstr "Taille maximale : 2 Mo"
 msgid "This image cannot be loaded."
 msgstr "Cette image ne peut pas être chargée."
 
-#: apps/member/forms.py:141 apps/member/views.py:101
-#: apps/registration/forms.py:33 apps/registration/views.py:258
+#: apps/member/forms.py:141 apps/member/views.py:100
+#: apps/registration/forms.py:33 apps/registration/views.py:254
 msgid "An alias with a similar name already exists."
 msgstr "Un alias avec un nom similaire existe déjà."
 
@@ -900,7 +900,7 @@ msgid "Account #"
 msgstr "Compte n°"
 
 #: apps/member/templates/member/base.html:48
-#: apps/member/templates/member/base.html:62 apps/member/views.py:58
+#: apps/member/templates/member/base.html:62 apps/member/views.py:59
 #: apps/registration/templates/registration/future_profile_detail.html:48
 #: apps/wei/templates/wei/weimembership_form.html:117
 msgid "Update Profile"
@@ -932,7 +932,7 @@ msgid ""
 "Are you sure you want to lock this note? This will prevent any transaction "
 "that would be performed, until the note is unlocked."
 msgstr ""
-"Êtes-vous sûr de vouloir verrouiller cette note ? Cela empêchera toute "
+"Êtes-vous sûr⋅e de vouloir verrouiller cette note ? Cela empêchera toute "
 "transaction qui devrait être faite, jusqu'à ce qu'elle soit déverrouillée."
 
 #: apps/member/templates/member/base.html:104
@@ -956,8 +956,8 @@ msgstr "Verrouiller de force"
 msgid ""
 "Are you sure you want to unlock this note? Transactions will be re-enabled."
 msgstr ""
-"Êtes-vous sûr de vouloir déverrouiller cette note ? Les transactions seront "
-"à nouveau possible."
+"Êtes-vous sûr⋅e de vouloir déverrouiller cette note ? Les transactions "
+"seront à nouveau possible."
 
 #: apps/member/templates/member/club_alias.html:10
 #: apps/member/templates/member/profile_alias.html:10 apps/member/views.py:253
@@ -1053,18 +1053,50 @@ msgstr "Changer le mot de passe"
 msgid "API token"
 msgstr "Accès API"
 
-#: apps/member/templates/member/manage_auth_tokens.html:19
+#: apps/member/templates/member/manage_auth_tokens.html:12
+msgid "Token authentication"
+msgstr "Authentification par jeton"
+
+#: apps/member/templates/member/manage_auth_tokens.html:28
 msgid "Token"
 msgstr "Jeton"
 
-#: apps/member/templates/member/manage_auth_tokens.html:26
+#: apps/member/templates/member/manage_auth_tokens.html:35
 msgid "Created"
 msgstr "Créé le"
 
-#: apps/member/templates/member/manage_auth_tokens.html:34
+#: apps/member/templates/member/manage_auth_tokens.html:39
+msgid "Warning"
+msgstr "Attention"
+
+#: apps/member/templates/member/manage_auth_tokens.html:44
 msgid "Regenerate token"
 msgstr "Regénérer le jeton"
 
+#: apps/member/templates/member/manage_auth_tokens.html:53
+msgid "OAuth2 authentication"
+msgstr "Authentification OAuth2"
+
+#: apps/member/templates/member/manage_auth_tokens.html:79
+msgid "Authorization:"
+msgstr "Autorisation :"
+
+#: apps/member/templates/member/manage_auth_tokens.html:83
+msgid "Token:"
+msgstr "Jeton :"
+
+#: apps/member/templates/member/manage_auth_tokens.html:87
+msgid "Revoke Token:"
+msgstr "Révoquer le jeton :"
+
+#: apps/member/templates/member/manage_auth_tokens.html:91
+msgid "Introspect Token:"
+msgstr "Introspection :"
+
+#: apps/member/templates/member/manage_auth_tokens.html:97
+msgid "Show my applications"
+msgstr "Voir mes applications"
+
 #: apps/member/templates/member/picture_update.html:35
 msgid "Nevermind"
 msgstr "Annuler"
@@ -1097,7 +1129,7 @@ msgstr "Sauvegarder les changements"
 msgid "Registrations"
 msgstr "Inscriptions"
 
-#: apps/member/views.py:71 apps/registration/forms.py:23
+#: apps/member/views.py:72 apps/registration/forms.py:23
 msgid "This address must be valid."
 msgstr "Cette adresse doit être valide."
 
@@ -1481,6 +1513,9 @@ msgstr "Pas de motif spécifié"
 #: apps/treasury/templates/treasury/sogecredit_detail.html:65
 #: apps/wei/tables.py:74 apps/wei/tables.py:114
 #: apps/wei/templates/wei/weiregistration_confirm_delete.html:31
+#: note_kfet/templates/oauth2_provider/application_confirm_delete.html:18
+#: note_kfet/templates/oauth2_provider/application_detail.html:39
+#: note_kfet/templates/oauth2_provider/authorized-token-delete.html:12
 msgid "Delete"
 msgstr "Supprimer"
 
@@ -1490,6 +1525,7 @@ msgstr "Supprimer"
 #: apps/wei/templates/wei/bus_detail.html:20
 #: apps/wei/templates/wei/busteam_detail.html:20
 #: apps/wei/templates/wei/busteam_detail.html:40
+#: note_kfet/templates/oauth2_provider/application_detail.html:38
 msgid "Edit"
 msgstr "Éditer"
 
@@ -1731,7 +1767,7 @@ msgstr "s'applique au club"
 msgid "role permissions"
 msgstr "permissions par rôles"
 
-#: apps/permission/signals.py:63
+#: apps/permission/signals.py:67
 #, python-brace-format
 msgid ""
 "You don't have the permission to change the field {field} on this instance "
@@ -1740,7 +1776,7 @@ msgstr ""
 "Vous n'avez pas la permission de modifier le champ {field} sur l'instance du "
 "modèle {app_label}.{model_name}."
 
-#: apps/permission/signals.py:73 apps/permission/views.py:105
+#: apps/permission/signals.py:77 apps/permission/views.py:105
 #, python-brace-format
 msgid ""
 "You don't have the permission to add an instance of model {app_label}."
@@ -1749,7 +1785,7 @@ msgstr ""
 "Vous n'avez pas la permission d'ajouter une instance du modèle {app_label}."
 "{model_name}."
 
-#: apps/permission/signals.py:102
+#: apps/permission/signals.py:106
 #, python-brace-format
 msgid ""
 "You don't have the permission to delete this instance of model {app_label}."
@@ -1799,6 +1835,25 @@ msgstr "Requête :"
 msgid "No associated permission"
 msgstr "Pas de permission associée"
 
+#: apps/permission/templates/permission/scopes.html:8
+msgid "Available scopes"
+msgstr "Scopes disponibles"
+
+#: apps/permission/templates/permission/scopes.html:42
+#: note_kfet/templates/oauth2_provider/application_list.html:24
+msgid "No applications defined"
+msgstr "Pas d'application définie"
+
+#: apps/permission/templates/permission/scopes.html:43
+#: note_kfet/templates/oauth2_provider/application_list.html:25
+msgid "Click here"
+msgstr "Cliquez ici"
+
+#: apps/permission/templates/permission/scopes.html:43
+#: note_kfet/templates/oauth2_provider/application_list.html:25
+msgid "if you want to register a new one"
+msgstr "si vous voulez en enregistrer une nouvelle"
+
 #: apps/permission/views.py:72
 #, python-brace-format
 msgid ""
@@ -1977,35 +2032,35 @@ msgstr "L'équipe de la Note Kfet."
 msgid "Register new user"
 msgstr "Enregistrer un nouvel utilisateur"
 
-#: apps/registration/views.py:93
+#: apps/registration/views.py:95
 msgid "Email validation"
 msgstr "Validation de l'adresse mail"
 
-#: apps/registration/views.py:95
+#: apps/registration/views.py:97
 msgid "Validate email"
 msgstr "Valider l'adresse e-mail"
 
-#: apps/registration/views.py:137
+#: apps/registration/views.py:141
 msgid "Email validation unsuccessful"
 msgstr "La validation de l'adresse mail a échoué"
 
-#: apps/registration/views.py:148
+#: apps/registration/views.py:152
 msgid "Email validation email sent"
 msgstr "L'email de vérification de l'adresse email a bien été envoyé"
 
-#: apps/registration/views.py:156
+#: apps/registration/views.py:160
 msgid "Resend email validation link"
 msgstr "Renvoyer le lien de validation"
 
-#: apps/registration/views.py:174
+#: apps/registration/views.py:178
 msgid "Pre-registered users list"
 msgstr "Liste des utilisateurs en attente d'inscription"
 
-#: apps/registration/views.py:198
+#: apps/registration/views.py:202
 msgid "Unregistered users"
 msgstr "Utilisateurs en attente d'inscription"
 
-#: apps/registration/views.py:211
+#: apps/registration/views.py:215
 msgid "Registration detail"
 msgstr "Détails de l'inscription"
 
@@ -2225,7 +2280,7 @@ msgstr "Cette facture est verrouillée et ne peut pas être supprimée."
 msgid ""
 "Are you sure you want to delete this invoice? This action can't be undone."
 msgstr ""
-"Êtes-vous sûr de vouloir supprimer cette facture ? Cette action ne pourra "
+"Êtes-vous sûr⋅e de vouloir supprimer cette facture ? Cette action ne pourra "
 "pas être annulée."
 
 #: apps/treasury/templates/treasury/invoice_confirm_delete.html:28
@@ -2863,7 +2918,7 @@ msgid ""
 "Are you sure you want to delete the registration of %(user)s for the WEI "
 "%(wei_name)s? This action can't be undone."
 msgstr ""
-"Êtes-vous sûr de vouloir supprimer l'inscription de %(user)s pour le WEI "
+"Êtes-vous sûr⋅e de vouloir supprimer l'inscription de %(user)s pour le WEI "
 "%(wei_name)s ? Cette action ne pourra pas être annulée."
 
 #: apps/wei/templates/wei/weiregistration_list.html:19
@@ -3127,6 +3182,113 @@ msgstr "Chercher par un attribut tel que le nom …"
 msgid "There is no results."
 msgstr "Il n'y a pas de résultat."
 
+#: note_kfet/templates/oauth2_provider/application_confirm_delete.html:8
+msgid "Are you sure to delete the application"
+msgstr "Êtes-vous sûr⋅e de vouloir supprimer l'application"
+
+#: note_kfet/templates/oauth2_provider/application_confirm_delete.html:17
+#: note_kfet/templates/oauth2_provider/authorize.html:28
+msgid "Cancel"
+msgstr "Annuler"
+
+#: note_kfet/templates/oauth2_provider/application_detail.html:11
+msgid "Client id"
+msgstr "ID client"
+
+#: note_kfet/templates/oauth2_provider/application_detail.html:14
+msgid "Client secret"
+msgstr "Secret client"
+
+#: note_kfet/templates/oauth2_provider/application_detail.html:17
+msgid "Client type"
+msgstr "Type de client"
+
+#: note_kfet/templates/oauth2_provider/application_detail.html:20
+msgid "Authorization Grant Type"
+msgstr "Type d'autorisation"
+
+#: note_kfet/templates/oauth2_provider/application_detail.html:23
+msgid "Redirect Uris"
+msgstr "URIs de redirection"
+
+#: note_kfet/templates/oauth2_provider/application_detail.html:29
+#, python-format
+msgid ""
+"You can go <a href=\"%(scopes_url)s\">here</a> to generate authorization "
+"link templates and convert permissions to scope numbers with the permissions "
+"that you want to grant for your application."
+msgstr ""
+"Vous pouvez aller <a href=\"%(scopes_url)s\">ici</a> pour générer des modèles "
+"de liens d'autorisation et convertir des permissions en identifiants de "
+"scopes avec les permissions que vous souhaitez attribuer à votre application."
+
+#: note_kfet/templates/oauth2_provider/application_detail.html:37
+#: note_kfet/templates/oauth2_provider/application_form.html:23
+msgid "Go Back"
+msgstr "Retour en arrière"
+
+#: note_kfet/templates/oauth2_provider/application_form.html:12
+msgid "Edit application"
+msgstr "Modifier l'application"
+
+#: note_kfet/templates/oauth2_provider/application_list.html:7
+msgid "Your applications"
+msgstr "Vos applications"
+
+#: note_kfet/templates/oauth2_provider/application_list.html:11
+msgid ""
+"You can find on this page the list of the applications that you already "
+"registered."
+msgstr ""
+"Vous pouvez trouver sur cette page la liste des applications que vous avez "
+"déjà enregistrées."
+
+#: note_kfet/templates/oauth2_provider/application_list.html:30
+msgid "New Application"
+msgstr "Nouvelle application"
+
+#: note_kfet/templates/oauth2_provider/application_list.html:31
+msgid "Authorized Tokens"
+msgstr "Jetons autorisés"
+
+#: note_kfet/templates/oauth2_provider/application_registration_form.html:5
+msgid "Register a new application"
+msgstr "Enregistrer une nouvelle application"
+
+#: note_kfet/templates/oauth2_provider/authorize.html:9
+#: note_kfet/templates/oauth2_provider/authorize.html:29
+msgid "Authorize"
+msgstr "Autoriser"
+
+#: note_kfet/templates/oauth2_provider/authorize.html:14
+msgid "Application requires following permissions:"
+msgstr "L'application requiert les permissions suivantes :"
+
+#: note_kfet/templates/oauth2_provider/authorize.html:36
+#: note_kfet/templates/oauth2_provider/authorized-oob.html:15
+msgid "Error:"
+msgstr "Erreur :"
+
+#: note_kfet/templates/oauth2_provider/authorized-oob.html:13
+msgid "Success"
+msgstr "Succès"
+
+#: note_kfet/templates/oauth2_provider/authorized-oob.html:21
+msgid "Please return to your application and enter this code:"
+msgstr "Merci de retourner à votre application et entrez ce code :"
+
+#: note_kfet/templates/oauth2_provider/authorized-token-delete.html:9
+msgid "Are you sure you want to delete this token?"
+msgstr "Êtes-vous sûr⋅e de vouloir supprimer ce jeton ?"
+
+#: note_kfet/templates/oauth2_provider/authorized-tokens.html:7
+msgid "Tokens"
+msgstr "Jetons"
+
+#: note_kfet/templates/oauth2_provider/authorized-tokens.html:22
+msgid "There are no authorized tokens yet."
+msgstr "Il n'y a pas encore de jeton autorisé."
+
 #: note_kfet/templates/registration/logged_out.html:13
 msgid "Thanks for spending some quality time with the Web site today."
 msgstr "Merci d'avoir utilisé la Note Kfet."
@@ -3168,7 +3330,7 @@ msgid ""
 "password twice so we can verify you typed it in correctly."
 msgstr ""
 "Veuillez entrer votre ancien mot de passe pour des raisons de sécurité, puis "
-"renseigner votre nouveau mot de passe à deux reprises, pour être sûr de "
+"renseigner votre nouveau mot de passe à deux reprises, pour être sûr⋅e de "
 "l'avoir tapé correctement."
 
 #: note_kfet/templates/registration/password_change_form.html:16
diff --git a/note_kfet/admin.py b/note_kfet/admin.py
index 375122d830d6d5de19ed0cf2ea5cf1bfafcc84c2..dc209c67bed12fa6b32e486838ebefae85caa749 100644
--- a/note_kfet/admin.py
+++ b/note_kfet/admin.py
@@ -6,7 +6,6 @@ from django.contrib.admin import AdminSite
 from django.contrib.sites.admin import Site, SiteAdmin
 
 from member.views import CustomLoginView
-from .middlewares import get_current_session
 
 
 class StrongAdminSite(AdminSite):
@@ -14,8 +13,7 @@ class StrongAdminSite(AdminSite):
         """
         Authorize only staff that have the correct permission mask
         """
-        session = get_current_session()
-        return request.user.is_active and request.user.is_staff and session.get("permission_mask", -1) >= 42
+        return request.user.is_active and request.user.is_staff and request.session.get("permission_mask", -1) >= 42
 
     def login(self, request, extra_context=None):
         return CustomLoginView.as_view()(request)
diff --git a/note_kfet/middlewares.py b/note_kfet/middlewares.py
index fcb84c9dfd6f7c7a786430b6d347219abb4fd273..e763a571df282047970e2accdaf6cf4d80d7f9e3 100644
--- a/note_kfet/middlewares.py
+++ b/note_kfet/middlewares.py
@@ -1,43 +1,23 @@
 # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
 # SPDX-License-Identifier: GPL-3.0-or-later
 
+from threading import local
+
 from django.conf import settings
 from django.contrib.auth import login
-from django.contrib.auth.models import AnonymousUser, User
-from django.contrib.sessions.backends.db import SessionStore
-
-from threading import local
+from django.contrib.auth.models import User
 
-USER_ATTR_NAME = getattr(settings, 'LOCAL_USER_ATTR_NAME', '_current_user')
-SESSION_ATTR_NAME = getattr(settings, 'LOCAL_SESSION_ATTR_NAME', '_current_session')
-IP_ATTR_NAME = getattr(settings, 'LOCAL_IP_ATTR_NAME', '_current_ip')
+REQUEST_ATTR_NAME = getattr(settings, 'LOCAL_REQUEST_ATTR_NAME', '_current_request')
 
 _thread_locals = local()
 
 
-def _set_current_user_and_ip(user=None, session=None, ip=None):
-    setattr(_thread_locals, USER_ATTR_NAME, user)
-    setattr(_thread_locals, SESSION_ATTR_NAME, session)
-    setattr(_thread_locals, IP_ATTR_NAME, ip)
-
-
-def get_current_user() -> User:
-    return getattr(_thread_locals, USER_ATTR_NAME, None)
+def _set_current_request(request=None):
+    setattr(_thread_locals, REQUEST_ATTR_NAME, request)
 
 
-def get_current_session() -> SessionStore:
-    return getattr(_thread_locals, SESSION_ATTR_NAME, None)
-
-
-def get_current_ip() -> str:
-    return getattr(_thread_locals, IP_ATTR_NAME, None)
-
-
-def get_current_authenticated_user():
-    current_user = get_current_user()
-    if isinstance(current_user, AnonymousUser):
-        return None
-    return current_user
+def get_current_request():
+    return getattr(_thread_locals, REQUEST_ATTR_NAME, None)
 
 
 class SessionMiddleware(object):
@@ -49,8 +29,6 @@ class SessionMiddleware(object):
         self.get_response = get_response
 
     def __call__(self, request):
-        user = request.user
-
         # If we authenticate through a token to connect to the API, then we query the good user
         if 'HTTP_AUTHORIZATION' in request.META and request.path.startswith("/api"):
             token = request.META.get('HTTP_AUTHORIZATION')
@@ -60,20 +38,14 @@ class SessionMiddleware(object):
                 if Token.objects.filter(key=token).exists():
                     token_obj = Token.objects.get(key=token)
                     user = token_obj.user
+                    request.user = user
                     session = request.session
                     session["permission_mask"] = 42
                     session.save()
 
-        if 'HTTP_X_REAL_IP' in request.META:
-            ip = request.META.get('HTTP_X_REAL_IP')
-        elif 'HTTP_X_FORWARDED_FOR' in request.META:
-            ip = request.META.get('HTTP_X_FORWARDED_FOR').split(', ')[0]
-        else:
-            ip = request.META.get('REMOTE_ADDR')
-
-        _set_current_user_and_ip(user, request.session, ip)
+        _set_current_request(request)
         response = self.get_response(request)
-        _set_current_user_and_ip(None, None, None)
+        _set_current_request(None)
 
         return response
 
diff --git a/note_kfet/settings/base.py b/note_kfet/settings/base.py
index 6cb068a5c8398fdf94e997cba1b5ff0b94e6e74f..a0ece71504efe9f8dea0f2f0c0e421c15301d2a0 100644
--- a/note_kfet/settings/base.py
+++ b/note_kfet/settings/base.py
@@ -245,6 +245,11 @@ REST_FRAMEWORK = {
     'PAGE_SIZE': 20,
 }
 
+# OAuth2 Provider
+OAUTH2_PROVIDER = {
+    'SCOPES_BACKEND_CLASS': 'permission.scopes.PermissionScopes',
+}
+
 # Take control on how widget templates are sourced
 # See https://docs.djangoproject.com/en/2.2/ref/forms/renderers/#templatessetting
 FORM_RENDERER = 'django.forms.renderers.TemplatesSetting'
diff --git a/note_kfet/templates/oauth2_provider/application_confirm_delete.html b/note_kfet/templates/oauth2_provider/application_confirm_delete.html
new file mode 100644
index 0000000000000000000000000000000000000000..ec7f19fb262e424dca64e2e841d827e59944c007
--- /dev/null
+++ b/note_kfet/templates/oauth2_provider/application_confirm_delete.html
@@ -0,0 +1,24 @@
+{% extends "base.html" %}
+
+{% load i18n %}
+
+{% block content %}
+    <div class="card">
+        <div class="card-header text-center">
+            <h3>{% trans "Are you sure to delete the application" %} {{ application.name }}?</h3>
+        </div>
+        <div class="card-footer text-center">
+
+            <form method="post" action="{% url 'oauth2_provider:delete' application.pk %}">
+                {% csrf_token %}
+
+                <div class="control-group">
+                    <div class="controls">
+                        <a class="btn btn-secondary btn-large" href="{% url "oauth2_provider:list" %}">{% trans "Cancel" %}</a>
+                        <input type="submit" class="btn btn-large btn-danger" name="allow" value="{% trans "Delete" %}"/>
+                    </div>
+                </div>
+            </form>
+        </div>
+    </div>
+{% endblock content %}
diff --git a/note_kfet/templates/oauth2_provider/application_detail.html b/note_kfet/templates/oauth2_provider/application_detail.html
new file mode 100644
index 0000000000000000000000000000000000000000..5d3cc008e0bb5e19153548b61c19e593015dacc3
--- /dev/null
+++ b/note_kfet/templates/oauth2_provider/application_detail.html
@@ -0,0 +1,42 @@
+{% extends "base.html" %}
+
+{% load i18n %}
+{% block content %}
+    <div class="card">
+        <div class="card-header text-center">
+            <h3>{{ application.name }}</h3>
+        </div>
+        <div class="card-body">
+            <dl class="row">
+                <dt class="col-xl-6">{% trans "Client id" %}</dt>
+                <dd class="col-xl-6"><input class="form-control" type="text" value="{{ application.client_id }}" readonly></dd>
+
+                <dt class="col-xl-6">{% trans "Client secret" %}</dt>
+                <dd class="col-xl-6"><input class="form-control" type="text" value="****************************************************************" readonly></dd>
+
+                <dt class="col-xl-6">{% trans "Client type" %}</dt>
+                <dd class="col-xl-6">{{ application.client_type }}</dd>
+
+                <dt class="col-xl-6">{% trans "Authorization Grant Type" %}</dt>
+                <dd class="col-xl-6">{{ application.authorization_grant_type }}</dd>
+
+                <dt class="col-xl-6">{% trans "Redirect Uris" %}</dt>
+                <dd class="col-xl-6"><textarea class="form-control" readonly>{{ application.redirect_uris }}</textarea></dd>
+            </dl>
+
+            <div class="alert alert-info">
+                {% url 'permission:scopes' as scopes_url %}
+                {% blocktrans trimmed %}
+                    You can go <a href="{{ scopes_url }}">here</a> to generate authorization link templates and convert
+                    permissions to scope numbers with the permissions that you want to grant for your application.
+                {% endblocktrans %}
+            </div>
+
+        </div>
+        <div class="card-footer text-center">
+            <a class="btn btn-secondary" href="{% url "oauth2_provider:list" %}">{% trans "Go Back" %}</a>
+            <a class="btn btn-primary" href="{% url "oauth2_provider:update" application.id %}">{% trans "Edit" %}</a>
+            <a class="btn btn-danger" href="{% url "oauth2_provider:delete" application.id %}">{% trans "Delete" %}</a>
+        </div>
+    </div>
+{% endblock content %}
diff --git a/note_kfet/templates/oauth2_provider/application_form.html b/note_kfet/templates/oauth2_provider/application_form.html
new file mode 100644
index 0000000000000000000000000000000000000000..aa0840189cfce45043009cabdf1e3411b3f0394d
--- /dev/null
+++ b/note_kfet/templates/oauth2_provider/application_form.html
@@ -0,0 +1,30 @@
+{% extends "base.html" %}
+
+{% load i18n %}
+{% load crispy_forms_filters %}
+
+{% block content %}
+    <form class="form-horizontal" method="post" action="{% block app-form-action-url %}{% url 'oauth2_provider:update' application.id %}{% endblock app-form-action-url %}">
+        <div class="card">
+            <div class="card-header text-center">
+                <h3 class="block-center-heading">
+                    {% block app-form-title %}
+                        {% trans "Edit application" %} {{ application.name }}
+                    {% endblock app-form-title %}
+                </h3>
+            </div>
+            <div class="card-body">
+                    {% csrf_token %}
+                    {{ form|crispy }}
+            </div>
+            <div class="card-footer text-center control-group">
+                <div class="controls">
+                    <a class="btn btn-secondary" href="{% block app-form-back-url %}{% url "oauth2_provider:detail" application.id %}{% endblock app-form-back-url %}">
+                        {% trans "Go Back" %}
+                    </a>
+                    <button type="submit" class="btn btn-primary">Save</button>
+                </div>
+            </div>
+        </div>
+    </form>
+{% endblock %}
diff --git a/note_kfet/templates/oauth2_provider/application_list.html b/note_kfet/templates/oauth2_provider/application_list.html
new file mode 100644
index 0000000000000000000000000000000000000000..0238b345b3085d84503019720f716d102c370acc
--- /dev/null
+++ b/note_kfet/templates/oauth2_provider/application_list.html
@@ -0,0 +1,34 @@
+{% extends "base.html" %}
+
+{% load i18n %}
+{% block content %}
+    <div class="card">
+        <div class="card-header text-center">
+            <h3>{% trans "Your applications" %}</h3>
+        </div>
+        <div class="card-body">
+            <div class="alert alert-info">
+                {% blocktrans trimmed %}
+                    You can find on this page the list of the applications that you already registered.
+                {% endblocktrans %}
+            </div>
+
+            {% if applications %}
+                <ul>
+                    {% for application in applications %}
+                        <li><a href="{{ application.get_absolute_url }}">{{ application.name }}</a></li>
+                    {% endfor %}
+                </ul>
+            {% else %}
+                <p>
+                    {% trans "No applications defined" %}.
+                    <a href="{% url 'oauth2_provider:register' %}">{% trans "Click here" %}</a> {% trans "if you want to register a new one" %}.
+                </p>
+            {% endif %}
+        </div>
+        <div class="card-footer text-center">
+            <a class="btn btn-success" href="{% url "oauth2_provider:register" %}">{% trans "New Application" %}</a>
+            <a class="btn btn-secondary" href="{% url "oauth2_provider:authorized-token-list" %}">{% trans "Authorized Tokens" %}</a>
+        </div>
+    </div>
+{% endblock content %}
diff --git a/note_kfet/templates/oauth2_provider/application_registration_form.html b/note_kfet/templates/oauth2_provider/application_registration_form.html
new file mode 100644
index 0000000000000000000000000000000000000000..c22eca9ef81a55a3480626efebfedb491d80860f
--- /dev/null
+++ b/note_kfet/templates/oauth2_provider/application_registration_form.html
@@ -0,0 +1,9 @@
+{% extends "oauth2_provider/application_form.html" %}
+
+{% load i18n %}
+
+{% block app-form-title %}{% trans "Register a new application" %}{% endblock app-form-title %}
+
+{% block app-form-action-url %}{% url 'oauth2_provider:register' %}{% endblock app-form-action-url %}
+
+{% block app-form-back-url %}{% url "oauth2_provider:list" %}"{% endblock app-form-back-url %}
diff --git a/note_kfet/templates/oauth2_provider/authorize.html b/note_kfet/templates/oauth2_provider/authorize.html
new file mode 100644
index 0000000000000000000000000000000000000000..543bcf2d6be41dbf038e614a57eafeee8a33f5a2
--- /dev/null
+++ b/note_kfet/templates/oauth2_provider/authorize.html
@@ -0,0 +1,49 @@
+{% extends "base.html" %}
+
+{% load i18n %}
+{% load crispy_forms_filters %}
+
+{% block content %}
+    <div class="card">
+        <div class="card-header text-center">
+            <h3>{% trans "Authorize" %} {{ application.name }} ?</h3>
+        </div>
+        {% if not error %}
+            <form id="authorizationForm" method="post">
+                <div class="card-body">
+                    <p>{% trans "Application requires following permissions:" %}</p>
+
+                    <ul>
+                        {% for scope in scopes_descriptions %}
+                            <li>{{ scope }}</li>
+                        {% endfor %}
+                    </ul>
+
+                    {% csrf_token %}
+                    {{ form|crispy }}
+                </div>
+                <div class="card-footer text-center">
+                    <div class="control-group">
+                        <div class="controls">
+                            <input type="submit" class="btn btn-large btn-danger" value="{% trans "Cancel" %}"/>
+                            <input type="submit" class="btn btn-large btn-primary" name="allow" value="{% trans "Authorize" %}"/>
+                        </div>
+                    </div>
+                </div>
+            </form>
+        {% else %}
+            <div class="card-body">
+                <h2>{% trans "Error:" %} {{ error.error }}</h2>
+                <p>{{ error.description }}</p>
+            </div>
+        {% endif %}
+    </div>
+{% endblock %}
+
+{% block extrajavascript %}
+    <script>
+        {# Small hack to have the remove the allow checkbox and replace it with the button #}
+        {# Django oauth toolkit does simply not render the wdiget since it is not hidden, and create directly the button #}
+        document.getElementById('div_id_allow').parentElement.remove()
+    </script>
+{% endblock %}
diff --git a/note_kfet/templates/oauth2_provider/authorized-oob.html b/note_kfet/templates/oauth2_provider/authorized-oob.html
new file mode 100644
index 0000000000000000000000000000000000000000..c0c3a4f87f65f629a4264ec2be699e4e63264895
--- /dev/null
+++ b/note_kfet/templates/oauth2_provider/authorized-oob.html
@@ -0,0 +1,29 @@
+{% extends "base.html" %}
+
+{% load i18n %}
+
+{% block title %}
+Success code={{code}}
+{% endblock %}
+
+{% block content %}
+    <div class="card">
+        <h3 class="card-header text-center">
+            {% if not error %}
+               {% trans "Success" %}
+            {% else %}
+                {% trans "Error:" %} {{ error.error }}
+            {% endif %}
+        </h3>
+
+        <div class="card-body">
+            {% if not error %}
+                <p>{% trans "Please return to your application and enter this code:" %}</p>
+
+                <p><code>{{ code }}</code></p>
+            {% else %}
+                <p>{{ error.description }}</p>
+            {% endif %}
+        </div>
+    </div>
+{% endblock %}
diff --git a/note_kfet/templates/oauth2_provider/authorized-token-delete.html b/note_kfet/templates/oauth2_provider/authorized-token-delete.html
new file mode 100644
index 0000000000000000000000000000000000000000..8d91bf35e112222bc69d1efb8af1dbe1ceab05fa
--- /dev/null
+++ b/note_kfet/templates/oauth2_provider/authorized-token-delete.html
@@ -0,0 +1,16 @@
+{% extends "base.html" %}
+
+{% load i18n %}
+{% block content %}
+    <div class="card">
+        <form action="" method="post">
+            {% csrf_token %}
+            <h3 class="card-header text-center">
+                {% trans "Are you sure you want to delete this token?" %}
+            </h3>
+            <div class="card-footer text-center">
+                <input type="submit" value="{% trans "Delete" %}" />
+            </div>
+        </form>
+    </div>
+{% endblock %}
diff --git a/note_kfet/templates/oauth2_provider/authorized-tokens.html b/note_kfet/templates/oauth2_provider/authorized-tokens.html
new file mode 100644
index 0000000000000000000000000000000000000000..3d7f40f2bdd07ede41817d6854cbb5083a05b835
--- /dev/null
+++ b/note_kfet/templates/oauth2_provider/authorized-tokens.html
@@ -0,0 +1,27 @@
+{% extends "base.html" %}
+
+{% load i18n %}
+{% block content %}
+    <div class="card">
+        <h3 class="card-header text-center">
+            {% trans "Tokens" %}
+        </h3>
+        <div class="card-body">
+            <ul>
+            {% for authorized_token in authorized_tokens %}
+                <li>
+                    {{ authorized_token.application }}
+                    (<a href="{% url 'oauth2_provider:authorized-token-delete' authorized_token.pk %}">revoke</a>)
+                </li>
+                <ul>
+                {% for scope_name, scope_description in authorized_token.scopes.items %}
+                    <li>{{ scope_name }}: {{ scope_description }}</li>
+                {% endfor %}
+                </ul>
+            {% empty %}
+                <li>{% trans "There are no authorized tokens yet." %}</li>
+            {% endfor %}
+            </ul>
+        </div>
+    </div>
+{% endblock %}
diff --git a/note_kfet/views.py b/note_kfet/views.py
index 797de4efbe5733b48fd8c726c06a7ac025cd0204..b65bf7619cea9684a0cad61159fcf04c598256db 100644
--- a/note_kfet/views.py
+++ b/note_kfet/views.py
@@ -19,11 +19,11 @@ class IndexView(LoginRequiredMixin, RedirectView):
         user = self.request.user
 
         # The account note will have the consumption page as default page
-        if not PermissionBackend.check_perm(user, "auth.view_user", user):
+        if not PermissionBackend.check_perm(self.request, "auth.view_user", user):
             return reverse("note:consos")
 
         # People that can see the alias BDE are Kfet members
-        if PermissionBackend.check_perm(user, "alias.view_alias", Alias.objects.get(name="BDE")):
+        if PermissionBackend.check_perm(self.request, "alias.view_alias", Alias.objects.get(name="BDE")):
             return reverse("note:transfer")
 
         # Non-Kfet members will don't see the transfer page, but their profile page