Identify sessions by session key - a hash that can be safely shared.

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