diff --git a/passbook/policies/mixins.py b/passbook/policies/views.py
similarity index 60%
rename from passbook/policies/mixins.py
rename to passbook/policies/views.py
index 8de3d1bba..a3f8480b2 100644
--- a/passbook/policies/mixins.py
+++ b/passbook/policies/views.py
@@ -1,11 +1,12 @@
 """passbook access helper classes"""
-from typing import Optional
+from typing import Any, Optional
 
 from django.contrib import messages
 from django.contrib.auth.mixins import AccessMixin
 from django.contrib.auth.views import redirect_to_login
 from django.http import HttpRequest, HttpResponse
 from django.utils.translation import gettext as _
+from django.views.generic.base import View
 from structlog import get_logger
 
 from passbook.core.models import Application, Provider, User
@@ -23,16 +24,40 @@ class BaseMixin:
     request: HttpRequest
 
 
-class PolicyAccessMixin(BaseMixin, AccessMixin):
+class PolicyAccessView(AccessMixin, View):
     """Mixin class for usage in Authorization views.
     Provider functions to check application access, etc"""
 
-    def handle_no_permission(self, application: Optional[Application] = None):
+    provider: Provider
+    application: Application
+
+    def resolve_provider_application(self):
+        """Resolve self.provider and self.application. *.DoesNotExist Exceptions cause a normal
+        AccessDenied view to be shown. An Http404 exception
+        is not caught, and will return directly"""
+        raise NotImplementedError
+
+    def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
+        try:
+            self.resolve_provider_application()
+        except (Application.DoesNotExist, Provider.DoesNotExist):
+            return self.handle_no_permission_authenticated()
+        # Check if user is unauthenticated, so we pass the application
+        # for the identification stage
+        if not request.user.is_authenticated:
+            return self.handle_no_permission()
+        # Check permissions
+        result = self.user_has_access()
+        if not result.passing:
+            return self.handle_no_permission_authenticated(result)
+        return super().dispatch(request, *args, **kwargs)
+
+    def handle_no_permission(self) -> HttpResponse:
         """User has no access and is not authenticated, so we remember the application
         they try to access and redirect to the login URL. The application is saved to show
         a hint on the Identification Stage what the user should login for."""
-        if application:
-            self.request.session[SESSION_KEY_APPLICATION_PRE] = application
+        if self.application:
+            self.request.session[SESSION_KEY_APPLICATION_PRE] = self.application
         return redirect_to_login(
             self.request.get_full_path(),
             self.get_login_url(),
@@ -48,34 +73,18 @@ class PolicyAccessMixin(BaseMixin, AccessMixin):
             response.policy_result = result
         return response
 
-    def provider_to_application(self, provider: Provider) -> Application:
-        """Lookup application assigned to provider, throw error if no application assigned"""
-        try:
-            return provider.application
-        except Application.DoesNotExist as exc:
-            messages.error(
-                self.request,
-                _(
-                    'Provider "%(name)s" has no application assigned'
-                    % {"name": provider}
-                ),
-            )
-            raise exc
-
-    def user_has_access(
-        self, application: Application, user: Optional[User] = None
-    ) -> PolicyResult:
+    def user_has_access(self, user: Optional[User] = None) -> PolicyResult:
         """Check if user has access to application."""
         user = user or self.request.user
         policy_engine = PolicyEngine(
-            application, user or self.request.user, self.request
+            self.application, user or self.request.user, self.request
         )
         policy_engine.build()
         result = policy_engine.result
         LOGGER.debug(
             "AccessMixin user_has_access",
             user=user,
-            app=application,
+            app=self.application,
             result=result,
         )
         if not result.passing:
diff --git a/passbook/providers/oauth2/views/authorize.py b/passbook/providers/oauth2/views/authorize.py
index 312864d4e..4a482985d 100644
--- a/passbook/providers/oauth2/views/authorize.py
+++ b/passbook/providers/oauth2/views/authorize.py
@@ -7,7 +7,6 @@ from uuid import uuid4
 from django.http import HttpRequest, HttpResponse
 from django.shortcuts import get_object_or_404, redirect
 from django.utils import timezone
-from django.views import View
 from structlog import get_logger
 
 from passbook.audit.models import Event, EventAction
@@ -24,7 +23,7 @@ from passbook.flows.views import SESSION_KEY_PLAN
 from passbook.lib.utils.time import timedelta_from_string
 from passbook.lib.utils.urls import redirect_with_qs
 from passbook.lib.views import bad_request_message
-from passbook.policies.mixins import PolicyAccessMixin
+from passbook.policies.views import PolicyAccessView
 from passbook.providers.oauth2.constants import (
     PROMPT_CONSNET,
     PROMPT_NONE,
@@ -329,28 +328,17 @@ class OAuthFulfillmentStage(StageView):
         return urlunsplit(uri)
 
 
-class AuthorizationFlowInitView(PolicyAccessMixin, View):
+class AuthorizationFlowInitView(PolicyAccessView):
     """OAuth2 Flow initializer, checks access to application and starts flow"""
 
+    def resolve_provider_application(self):
+        client_id = self.request.GET.get("client_id")
+        self.provider = get_object_or_404(OAuth2Provider, client_id=client_id)
+        self.application = self.provider.application
+
     # pylint: disable=unused-argument
     def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
         """Check access to application, start FlowPLanner, return to flow executor shell"""
-        client_id = request.GET.get("client_id")
-        # TODO: This whole block should be moved to a base class
-        provider = get_object_or_404(OAuth2Provider, client_id=client_id)
-        try:
-            application = self.provider_to_application(provider)
-        except Application.DoesNotExist:
-            return self.handle_no_permission_authenticated()
-        # Check if user is unauthenticated, so we pass the application
-        # for the identification stage
-        if not request.user.is_authenticated:
-            return self.handle_no_permission(application)
-        # Check permissions
-        result = self.user_has_access(application)
-        if not result.passing:
-            return self.handle_no_permission_authenticated(result)
-        # TODO: End block
         # Extract params so we can save them in the plan context
         try:
             params = OAuthAuthorizationParams.from_request(request)
@@ -358,14 +346,14 @@ class AuthorizationFlowInitView(PolicyAccessMixin, View):
             # pylint: disable=no-member
             return bad_request_message(request, error.description, title=error.error)
         # Regardless, we start the planner and return to it
-        planner = FlowPlanner(provider.authorization_flow)
+        planner = FlowPlanner(self.provider.authorization_flow)
         # planner.use_cache = False
         planner.allow_empty_flows = True
         plan: FlowPlan = planner.plan(
             self.request,
             {
                 PLAN_CONTEXT_SSO: True,
-                PLAN_CONTEXT_APPLICATION: application,
+                PLAN_CONTEXT_APPLICATION: self.application,
                 # OAuth2 related params
                 PLAN_CONTEXT_PARAMS: params,
                 PLAN_CONTEXT_SCOPE_DESCRIPTIONS: UserInfoView().get_scope_descriptions(
@@ -390,5 +378,5 @@ class AuthorizationFlowInitView(PolicyAccessMixin, View):
         return redirect_with_qs(
             "passbook_flows:flow-executor-shell",
             self.request.GET,
-            flow_slug=provider.authorization_flow.slug,
+            flow_slug=self.provider.authorization_flow.slug,
         )
diff --git a/passbook/providers/saml/views.py b/passbook/providers/saml/views.py
index 37450b623..9f7ae4658 100644
--- a/passbook/providers/saml/views.py
+++ b/passbook/providers/saml/views.py
@@ -23,7 +23,7 @@ from passbook.flows.stage import StageView
 from passbook.flows.views import SESSION_KEY_PLAN
 from passbook.lib.utils.urls import redirect_with_qs
 from passbook.lib.views import bad_request_message
-from passbook.policies.mixins import PolicyAccessMixin
+from passbook.policies.views import PolicyAccessView
 from passbook.providers.saml.exceptions import CannotHandleAssertion
 from passbook.providers.saml.models import SAMLBindings, SAMLProvider
 from passbook.providers.saml.processors.assertion import AssertionProcessor
@@ -46,34 +46,35 @@ REQUEST_KEY_RELAY_STATE = "RelayState"
 SESSION_KEY_AUTH_N_REQUEST = "authn_request"
 
 
-class SAMLSSOView(PolicyAccessMixin, View):
+class SAMLSSOView(PolicyAccessView):
     """ "SAML SSO Base View, which plans a flow and injects our final stage.
     Calls get/post handler."""
 
-    application: Application
-    provider: SAMLProvider
-
-    def dispatch(
-        self, request: HttpRequest, *args, application_slug: str, **kwargs
-    ) -> HttpResponse:
-        self.application = get_object_or_404(Application, slug=application_slug)
+    def resolve_provider_application(self):
+        self.application = get_object_or_404(
+            Application, slug=self.kwargs["application_slug"]
+        )
         self.provider: SAMLProvider = get_object_or_404(
             SAMLProvider, pk=self.application.provider_id
         )
-        if not request.user.is_authenticated:
-            return self.handle_no_permission(self.application)
-        has_access = self.user_has_access(self.application)
-        if not has_access.passing:
-            return self.handle_no_permission_authenticated(has_access)
-        # Call the method handler, which checks the SAML Request
-        method_response = super().dispatch(request, *args, application_slug, **kwargs)
+
+    def check_saml_request(self) -> Optional[HttpRequest]:
+        """Handler to verify the SAML Request. Must be implemented by a subclass"""
+        raise NotImplementedError
+
+    # pylint: disable=unused-argument
+    def get(self, request: HttpRequest, application_slug: str) -> HttpResponse:
+        """Verify the SAML Request, and if valid initiate the FlowPlanner for the application"""
+        # Call the method handler, which checks the SAML
+        # Request and returns a HTTP Response on error
+        method_response = self.check_saml_request()
         if method_response:
             return method_response
         # Regardless, we start the planner and return to it
         planner = FlowPlanner(self.provider.authorization_flow)
         planner.allow_empty_flows = True
         plan = planner.plan(
-            self.request,
+            request,
             {
                 PLAN_CONTEXT_SSO: True,
                 PLAN_CONTEXT_APPLICATION: self.application,
@@ -81,23 +82,25 @@ class SAMLSSOView(PolicyAccessMixin, View):
             },
         )
         plan.append(in_memory_stage(SAMLFlowFinalView))
-        self.request.session[SESSION_KEY_PLAN] = plan
+        request.session[SESSION_KEY_PLAN] = plan
         return redirect_with_qs(
             "passbook_flows:flow-executor-shell",
-            self.request.GET,
+            request.GET,
             flow_slug=self.provider.authorization_flow.slug,
         )
 
+    def post(self, request: HttpRequest, application_slug: str) -> HttpResponse:
+        """GET and POST use the same handler, but we can't
+        override .dispatch easily because PolicyAccessView's dispatch"""
+        return self.get(request, application_slug)
+
 
 class SAMLSSOBindingRedirectView(SAMLSSOView):
     """SAML Handler for SSO/Redirect bindings, which are sent via GET"""
 
-    # pylint: disable=unused-argument
-    def get(  # lgtm [py/similar-function]
-        self, request: HttpRequest, application_slug: str
-    ) -> Optional[HttpResponse]:
+    def check_saml_request(self) -> Optional[HttpRequest]:
         """Handle REDIRECT bindings"""
-        if REQUEST_KEY_SAML_REQUEST not in request.GET:
+        if REQUEST_KEY_SAML_REQUEST not in self.request.GET:
             LOGGER.info("handle_saml_request: SAML payload missing")
             return bad_request_message(
                 self.request, "The SAML request payload is missing."
@@ -105,10 +108,10 @@ class SAMLSSOBindingRedirectView(SAMLSSOView):
 
         try:
             auth_n_request = AuthNRequestParser(self.provider).parse_detached(
-                request.GET[REQUEST_KEY_SAML_REQUEST],
-                request.GET.get(REQUEST_KEY_RELAY_STATE),
-                request.GET.get(REQUEST_KEY_SAML_SIGNATURE),
-                request.GET.get(REQUEST_KEY_SAML_SIG_ALG),
+                self.request.GET[REQUEST_KEY_SAML_REQUEST],
+                self.request.GET.get(REQUEST_KEY_RELAY_STATE),
+                self.request.GET.get(REQUEST_KEY_SAML_SIGNATURE),
+                self.request.GET.get(REQUEST_KEY_SAML_SIG_ALG),
             )
             self.request.session[SESSION_KEY_AUTH_N_REQUEST] = auth_n_request
         except CannotHandleAssertion as exc:
@@ -121,12 +124,9 @@ class SAMLSSOBindingRedirectView(SAMLSSOView):
 class SAMLSSOBindingPOSTView(SAMLSSOView):
     """SAML Handler for SSO/POST bindings"""
 
-    # pylint: disable=unused-argument
-    def post(
-        self, request: HttpRequest, application_slug: str
-    ) -> Optional[HttpResponse]:
+    def check_saml_request(self) -> Optional[HttpRequest]:
         """Handle POST bindings"""
-        if REQUEST_KEY_SAML_REQUEST not in request.POST:
+        if REQUEST_KEY_SAML_REQUEST not in self.request.POST:
             LOGGER.info("handle_saml_request: SAML payload missing")
             return bad_request_message(
                 self.request, "The SAML request payload is missing."
@@ -134,8 +134,8 @@ class SAMLSSOBindingPOSTView(SAMLSSOView):
 
         try:
             auth_n_request = AuthNRequestParser(self.provider).parse(
-                request.POST[REQUEST_KEY_SAML_REQUEST],
-                request.POST.get(REQUEST_KEY_RELAY_STATE),
+                self.request.POST[REQUEST_KEY_SAML_REQUEST],
+                self.request.POST.get(REQUEST_KEY_RELAY_STATE),
             )
             self.request.session[SESSION_KEY_AUTH_N_REQUEST] = auth_n_request
         except CannotHandleAssertion as exc:
@@ -147,10 +147,7 @@ class SAMLSSOBindingPOSTView(SAMLSSOView):
 class SAMLSSOBindingInitView(SAMLSSOView):
     """SAML Handler for for IdP Initiated login flows"""
 
-    # pylint: disable=unused-argument
-    def get(
-        self, request: HttpRequest, application_slug: str
-    ) -> Optional[HttpResponse]:
+    def check_saml_request(self) -> Optional[HttpRequest]:
         """Create SAML Response from scratch"""
         LOGGER.debug(
             "handle_saml_no_request: No SAML Request, using IdP-initiated flow."
diff --git a/swagger.yaml b/swagger.yaml
index 6bfd6a28c..66f0eafd0 100755
--- a/swagger.yaml
+++ b/swagger.yaml
@@ -122,7 +122,7 @@ paths:
   /core/applications/:
     get:
       operationId: core_applications_list
-      description: Application Viewset
+      description: Custom list method that checks Policy based access instead of guardian
       parameters:
         - name: ordering
           in: query
@@ -186,7 +186,7 @@ paths:
       tags:
         - core
     parameters: []
-  /core/applications/{pbm_uuid}/:
+  /core/applications/{slug}/:
     get:
       operationId: core_applications_read
       description: Application Viewset
@@ -240,12 +240,13 @@ paths:
       tags:
         - core
     parameters:
-      - name: pbm_uuid
+      - name: slug
         in: path
-        description: A UUID string identifying this Application.
+        description: Internal application name, used in URLs.
         required: true
         type: string
-        format: uuid
+        format: slug
+        pattern: ^[-a-zA-Z0-9_]+$
   /core/groups/:
     get:
       operationId: core_groups_list