"""Sign-in with Authelia and linking it to an existing account (accounts/oidc.py).""" import logging from django.conf import settings from django.contrib import messages from django.contrib.auth import login from django.contrib.auth.decorators import login_required from django.shortcuts import redirect from django.utils import timezone from django.utils.http import url_has_allowed_host_and_scheme from django.views.decorators.http import require_http_methods from . import oidc from .models import OidcIdentity log = logging.getLogger(__name__) SESSION_KEY = 'oidc_flow' def _redirect_uri(request): return settings.OIDC_REDIRECT_URI or request.build_absolute_uri('/accounts/oidc/callback/') def _safe_next(request, value): if value and url_has_allowed_host_and_scheme(value, allowed_hosts={request.get_host()}, require_https=request.is_secure()): return value return '/' def oidc_context(request): """What the settings page shows about the provider.""" return { 'oidc_enabled': oidc.enabled(), 'oidc_provider': settings.OIDC_PROVIDER_NAME, 'oidc_identities': list(request.user.oidc_identities.all()) if request.user.is_authenticated else [], } @require_http_methods(['GET']) def oidc_start(request): """Off to the provider. `?link=1` (signed in) links the account instead of signing in.""" if not oidc.enabled(): messages.error(request, 'Die Anmeldung über Authelia ist auf diesem Server nicht eingerichtet.') return redirect('login') link = request.GET.get('link') == '1' if link and not request.user.is_authenticated: return redirect('login') flow = oidc.new_flow() flow['link'] = link flow['next'] = _safe_next(request, request.GET.get('next', '')) try: url = oidc.authorize_url(flow, _redirect_uri(request)) except oidc.OidcError as exc: log.warning('oidc start: %s', exc) messages.error(request, 'Authelia ist gerade nicht erreichbar.') return redirect('settings' if link else 'login') request.session[SESSION_KEY] = flow return redirect(url) @require_http_methods(['GET']) def oidc_callback(request): flow = request.session.pop(SESSION_KEY, None) fail_to = 'login' if flow and flow.get('link'): fail_to = 'settings' if not flow or request.GET.get('state') != flow.get('state'): messages.error(request, 'Die Anmeldung über Authelia ist abgelaufen. Bitte noch einmal.') return redirect(fail_to) if request.GET.get('error'): messages.error(request, 'Authelia hat die Anmeldung abgebrochen.') return redirect(fail_to) code = request.GET.get('code', '') try: claims = oidc.exchange_code(code, flow, _redirect_uri(request)) except oidc.OidcError as exc: log.warning('oidc callback: %s', exc) messages.error(request, 'Die Anmeldung über Authelia hat nicht geklappt.') return redirect(fail_to) issuer = settings.OIDC_ISSUER.rstrip('/') sub = claims['sub'] shown = { 'username': (claims.get('preferred_username') or '')[:150], 'email': (claims.get('email') or '')[:254], } identity = OidcIdentity.objects.filter(issuer=issuer, sub=sub).select_related('user').first() if flow.get('link'): if not request.user.is_authenticated: return redirect('login') if identity and identity.user_id != request.user.id: messages.error(request, 'Dieses Authelia-Konto ist schon mit einem anderen diora-Konto verknüpft.') return redirect('settings') if identity is None: OidcIdentity.objects.create(user=request.user, issuer=issuer, sub=sub, **shown) else: OidcIdentity.objects.filter(pk=identity.pk).update(**shown) messages.success(request, f'Authelia-Konto „{shown["username"] or sub}“ verknüpft. ' 'Du kannst dich jetzt auch darüber anmelden.') return redirect('settings') if identity is None: messages.error(request, 'Dieses Authelia-Konto ist mit keinem diora-Konto verknüpft. ' 'Melde dich einmal mit Passwort an und verknüpfe es in den Einstellungen.') return redirect('login') if not identity.user.is_active: messages.error(request, 'Dieses diora-Konto ist gesperrt.') return redirect('login') OidcIdentity.objects.filter(pk=identity.pk).update(last_login_at=timezone.now(), **shown) login(request, identity.user, backend='django.contrib.auth.backends.ModelBackend') # The page asks for the book key (static/js/app/33-unlock.js); a password login # brings it along, this one cannot. return redirect(flow.get('next') or '/') @login_required @require_http_methods(['POST']) def oidc_unlink(request, pk): OidcIdentity.objects.filter(user=request.user, pk=pk).delete() messages.success(request, 'Verknüpfung mit Authelia gelöst.') return redirect('settings')