diff --git a/passbook/providers/samlv2/saml/constants.py b/passbook/providers/samlv2/saml/constants.py index daab431f8..0b657dfe5 100644 --- a/passbook/providers/samlv2/saml/constants.py +++ b/passbook/providers/samlv2/saml/constants.py @@ -3,6 +3,11 @@ NS_SAML_PROTOCOL = "urn:oasis:names:tc:SAML:2.0:protocol" NS_SAML_ASSERTION = "urn:oasis:names:tc:SAML:2.0:assertion" NS_SIGNATURE = "http://www.w3.org/2000/09/xmldsig#" +REQ_KEY_REQUEST = "SAMLRequest" +REQ_KEY_SIGNATURE = "Signature" + +SESSION_KEY = "passbook_saml_request" + SAML_ATTRIB_ACS_URL = "AssertionConsumerServiceURL" SAML_ATTRIB_DESTINATION = "Destination" SAML_ATTRIB_ID = "ID" diff --git a/passbook/providers/samlv2/saml/provider.py b/passbook/providers/samlv2/saml/provider.py new file mode 100644 index 000000000..fef98d4e5 --- /dev/null +++ b/passbook/providers/samlv2/saml/provider.py @@ -0,0 +1,5 @@ +"""SAML Provider logic""" + + +class SAMLProvider: + """SAML Provider""" diff --git a/passbook/providers/samlv2/urls.py b/passbook/providers/samlv2/urls.py index 48315d8a4..2bd987d70 100644 --- a/passbook/providers/samlv2/urls.py +++ b/passbook/providers/samlv2/urls.py @@ -1,31 +1,34 @@ """passbook samlv2 URLs""" from django.urls import path -from passbook.providers.samlv2.views import idp_initiated, slo, sso +from passbook.providers.samlv2.views import authorize, idp_initiated, slo, sso urlpatterns = [ path( - "/sso/redirect/", + "/authorize/", + authorize.AuthorizeView.as_view(), + name="authorize", + ), + path( + "/sso/redirect/", sso.SAMLRedirectBindingView.as_view(), name="sso-redirect", ), path( - "/sso/post/", - sso.SAMLPostBindingView.as_view(), - name="sso-post", + "/sso/post/", sso.SAMLPostBindingView.as_view(), name="sso-post", ), path( - "/slo/redirect/", + "/slo/redirect/", slo.SAMLRedirectBindingView.as_view(), name="slo-redirect", ), path( - "/slo/redirect/", + "/slo/redirect/", slo.SAMLPostBindingView.as_view(), name="slo-post", ), path( - "/initiate/", + "/initiate/", idp_initiated.IDPInitiatedView.as_view(), name="initiate", ), diff --git a/passbook/providers/samlv2/views/authorize.py b/passbook/providers/samlv2/views/authorize.py new file mode 100644 index 000000000..9b000d00b --- /dev/null +++ b/passbook/providers/samlv2/views/authorize.py @@ -0,0 +1,6 @@ +"""SAML Provider authorization view""" +from django.views.generic import FormView + + +class AuthorizeView(FormView): + """Authorization view""" diff --git a/passbook/providers/samlv2/views/base.py b/passbook/providers/samlv2/views/base.py new file mode 100644 index 000000000..6bea319bc --- /dev/null +++ b/passbook/providers/samlv2/views/base.py @@ -0,0 +1,31 @@ +"""SAML base views""" +from typing import Optional + +from django.http import HttpRequest, HttpResponse +from django.shortcuts import get_object_or_404 +from django.views import View + +from passbook.core.models import Application +from passbook.core.views.access import AccessMixin +from passbook.providers.samlv2.saml.constants import SESSION_KEY +from passbook.providers.samlv2.saml.parser import SAMLRequest + + +class BaseSAMLView(AccessMixin, View): + """Base SAML View to resolve app_slug""" + + application: Application + + def setup(self, request: HttpRequest, *args, **kwargs): + View.setup(self, request, *args, **kwargs) + self.application = self.get_application(self.kwargs.get("app_slug")) + + def get_application(self, app_slug: str) -> Optional[Application]: + """Return application or raise 404""" + return get_object_or_404(Application, slug=app_slug) + + def handle_saml_request(self, request: SAMLRequest) -> HttpResponse: + """Handle SAML Request""" + self.request.SESSION[SESSION_KEY] = request + if self.application.skip_authorization: + pass diff --git a/passbook/providers/samlv2/views/sso.py b/passbook/providers/samlv2/views/sso.py index b5502b14f..1c563a881 100644 --- a/passbook/providers/samlv2/views/sso.py +++ b/passbook/providers/samlv2/views/sso.py @@ -1,10 +1,41 @@ """Single Signon Views""" -from django.views import View +from django.http import HttpRequest, HttpResponse, HttpResponseBadRequest + +from passbook.providers.samlv2.saml.constants import REQ_KEY_REQUEST, REQ_KEY_SIGNATURE +from passbook.providers.samlv2.saml.parser import SAMLRequest +from passbook.providers.samlv2.views.base import BaseSAMLView + +# SAML Authentication flow in passbook +# - Parse and Verify SAML Request +# - Check access to application (this is done after parsing as it might take a few seconds) +# - Ask for user authorization (if required from Application) +# - Log Access to audit log +# - Create response with unique ID to protect against replay -class SAMLPostBindingView(View): +class SAMLPostBindingView(BaseSAMLView): """Handle SAML POST-type Requests""" + # pylint: disable=unused-argument + def post(self, request: HttpRequest, app_slug: str) -> HttpResponse: + """Handle POST Requests""" + if REQ_KEY_REQUEST not in request.POST: + return HttpResponseBadRequest() + raw_saml_request = request.POST.get(REQ_KEY_REQUEST) + detached_signature = request.POST.get(REQ_KEY_SIGNATURE, None) + srq = SAMLRequest.parse(raw_saml_request, detached_signature) + return self.handle_saml_request(srq) -class SAMLRedirectBindingView(View): + +class SAMLRedirectBindingView(BaseSAMLView): """Handle SAML Redirect-type Requests""" + + # pylint: disable=unused-argument + def get(self, request: HttpRequest, app_slug: str) -> HttpResponse: + """Handle GET Requests""" + if REQ_KEY_REQUEST not in request.GET: + return HttpResponseBadRequest() + raw_saml_request = request.GET.get(REQ_KEY_REQUEST) + detached_signature = request.GET.get(REQ_KEY_SIGNATURE, None) + srq = SAMLRequest.parse(raw_saml_request, detached_signature) + return self.handle_saml_request(srq)