This repository has been archived on 2024-05-31. You can view files and clone it, but cannot push or open issues or pull requests.
authentik/passbook/sources/oauth/views/core.py

291 lines
10 KiB
Python
Raw Normal View History

2018-11-22 12:12:24 +00:00
"""Core OAauth Views"""
from typing import Callable, Optional
2018-11-11 12:41:48 +00:00
from django.conf import settings
from django.contrib import messages
2019-04-29 21:22:54 +00:00
from django.contrib.auth import authenticate
2018-11-22 12:12:24 +00:00
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import Http404
from django.shortcuts import get_object_or_404, redirect, render
2018-11-11 12:41:48 +00:00
from django.urls import reverse
from django.utils.translation import ugettext as _
from django.views.generic import RedirectView, View
2019-10-01 08:24:10 +00:00
from structlog import get_logger
2018-11-11 12:41:48 +00:00
from passbook.audit.models import Event, EventAction
from passbook.flows.models import Flow, FlowDesignation
from passbook.flows.planner import (
PLAN_CONTEXT_PENDING_USER,
PLAN_CONTEXT_SSO,
FlowPlanner,
)
from passbook.flows.views import SESSION_KEY_PLAN
from passbook.lib.utils.urls import redirect_with_qs
2019-10-07 14:33:48 +00:00
from passbook.sources.oauth.clients import get_client
2019-12-31 11:51:16 +00:00
from passbook.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
2020-05-08 17:46:39 +00:00
from passbook.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
2018-11-11 12:41:48 +00:00
LOGGER = get_logger()
2018-11-11 12:41:48 +00:00
2018-12-18 12:24:26 +00:00
# pylint: disable=too-few-public-methods
2018-11-11 12:41:48 +00:00
class OAuthClientMixin:
"Mixin for getting OAuth client for a source."
client_class: Optional[Callable] = None
2018-11-11 12:41:48 +00:00
def get_client(self, source):
"Get instance of the OAuth client for this source."
if self.client_class is not None:
# pylint: disable=not-callable
return self.client_class(source)
return get_client(source)
class OAuthRedirect(OAuthClientMixin, RedirectView):
"Redirect user to OAuth source to enable access."
permanent = False
params = None
# pylint: disable=unused-argument
def get_additional_parameters(self, source):
"Return additional redirect parameters for this source."
return self.params or {}
def get_callback_url(self, source):
"Return the callback url for this source."
2019-12-31 11:51:16 +00:00
return reverse(
"passbook_sources_oauth:oauth-client-callback",
kwargs={"source_slug": source.slug},
)
2018-11-11 12:41:48 +00:00
def get_redirect_url(self, **kwargs):
"Build redirect url for a given source."
2019-12-31 11:51:16 +00:00
slug = kwargs.get("source_slug", "")
2018-11-11 12:41:48 +00:00
try:
source = OAuthSource.objects.get(slug=slug)
except OAuthSource.DoesNotExist:
raise Http404("Unknown OAuth source '%s'." % slug)
else:
if not source.enabled:
2019-12-31 11:51:16 +00:00
raise Http404("source %s is not enabled." % slug)
2018-11-11 12:41:48 +00:00
client = self.get_client(source)
callback = self.get_callback_url(source)
params = self.get_additional_parameters(source)
2019-12-31 11:51:16 +00:00
return client.get_redirect_url(
self.request, callback=callback, parameters=params
)
2018-11-11 12:41:48 +00:00
class OAuthCallback(OAuthClientMixin, View):
"Base OAuth callback view."
source_id = None
source = None
def get(self, request, *_, **kwargs):
2018-11-11 12:41:48 +00:00
"""View Get handler"""
2019-12-31 11:51:16 +00:00
slug = kwargs.get("source_slug", "")
2018-11-11 12:41:48 +00:00
try:
self.source = OAuthSource.objects.get(slug=slug)
except OAuthSource.DoesNotExist:
raise Http404("Unknown OAuth source '%s'." % slug)
else:
if not self.source.enabled:
2019-12-31 11:51:16 +00:00
raise Http404("source %s is not enabled." % slug)
2018-11-11 12:41:48 +00:00
client = self.get_client(self.source)
callback = self.get_callback_url(self.source)
# Fetch access token
token = client.get_access_token(self.request, callback=callback)
if token is None:
2019-12-31 11:51:16 +00:00
return self.handle_login_failure(
self.source, "Could not retrieve token."
)
if "error" in token:
return self.handle_login_failure(self.source, token["error"])
2018-11-11 12:41:48 +00:00
# Fetch profile info
info = client.get_profile_info(token)
2018-11-11 12:41:48 +00:00
if info is None:
2019-12-31 11:51:16 +00:00
return self.handle_login_failure(
self.source, "Could not retrieve profile."
)
2018-11-11 12:41:48 +00:00
identifier = self.get_user_id(self.source, info)
if identifier is None:
return self.handle_login_failure(self.source, "Could not determine id.")
# Get or create access record
defaults = {
"access_token": token.get("access_token"),
2018-11-11 12:41:48 +00:00
}
existing = UserOAuthSourceConnection.objects.filter(
2019-12-31 11:51:16 +00:00
source=self.source, identifier=identifier
)
2018-11-11 12:41:48 +00:00
if existing.exists():
connection = existing.first()
connection.access_token = token.get("access_token")
2019-12-31 11:51:16 +00:00
UserOAuthSourceConnection.objects.filter(pk=connection.pk).update(
**defaults
)
2018-11-11 12:41:48 +00:00
else:
connection = UserOAuthSourceConnection(
source=self.source,
identifier=identifier,
access_token=token.get("access_token"),
2018-11-11 12:41:48 +00:00
)
2019-12-31 11:51:16 +00:00
user = authenticate(
source=self.source, identifier=identifier, request=request
)
2018-11-11 12:41:48 +00:00
if user is None:
2020-02-18 20:35:58 +00:00
LOGGER.debug("Handling new user", source=self.source)
2018-12-18 12:24:26 +00:00
return self.handle_new_user(self.source, connection, info)
2020-02-18 20:35:58 +00:00
LOGGER.debug("Handling existing user", source=self.source)
2018-11-11 12:41:48 +00:00
return self.handle_existing_user(self.source, user, connection, info)
# pylint: disable=unused-argument
def get_callback_url(self, source):
"Return callback url if different than the current url."
return False
# pylint: disable=unused-argument
def get_error_redirect(self, source, reason):
"Return url to redirect on login failure."
return settings.LOGIN_URL
def get_or_create_user(self, source, access, info):
"Create a shell auth.User."
2018-12-09 16:44:54 +00:00
raise NotImplementedError()
2018-11-11 12:41:48 +00:00
# pylint: disable=unused-argument
def get_user_id(self, source, info):
"Return unique identifier from the profile info."
2019-12-31 11:51:16 +00:00
id_key = self.source_id or "id"
2018-11-11 12:41:48 +00:00
result = info
try:
2019-12-31 11:51:16 +00:00
for key in id_key.split("."):
2018-11-11 12:41:48 +00:00
result = result[key]
return result
except KeyError:
return None
def handle_login(self, user, source, access):
2020-05-08 17:46:39 +00:00
"""Prepare Authentication Plan, redirect user FlowExecutor"""
2019-12-31 11:51:16 +00:00
user = authenticate(
source=access.source, identifier=access.identifier, request=self.request
)
# We run the Flow planner here so we can pass the Pending user in the context
flow = get_object_or_404(Flow, designation=FlowDesignation.AUTHENTICATION)
planner = FlowPlanner(flow)
plan = planner.plan(
self.request,
{
PLAN_CONTEXT_PENDING_USER: user,
PLAN_CONTEXT_AUTHENTICATION_BACKEND: user.backend,
PLAN_CONTEXT_SSO: True,
},
)
self.request.session[SESSION_KEY_PLAN] = plan
return redirect_with_qs(
"passbook_flows:flow-executor", self.request.GET, flow_slug=flow.slug,
)
2018-11-11 12:41:48 +00:00
# pylint: disable=unused-argument
def handle_existing_user(self, source, user, access, info):
"Login user and redirect."
2019-12-31 11:51:16 +00:00
messages.success(
self.request,
_(
"Successfully authenticated with %(source)s!"
% {"source": self.source.name}
),
)
return self.handle_login(user, source, access)
2018-11-11 12:41:48 +00:00
def handle_login_failure(self, source, reason):
"Message user and redirect on error."
2020-02-18 20:35:58 +00:00
LOGGER.warning("Authentication Failure", reason=reason)
2019-12-31 11:51:16 +00:00
messages.error(self.request, _("Authentication Failed."))
2018-11-11 12:41:48 +00:00
return redirect(self.get_error_redirect(source, reason))
def handle_new_user(self, source, access, info):
"Create a shell auth.User and redirect."
2018-11-22 12:12:24 +00:00
was_authenticated = False
if self.request.user.is_authenticated:
2018-11-11 12:41:48 +00:00
# there's already a user logged in, just link them up
user = self.request.user
2018-11-22 12:12:24 +00:00
was_authenticated = True
2018-11-11 12:41:48 +00:00
else:
user = self.get_or_create_user(source, access, info)
2018-11-22 12:12:24 +00:00
access.user = user
access.save()
UserOAuthSourceConnection.objects.filter(pk=access.pk).update(user=user)
2019-12-31 11:51:16 +00:00
Event.new(
EventAction.CUSTOM, message="Linked OAuth Source", source=source
2019-12-31 11:51:16 +00:00
).from_http(self.request)
2018-11-22 12:12:24 +00:00
if was_authenticated:
2019-12-31 11:51:16 +00:00
messages.success(
self.request,
_("Successfully linked %(source)s!" % {"source": self.source.name}),
)
return redirect(
reverse(
"passbook_sources_oauth:oauth-client-user",
kwargs={"source_slug": self.source.slug},
)
)
# User was not authenticated, new user has been created
2019-12-31 11:51:16 +00:00
user = authenticate(
source=access.source, identifier=access.identifier, request=self.request
)
messages.success(
self.request,
_(
"Successfully authenticated with %(source)s!"
% {"source": self.source.name}
),
)
return self.handle_login(user, source, access)
2018-11-11 12:41:48 +00:00
2018-11-22 12:12:24 +00:00
class DisconnectView(LoginRequiredMixin, View):
2018-11-11 12:41:48 +00:00
"""Delete connection with source"""
2018-11-22 12:12:24 +00:00
source = None
aas = None
def dispatch(self, request, source_slug):
self.source = get_object_or_404(OAuthSource, slug=source_slug)
2019-12-31 11:51:16 +00:00
self.aas = get_object_or_404(
UserOAuthSourceConnection, source=self.source, user=request.user
)
return super().dispatch(request, source_slug)
2018-11-22 12:12:24 +00:00
def post(self, request, source_slug):
2018-11-22 12:12:24 +00:00
"""Delete connection object"""
2019-12-31 11:51:16 +00:00
if "confirmdelete" in request.POST:
2018-11-22 12:12:24 +00:00
# User confirmed deletion
self.aas.delete()
2019-12-31 11:51:16 +00:00
messages.success(request, _("Connection successfully deleted"))
return redirect(
reverse(
"passbook_sources_oauth:oauth-client-user",
kwargs={"source_slug": self.source.slug},
)
)
return self.get(request, source_slug)
2018-11-22 12:12:24 +00:00
# pylint: disable=unused-argument
def get(self, request, source_slug):
2018-11-22 12:12:24 +00:00
"""Show delete form"""
2019-12-31 11:51:16 +00:00
return render(
request,
"generic/delete.html",
{
"object": self.source,
"delete_url": reverse(
"passbook_sources_oauth:oauth-client-disconnect",
kwargs={"source_slug": self.source.slug,},
),
},
)