mirror of
https://github.com/suitenumerique/meet.git
synced 2026-08-13 04:06:56 +00:00
🔒️(backend) secure native app OIDC login with exchange codes
Add one-time exchange code mechanism for native app OIDC login. Instead of exposing session ID in redirect URL, generates a short-lived single-use code stored in Redis. Native apps exchange this code for the session ID via a dedicated API endpoint. Includes NativeAppRedirect for custom URL schemes, rate limiting, logging, and whitelist of allowed schemes. Closes #1153 Co-Authored-By: gigi206
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
"""Custom OIDC authentication views for native app support.
|
||||
|
||||
When a native app (iOS, Android, Desktop) initiates OIDC login, it sets
|
||||
`returnTo` to a custom URL scheme (e.g. `visio://auth-callback`). After
|
||||
the OIDC flow completes, Django sets the session cookie and redirects to
|
||||
that URL. However, native apps cannot read browser cookies — they need
|
||||
the session ID passed explicitly.
|
||||
|
||||
Instead of exposing the session ID directly in the redirect URL, this
|
||||
module generates a short-lived, single-use exchange code. The native app
|
||||
then exchanges this code for the session ID via a dedicated API endpoint.
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from urllib.parse import urlencode, urlparse
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.http import HttpResponseRedirect
|
||||
|
||||
from lasuite.oidc_login.views import (
|
||||
OIDCAuthenticationCallbackView as BaseCallbackView,
|
||||
OIDCAuthenticationRequestView as BaseRequestView,
|
||||
)
|
||||
|
||||
# Cache key prefix and TTL for exchange codes
|
||||
EXCHANGE_CODE_PREFIX = "auth_exchange:"
|
||||
EXCHANGE_CODE_TTL = 30 # seconds
|
||||
|
||||
|
||||
class NativeAppRedirect(HttpResponseRedirect):
|
||||
"""HttpResponseRedirect subclass that allows native app custom URL schemes.
|
||||
|
||||
Django's HttpResponseRedirect only allows http, https, and ftp schemes.
|
||||
Native apps use custom schemes (e.g. visio://) for deep links, which
|
||||
Django rejects with DisallowedRedirect. This subclass extends
|
||||
allowed_schemes with the configured native app schemes.
|
||||
"""
|
||||
|
||||
allowed_schemes = HttpResponseRedirect.allowed_schemes + list(
|
||||
getattr(settings, "NATIVE_APP_REDIRECT_SCHEMES", [])
|
||||
)
|
||||
|
||||
|
||||
class OIDCAuthenticationRequestView(BaseRequestView):
|
||||
"""Custom authenticate view that preserves native app returnTo in session.
|
||||
|
||||
mozilla-django-oidc's get_next_url() rejects custom URL schemes
|
||||
(e.g. visio://auth-callback) because url_has_allowed_host_and_scheme()
|
||||
only allows http/https. We intercept the returnTo parameter and store
|
||||
it directly in the session for whitelisted schemes, bypassing the
|
||||
safety check (which is not relevant for native app deep links).
|
||||
"""
|
||||
|
||||
def get(self, request):
|
||||
redirect_field = getattr(settings, "OIDC_REDIRECT_FIELD_NAME", "returnTo")
|
||||
return_to = request.GET.get(redirect_field, "")
|
||||
parsed = urlparse(return_to)
|
||||
allowed_schemes = getattr(settings, "NATIVE_APP_REDIRECT_SCHEMES", [])
|
||||
|
||||
response = super().get(request)
|
||||
|
||||
# Override oidc_login_next AFTER super() which set it to None
|
||||
# via get_next_url() rejecting the custom scheme.
|
||||
if parsed.scheme in allowed_schemes:
|
||||
request.session["oidc_login_next"] = return_to
|
||||
request.session.save()
|
||||
|
||||
return response
|
||||
|
||||
|
||||
class OIDCAuthenticationCallbackView(BaseCallbackView):
|
||||
"""Callback view that generates an exchange code for native app deep links."""
|
||||
|
||||
def login_success(self):
|
||||
"""After successful login, append exchange code for whitelisted scheme redirects."""
|
||||
# Temporarily remove native redirect from session to prevent
|
||||
# super().login_success() from raising DisallowedRedirect when
|
||||
# it tries HttpResponseRedirect with a custom scheme.
|
||||
native_redirect = self.request.session.pop("oidc_login_next", None)
|
||||
allowed_schemes = getattr(settings, "NATIVE_APP_REDIRECT_SCHEMES", [])
|
||||
parsed = urlparse(native_redirect or "")
|
||||
|
||||
if native_redirect and parsed.scheme in allowed_schemes:
|
||||
# Let super() redirect to the default URL (homepage)
|
||||
super().login_success()
|
||||
|
||||
# Generate a short-lived, single-use exchange code
|
||||
exchange_code = uuid.uuid4().hex
|
||||
session_key = self.request.session.session_key
|
||||
cache.set(
|
||||
f"{EXCHANGE_CODE_PREFIX}{exchange_code}",
|
||||
session_key,
|
||||
EXCHANGE_CODE_TTL,
|
||||
)
|
||||
|
||||
separator = "&" if parsed.query else "?"
|
||||
new_url = (
|
||||
f"{native_redirect}{separator}{urlencode({'code': exchange_code})}"
|
||||
)
|
||||
return NativeAppRedirect(new_url)
|
||||
|
||||
return super().login_success()
|
||||
Reference in New Issue
Block a user