Auf der Login-Seite gibt es „Mit Authelia anmelden“, wenn der Server dafür eingerichtet ist. Das eigene Konto wird einmal in den Einstellungen verknüpft, die Anmeldung mit Passwort bleibt. Fremde Authelia-Konten kommen nicht hinein, neue Konten entstehen darüber nicht. Fehlt einem Gerät der Bücher-Schlüssel (nach der Authelia-Anmeldung oder nach gelöschtem Speicher), erzeugt diora keinen neuen mehr, der die Bücher unlesbar machen würde. Stattdessen fragt es: Ein schon entsperrtes Gerät zeigt denselben Code und gibt den Schlüssel verschlüsselt frei, oder man gibt einmal das diora-Passwort ein. Der Server sieht den Schlüssel dabei nie. Details: docs/accounts.md Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
124 lines
5 KiB
Python
124 lines
5 KiB
Python
"""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')
|