providers/oauth2: fix end-session view not working, add tests
This commit is contained in:
		@ -33,6 +33,7 @@ from passbook.providers.oauth2.models import (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
LOGGER = get_logger()
 | 
			
		||||
APPLICATION_SLUG = "grafana"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@skipUnless(platform.startswith("linux"), "requires local docker")
 | 
			
		||||
@ -69,6 +70,12 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
 | 
			
		||||
                "GF_AUTH_GENERIC_OAUTH_API_URL": (
 | 
			
		||||
                    self.url("passbook_providers_oauth2:userinfo")
 | 
			
		||||
                ),
 | 
			
		||||
                "GF_AUTH_SIGNOUT_REDIRECT_URL": (
 | 
			
		||||
                    self.url(
 | 
			
		||||
                        "passbook_providers_oauth2:end-session",
 | 
			
		||||
                        application_slug=APPLICATION_SLUG,
 | 
			
		||||
                    )
 | 
			
		||||
                ),
 | 
			
		||||
                "GF_LOG_LEVEL": "debug",
 | 
			
		||||
            },
 | 
			
		||||
        }
 | 
			
		||||
@ -97,7 +104,7 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
 | 
			
		||||
        )
 | 
			
		||||
        provider.save()
 | 
			
		||||
        Application.objects.create(
 | 
			
		||||
            name="Grafana", slug="grafana", provider=provider,
 | 
			
		||||
            name="Grafana", slug=APPLICATION_SLUG, provider=provider,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.driver.get("http://localhost:3000")
 | 
			
		||||
@ -137,7 +144,7 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
 | 
			
		||||
        )
 | 
			
		||||
        provider.save()
 | 
			
		||||
        Application.objects.create(
 | 
			
		||||
            name="Grafana", slug="grafana", provider=provider,
 | 
			
		||||
            name="Grafana", slug=APPLICATION_SLUG, provider=provider,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.driver.get("http://localhost:3000")
 | 
			
		||||
@ -171,6 +178,72 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
 | 
			
		||||
            USER().email,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_authorization_logout(self):
 | 
			
		||||
        """test OpenID Provider flow with logout"""
 | 
			
		||||
        sleep(1)
 | 
			
		||||
        # Bootstrap all needed objects
 | 
			
		||||
        authorization_flow = Flow.objects.get(
 | 
			
		||||
            slug="default-provider-authorization-implicit-consent"
 | 
			
		||||
        )
 | 
			
		||||
        provider = OAuth2Provider.objects.create(
 | 
			
		||||
            name="grafana",
 | 
			
		||||
            client_type=ClientTypes.CONFIDENTIAL,
 | 
			
		||||
            client_id=self.client_id,
 | 
			
		||||
            client_secret=self.client_secret,
 | 
			
		||||
            rsa_key=CertificateKeyPair.objects.first(),
 | 
			
		||||
            redirect_uris="http://localhost:3000/login/generic_oauth",
 | 
			
		||||
            authorization_flow=authorization_flow,
 | 
			
		||||
            response_type=ResponseTypes.CODE,
 | 
			
		||||
        )
 | 
			
		||||
        provider.property_mappings.set(
 | 
			
		||||
            ScopeMapping.objects.filter(
 | 
			
		||||
                scope_name__in=[SCOPE_OPENID, SCOPE_OPENID_EMAIL, SCOPE_OPENID_PROFILE]
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
        provider.save()
 | 
			
		||||
        Application.objects.create(
 | 
			
		||||
            name="Grafana", slug=APPLICATION_SLUG, provider=provider,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.driver.get("http://localhost:3000")
 | 
			
		||||
        self.driver.find_element(By.CLASS_NAME, "btn-service--oauth").click()
 | 
			
		||||
        self.driver.find_element(By.ID, "id_uid_field").click()
 | 
			
		||||
        self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username)
 | 
			
		||||
        self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
 | 
			
		||||
        self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
 | 
			
		||||
        self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
 | 
			
		||||
        self.driver.find_element(By.XPATH, "//a[contains(@href, '/profile')]").click()
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            self.driver.find_element(By.CLASS_NAME, "page-header__title").text,
 | 
			
		||||
            USER().name,
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            self.driver.find_element(By.CSS_SELECTOR, "input[name=name]").get_attribute(
 | 
			
		||||
                "value"
 | 
			
		||||
            ),
 | 
			
		||||
            USER().name,
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            self.driver.find_element(
 | 
			
		||||
                By.CSS_SELECTOR, "input[name=email]"
 | 
			
		||||
            ).get_attribute("value"),
 | 
			
		||||
            USER().email,
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            self.driver.find_element(
 | 
			
		||||
                By.CSS_SELECTOR, "input[name=login]"
 | 
			
		||||
            ).get_attribute("value"),
 | 
			
		||||
            USER().email,
 | 
			
		||||
        )
 | 
			
		||||
        self.driver.find_element(By.CSS_SELECTOR, "[href='/logout']").click()
 | 
			
		||||
        self.wait_for_url(
 | 
			
		||||
            self.url(
 | 
			
		||||
                "passbook_providers_oauth2:end-session",
 | 
			
		||||
                application_slug=APPLICATION_SLUG,
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
        self.driver.find_element(By.ID, "logout").click()
 | 
			
		||||
 | 
			
		||||
    def test_authorization_consent_explicit(self):
 | 
			
		||||
        """test OpenID Provider flow (default authorization flow with explicit consent)"""
 | 
			
		||||
        sleep(1)
 | 
			
		||||
@ -195,7 +268,7 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
 | 
			
		||||
        )
 | 
			
		||||
        provider.save()
 | 
			
		||||
        app = Application.objects.create(
 | 
			
		||||
            name="Grafana", slug="grafana", provider=provider,
 | 
			
		||||
            name="Grafana", slug=APPLICATION_SLUG, provider=provider,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.driver.get("http://localhost:3000")
 | 
			
		||||
@ -271,7 +344,7 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
 | 
			
		||||
        )
 | 
			
		||||
        provider.save()
 | 
			
		||||
        app = Application.objects.create(
 | 
			
		||||
            name="Grafana", slug="grafana", provider=provider,
 | 
			
		||||
            name="Grafana", slug=APPLICATION_SLUG, provider=provider,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        negative_policy = ExpressionPolicy.objects.create(
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,36 @@
 | 
			
		||||
{% extends 'login/base_full.html' %}
 | 
			
		||||
 | 
			
		||||
{% load static %}
 | 
			
		||||
{% load i18n %}
 | 
			
		||||
{% load passbook_utils %}
 | 
			
		||||
 | 
			
		||||
{% block title %}
 | 
			
		||||
{% trans 'End session' %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block card_title %}
 | 
			
		||||
{% blocktrans with application=application.name %}
 | 
			
		||||
You've logged out of {{ application }}.
 | 
			
		||||
{% endblocktrans %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block card %}
 | 
			
		||||
<form method="POST" class="pf-c-form">
 | 
			
		||||
    {% if message %}
 | 
			
		||||
    <h3>{% trans message %}</h3>
 | 
			
		||||
    {% endif %}
 | 
			
		||||
 | 
			
		||||
    <a id="pb-back-home" href="{% url 'passbook_core:overview' %}" class="pf-c-button pf-m-primary">{% trans 'Go back to passbook' %}</a>
 | 
			
		||||
 | 
			
		||||
    <a id="logout" href="{% url 'passbook_flows:default-invalidation' %}" class="pf-c-button pf-m-secondary">{% trans 'Log out of passbook' %}</a>
 | 
			
		||||
 | 
			
		||||
    {% if application.get_launch_url %}
 | 
			
		||||
    <a href="{{ application.get_launch_url }}" class="pf-c-button pf-m-secondary">
 | 
			
		||||
        {% blocktrans with application=application.name %}
 | 
			
		||||
            Log back into {{ application }}
 | 
			
		||||
        {% endblocktrans %}
 | 
			
		||||
    </a>
 | 
			
		||||
    {% endif %}
 | 
			
		||||
 | 
			
		||||
</form>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
@ -20,12 +20,16 @@ urlpatterns = [
 | 
			
		||||
        csrf_exempt(protected_resource_view([SCOPE_OPENID])(UserInfoView.as_view())),
 | 
			
		||||
        name="userinfo",
 | 
			
		||||
    ),
 | 
			
		||||
    path("end-session/", EndSessionView.as_view(), name="end-session",),
 | 
			
		||||
    path(
 | 
			
		||||
        "introspect/",
 | 
			
		||||
        csrf_exempt(TokenIntrospectionView.as_view()),
 | 
			
		||||
        name="token-introspection",
 | 
			
		||||
    ),
 | 
			
		||||
    path(
 | 
			
		||||
        "<slug:application_slug>/end-session/",
 | 
			
		||||
        EndSessionView.as_view(),
 | 
			
		||||
        name="end-session",
 | 
			
		||||
    ),
 | 
			
		||||
    path("<slug:application_slug>/jwks/", JWKSView.as_view(), name="jwks"),
 | 
			
		||||
    path(
 | 
			
		||||
        "<slug:application_slug>/.well-known/openid-configuration",
 | 
			
		||||
 | 
			
		||||
@ -32,7 +32,10 @@ class ProviderInfoView(View):
 | 
			
		||||
                reverse("passbook_providers_oauth2:userinfo")
 | 
			
		||||
            ),
 | 
			
		||||
            "end_session_endpoint": self.request.build_absolute_uri(
 | 
			
		||||
                reverse("passbook_providers_oauth2:end-session")
 | 
			
		||||
                reverse(
 | 
			
		||||
                    "passbook_providers_oauth2:end-session",
 | 
			
		||||
                    kwargs={"application_slug": provider.application.slug},
 | 
			
		||||
                )
 | 
			
		||||
            ),
 | 
			
		||||
            "introspection_endpoint": self.request.build_absolute_uri(
 | 
			
		||||
                reverse("passbook_providers_oauth2:token-introspection")
 | 
			
		||||
 | 
			
		||||
@ -1,45 +1,22 @@
 | 
			
		||||
"""passbook OAuth2 Session Views"""
 | 
			
		||||
from urllib.parse import parse_qs, urlencode, urlsplit, urlunsplit
 | 
			
		||||
from typing import Any, Dict
 | 
			
		||||
 | 
			
		||||
from django.contrib.auth.views import LogoutView
 | 
			
		||||
from django.http import HttpRequest, HttpResponse
 | 
			
		||||
from django.shortcuts import get_object_or_404
 | 
			
		||||
from django.views.generic.base import TemplateView
 | 
			
		||||
 | 
			
		||||
from passbook.core.models import Application
 | 
			
		||||
from passbook.providers.oauth2.models import OAuth2Provider
 | 
			
		||||
from passbook.providers.oauth2.utils import client_id_from_id_token
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class EndSessionView(LogoutView):
 | 
			
		||||
class EndSessionView(TemplateView):
 | 
			
		||||
    """Allow the client to end the Session"""
 | 
			
		||||
 | 
			
		||||
    def dispatch(
 | 
			
		||||
        self, request: HttpRequest, application_slug: str, *args, **kwargs
 | 
			
		||||
    ) -> HttpResponse:
 | 
			
		||||
    template_name = "providers/oauth2/end_session.html"
 | 
			
		||||
 | 
			
		||||
        application = get_object_or_404(Application, slug=application_slug)
 | 
			
		||||
        provider: OAuth2Provider = get_object_or_404(
 | 
			
		||||
            OAuth2Provider, pk=application.provider_id
 | 
			
		||||
    def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
 | 
			
		||||
        context = super().get_context_data(**kwargs)
 | 
			
		||||
 | 
			
		||||
        context["application"] = get_object_or_404(
 | 
			
		||||
            Application, slug=self.kwargs["application_slug"]
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        id_token_hint = request.GET.get("id_token_hint", "")
 | 
			
		||||
        post_logout_redirect_uri = request.GET.get("post_logout_redirect_uri", "")
 | 
			
		||||
        state = request.GET.get("state", "")
 | 
			
		||||
 | 
			
		||||
        if id_token_hint:
 | 
			
		||||
            client_id = client_id_from_id_token(id_token_hint)
 | 
			
		||||
            try:
 | 
			
		||||
                provider = OAuth2Provider.objects.get(client_id=client_id)
 | 
			
		||||
                if post_logout_redirect_uri in provider.post_logout_redirect_uris:
 | 
			
		||||
                    if state:
 | 
			
		||||
                        uri = urlsplit(post_logout_redirect_uri)
 | 
			
		||||
                        query_params = parse_qs(uri.query)
 | 
			
		||||
                        query_params["state"] = state
 | 
			
		||||
                        uri = uri._replace(query=urlencode(query_params, doseq=True))
 | 
			
		||||
                        self.next_page = urlunsplit(uri)
 | 
			
		||||
                    else:
 | 
			
		||||
                        self.next_page = post_logout_redirect_uri
 | 
			
		||||
            except OAuth2Provider.DoesNotExist:
 | 
			
		||||
                pass
 | 
			
		||||
 | 
			
		||||
        return super().dispatch(request, *args, **kwargs)
 | 
			
		||||
        return context
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user