ФУНКЦИИ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: устойчивый global logout и auth fallback

This commit is contained in:
DCCONSTRUCTIONS
2026-05-05 11:54:29 +03:00
parent ed18a07154
commit 4400b7f438
5 changed files with 324 additions and 19 deletions
@@ -6,6 +6,7 @@ from urllib.parse import urlencode
import jwt
import requests
from django.core.cache import cache
from django.http import HttpResponseRedirect
from django.utils import timezone
from django.views import View
@@ -53,6 +54,7 @@ class NodeDCOIDCInitiateEndpoint(View):
if request.GET.get("prompt") == "login":
params["prompt"] = "login"
params["max_age"] = "0"
return HttpResponseRedirect(f"{discovery['authorization_endpoint']}?{urlencode(params)}")
@@ -65,17 +67,28 @@ class NodeDCOIDCCallbackEndpoint(View):
base_url = base_host(request=request, is_app=True)
if request.GET.get("error"):
return oidc_error_redirect(base_url, next_path, "oidc_provider_error")
request.session.pop(OIDC_SESSION_KEY, None)
return oidc_login_redirect(base_url, next_path)
state = request.GET.get("state")
code = request.GET.get("code")
if not state or state != oidc_session.get("state") or not code:
return oidc_error_redirect(base_url, next_path, "oidc_state_failed")
request.session.pop(OIDC_SESSION_KEY, None)
return oidc_login_redirect(base_url, next_path)
try:
discovery = load_discovery(config["issuer"])
token_set = exchange_code(discovery, config, code, oidc_session.get("code_verifier"))
claims = verify_id_token(discovery, config, token_set["id_token"], oidc_session.get("nonce"))
except (KeyError, RuntimeError, requests.RequestException, jwt.PyJWTError):
request.session.pop(OIDC_SESSION_KEY, None)
return oidc_login_redirect(base_url, next_path)
if is_logout_guard_active(claims):
request.session.pop(OIDC_SESSION_KEY, None)
return oidc_login_redirect(base_url, next_path, prompt_login=True)
discovery = load_discovery(config["issuer"])
token_set = exchange_code(discovery, config, code, oidc_session.get("code_verifier"))
claims = verify_id_token(discovery, config, token_set["id_token"], oidc_session.get("nonce"))
groups = normalize_groups(claims.get("groups"))
if not has_required_group(groups):
@@ -260,5 +273,87 @@ def first_string_claim(claims, *keys):
return None
def normalize_logout_guard_value(value):
return value.strip().lower() if isinstance(value, str) else ""
def get_logout_guard_cache_key(kind, value):
normalized_value = normalize_logout_guard_value(value)
return f"nodedc:logout-guard:{kind}:{normalized_value}" if normalized_value else ""
def get_logout_guard_cache_keys(claims):
keys = set()
subject_key = get_logout_guard_cache_key("subject", claims.get("sub"))
email_key = get_logout_guard_cache_key("email", claims.get("email"))
if subject_key:
keys.add(subject_key)
if email_key:
keys.add(email_key)
return sorted(keys)
def get_claim_auth_time(claims):
auth_time = claims.get("auth_time")
try:
return int(auth_time)
except (TypeError, ValueError):
return 0
def get_logout_guard_time(claims):
guard_times = []
for cache_key in get_logout_guard_cache_keys(claims):
value = cache.get(cache_key)
try:
guard_times.append(int(value))
except (TypeError, ValueError):
pass
return max(guard_times) if guard_times else 0
def clear_logout_guard(claims):
cache_keys = get_logout_guard_cache_keys(claims)
if cache_keys:
cache.delete_many(cache_keys)
def is_logout_guard_active(claims):
logout_guard_time = get_logout_guard_time(claims)
if not logout_guard_time:
return False
auth_time = get_claim_auth_time(claims)
if auth_time > logout_guard_time:
clear_logout_guard(claims)
return False
return True
def oidc_error_redirect(base_url, next_path, error_code):
return HttpResponseRedirect(get_safe_redirect_url(base_url=base_url, next_path=next_path, params={"error": error_code}))
def oidc_login_redirect(base_url, next_path, prompt_login=False):
params = {}
if validate_next_path(next_path):
params["next_path"] = next_path
if prompt_login:
params["prompt"] = "login"
query_string = f"?{urlencode(params)}" if params else ""
return HttpResponseRedirect(f"{base_url.rstrip('/')}/auth/oidc/login/{query_string}")