From dbf18d84f24956f38d129aab55ef5ef338407097 Mon Sep 17 00:00:00 2001
From: Nicolas ROCHE <nroche@entrouvert.com>
Date: Thu, 16 Jul 2020 17:31:03 +0200
Subject: [PATCH] idp_oidc: revoke oidc claims authorization (#45200)

---
 .../authentic2/authorized_oauth_apps.html     | 36 +++++++++++++++++++
 src/authentic2/urls.py                        |  3 ++
 src/authentic2/views.py                       | 24 +++++++++++++
 tests/test_idp_oidc.py                        | 29 +++++++++++++++
 4 files changed, 92 insertions(+)
 create mode 100644 src/authentic2/templates/authentic2/authorized_oauth_apps.html

diff --git a/src/authentic2/templates/authentic2/authorized_oauth_apps.html b/src/authentic2/templates/authentic2/authorized_oauth_apps.html
new file mode 100644
index 00000000..8dccd25e
--- /dev/null
+++ b/src/authentic2/templates/authentic2/authorized_oauth_apps.html
@@ -0,0 +1,36 @@
+{% extends "authentic2/base-page.html" %}
+{% load i18n %}
+
+{% block content %}
+{% block oidc-authorized-oauth-apps-pre %}{% endblock %}
+<div id="oidc-authorized-oauth-apps" class="authorized-oauth-apps">
+  {% block oidc-authorized-oauth-apps-top %}{% endblock %}
+  {% if authorized_oauth_apps|length_is:0 %}
+  {% trans "You have no granted application access to your account." %}
+  {% else %}
+  <ul>
+    {% for auth in authorized_oauth_apps %}
+    <li>
+      <form method="post">
+        {% csrf_token %}
+        <input type="hidden" id="auth-id" name="auth_id" value="{{ auth.id }}">
+        <span class="oidc-authorized-oauth-apps-client">
+          {{ auth.client }}
+        </span>
+        <span class="oidc-authorized-oauth-apps-since">
+          allowed since {{ auth.created }}
+        </span>
+        <span class="oidc-authorized-oauth-apps-expired">
+          (expire on {{ auth.expired }})
+        </span>
+        <button class="delete-button">{% trans 'Delete' %}</button>
+      </form>
+    </li>
+    {% endfor %}
+  </ul>
+  </table>
+  {% endif %}
+  {% block oidc-authorized-oauth-apps-bottom %}{% endblock %}
+</div>
+{% block oidc-authorized-oauth-apps-post %}{% endblock %}
+{% endblock %}
diff --git a/src/authentic2/urls.py b/src/authentic2/urls.py
index 1e5b4bbf..db3512f9 100644
--- a/src/authentic2/urls.py
+++ b/src/authentic2/urls.py
@@ -64,16 +64,19 @@ accounts_urlpatterns = [
         views.edit_profile,
         name='profile_edit_with_scope'),
     url(r'^change-email/$',
         views.email_change,
         name='email-change'),
     url(r'^change-email/verify/$',
         views.email_change_verify,
         name='email-change-verify'),
+    url(r'^authorizations/$',
+        login_required(views.authorized_oauth_apps),
+        name='authorized-oauth-apps'),
     url(r'^$',
         views.profile,
         name='account_management'),
 
     # Password change
     url(r'^password/change/$',
         views.password_change,
         name='password_change'),
diff --git a/src/authentic2/views.py b/src/authentic2/views.py
index 78eb6760..589876fb 100644
--- a/src/authentic2/views.py
+++ b/src/authentic2/views.py
@@ -50,16 +50,17 @@ from django.contrib.auth import get_user_model
 from django.http import Http404
 from django.utils.http import urlsafe_base64_decode
 from django.views.generic.edit import CreateView
 from django.forms import CharField
 from django.http import HttpResponseBadRequest
 from django.template import loader
 
 from authentic2.compat.misc import default_token_generator
+from authentic2_idp_oidc.models import OIDCAuthorization
 from . import (utils, app_settings, decorators, constants,
                models, cbv, hooks, validators)
 from .utils import switch_user
 from .a2_rbac.utils import get_default_ou
 from .a2_rbac.models import OrganizationalUnit as OU
 from .forms import (
     passwords as passwords_forms,
     registration as registration_forms,
@@ -1264,8 +1265,31 @@ def notimplemented_view(request):
 class SuView(View):
     def get(self, request, uuid):
         user = switch_user.resolve_token(uuid)
         if not user:
             raise Http404
         return utils.simulate_authentication(request, user, 'su')
 
 su = SuView.as_view()
+
+
+class AuthorizedOauthAppsView(TemplateView):
+    template_name = 'authentic2/authorized_oauth_apps.html'
+    title = _('Granted Applications')
+
+    def get_context_data(self, **kwargs):
+        context = super(AuthorizedOauthAppsView, self).get_context_data(**kwargs)
+        context['authorized_oauth_apps'] = OIDCAuthorization.objects.filter(
+            user=self.request.user)
+        return context
+
+    def post(self, request, *args, **kwargs):
+        if request.user:
+            qs = OIDCAuthorization.objects.filter(user=request.user)
+            auth_id = request.POST.get('auth_id')
+            if auth_id:
+                qs = qs.filter(id=auth_id)
+            qs.delete()
+        return HttpResponseRedirect(reverse('authorized-oauth-apps'))
+
+
+authorized_oauth_apps = AuthorizedOauthAppsView.as_view()
diff --git a/tests/test_idp_oidc.py b/tests/test_idp_oidc.py
index a9517fe0..3a718f77 100644
--- a/tests/test_idp_oidc.py
+++ b/tests/test_idp_oidc.py
@@ -1599,8 +1599,37 @@ def test_oidc_client_clean():
     with pytest.raises(ValidationError, match=r'same domain'):
         OIDCClient(
             redirect_uris='https://example.com/ https://example2.com/',
             identifier_policy=OIDCClient.POLICY_PAIRWISE).clean()
 
     OIDCClient(
         redirect_uris='https://example.com/ https://example2.com/',
         sector_identifier_uri='https://example.com/').clean()
+
+
+def test_oidc_authorized_oauth_apps_views(app, oidc_client, simple_user):
+    url = make_url('authorized-oauth-apps')
+    response = app.get(url, status=302)
+    assert '/login/' in response.location
+
+    utils.login(app, simple_user)
+    response = app.get(url, status=200)
+    assert "You have no granted application access to your account." in response.text
+
+    OIDCAuthorization.objects.create(
+        client=oidc_client, user=simple_user, scopes='openid',
+        expired=now() + datetime.timedelta(days=2))
+    OIDCAuthorization.objects.create(
+        client=oidc_client, user=simple_user, scopes='openid profile',
+        expired=now() + datetime.timedelta(days=2))
+    OIDCAuthorization.objects.create(
+        client=oidc_client, user=simple_user, scopes='openid profile email',
+        expired=now() + datetime.timedelta(days=2))
+
+    response = app.get(url, status=200)
+    assert "You have no granted application access to your account." not in response.text
+    assert len(response.html.find_all('button', {'class': 'delete-button'})) == 3
+
+    # revoke one
+    response = response.forms[1].submit()
+    response = response.follow()
+    assert len(response.html.find_all('button', {'class': 'delete-button'})) == 2
-- 
2.27.0

