OAuth2 OpenID Connect provider support etc. #3

Merged
LeoVasanko merged 65 commits from oidconnect into main 2026-02-18 02:40:27 +00:00
9 changed files with 23 additions and 19 deletions
Showing only changes of commit 5750ae8e36 - Show all commits
+6 -1
View File
@@ -24,6 +24,11 @@ const terminatingSessions = ref({})
const hoveredCredentialUuid = ref(null)
const hoveredSession = ref(null)
// Convert credentials dict to array with uuid attached as 'credential'
const credentials = computed(() =>
Object.entries(props.userDetail?.credentials || {}).map(([uuid, c]) => ({ ...c, credential: uuid }))
)
// Template refs for navigation
const userInfoRef = ref(null)
const regActionsRef = ref(null)
@@ -218,7 +223,7 @@ defineExpose({ focusFirstElement })
<div class="section-body">
<CredentialList
ref="credentialListRef"
:credentials="userDetail.credentials ? Object.values(userDetail.credentials) : []"
:credentials="credentials"
:aaguid-info="userDetail.aaguid_info"
:allow-delete="true"
:hovered-credential-uuid="hoveredCredentialUuid"
+4 -2
View File
@@ -52,7 +52,7 @@
<div class="section-body">
<CredentialList
ref="credentialList"
:credentials="authStore.userInfo?.credentials ? Object.values(authStore.userInfo.credentials) : []"
:credentials="credentials"
:aaguid-info="authStore.userInfo?.aaguid_info || {}"
:loading="authStore.isLoading"
:hovered-credential-uuid="hoveredCredentialUuid"
@@ -369,7 +369,9 @@ const isAdmin = computed(() => {
return perms?.includes('auth:admin') || perms?.includes('auth:org:admin')
})
const hasMultipleSessions = computed(() => Object.keys(sessions.value).length > 1)
const credentials = computed(() => authStore.userInfo?.credentials ? Object.values(authStore.userInfo.credentials) : [])
const credentials = computed(() =>
Object.entries(authStore.userInfo?.credentials || {}).map(([uuid, c]) => ({ ...c, credential: uuid }))
)
const useWideLayout = computed(() => {
// Check if any single site has more than 8 sessions
const groups = {}
+2 -2
View File
@@ -11,8 +11,8 @@ import secrets
from datetime import UTC, datetime, timedelta
from uuid import UUID
import uuid7
import base64url
import uuid7
from paskia.config import SESSION_LIFETIME
from paskia.db.jsonl import (
@@ -474,7 +474,7 @@ def set_session_host(
def delete_session(
key: bytes, *, ctx: SessionContext | None = None, action: str = "delete_session"
key: str, *, ctx: SessionContext | None = None, action: str = "delete_session"
) -> None:
"""Delete a session.
+1 -1
View File
@@ -5,9 +5,9 @@ import secrets
from datetime import UTC, datetime
from uuid import UUID
import base64url
import msgspec
import uuid7
import base64url
from paskia import db
from paskia.util import hostutil
-1
View File
@@ -1,7 +1,6 @@
import logging
from uuid import UUID
import base64url
from fastapi import Body, FastAPI, HTTPException, Query, Request, Response
from fastapi.responses import JSONResponse
-1
View File
@@ -1,6 +1,5 @@
from uuid import UUID
import base64url
from fastapi import (
Body,
FastAPI,
+3 -5
View File
@@ -9,12 +9,10 @@ import asyncio
import logging
from uuid import UUID
import base64url
import httpx
from paskia import db
from paskia.util import oidjwt
from paskia.util.crypto import hash_secret
from paskia.util.hostutil import _load_config
_logger = logging.getLogger(__name__)
@@ -30,7 +28,7 @@ def _issuer() -> str:
def _collect_oidc_sessions(
session_keys: list[bytes],
session_keys: list[str],
) -> list[tuple[str, str, UUID, UUID | None]]:
"""Collect (backchannel_logout_uri, sid, client_uuid, user_uuid) for OIDC sessions.
@@ -46,7 +44,7 @@ def _collect_oidc_sessions(
client = data.oidc.clients.get(session.client_uuid)
if not client or not client.backchannel_logout_uri:
continue
sid = base64url.enc(hash_secret("oidc", session.key))
sid = session.key
notifications.append(
(client.backchannel_logout_uri, sid, session.client_uuid, session.user_uuid)
)
@@ -104,7 +102,7 @@ async def notify(
await asyncio.gather(*tasks, return_exceptions=True)
def schedule_notifications(session_keys: list[bytes]) -> None:
def schedule_notifications(session_keys: list[str]) -> None:
"""Collect OIDC info from sessions (before deletion) and schedule async notifications.
Must be called BEFORE the sessions are deleted. The actual HTTP requests
+4 -3
View File
@@ -19,6 +19,7 @@ from collections.abc import AsyncGenerator
from datetime import UTC, datetime, timedelta
from uuid import UUID
import base64url
import httpx
import pytest
import pytest_asyncio
@@ -251,10 +252,10 @@ def create_test_session(
ip: str = "127.0.0.1",
user_agent: str = "pytest",
duration: timedelta | None = None,
) -> tuple[bytes, str]:
) -> tuple[str, str]:
"""Create a test session. Returns (key, token) tuple.
- key: bytes used for session lookup (base64url encode for URLs)
- key: str used for session lookup (base64url encoded)
- token: stored in cookie/sent to client
"""
if duration is None:
@@ -267,7 +268,7 @@ def create_test_session(
# Generate token and derive key
token = secrets.token_urlsafe(12)
key = hash_secret("cookie", token)
key = base64url.enc(hash_secret("cookie", token))
session = Session.create(
user=user_uuid,
+3 -3
View File
@@ -1300,7 +1300,7 @@ class TestAdminSessions:
)
response = await client.delete(
f"/auth/api/admin/users/{test_user.uuid}/sessions/{base64url.enc(extra_db_key)}",
f"/auth/api/admin/users/{test_user.uuid}/sessions/{extra_db_key}",
headers={**auth_headers(session_token), "Host": "localhost:4401"},
)
assert response.status_code == 200
@@ -1364,9 +1364,9 @@ class TestAdminSessions:
f"/auth/api/admin/users/{test_user.uuid}/sessions/invalid!!id",
headers={**auth_headers(session_token), "Host": "localhost:4401"},
)
assert response.status_code == 400
assert response.status_code == 404
data = response.json()
assert "Invalid session ID format" in data["detail"]
assert "Session not found" in data["detail"]
@pytest.mark.asyncio
async def test_delete_session_not_found(