diff --git a/frontend/src/admin/AdminUserDetail.vue b/frontend/src/admin/AdminUserDetail.vue
index c71a0b0..608afaf 100644
--- a/frontend/src/admin/AdminUserDetail.vue
+++ b/frontend/src/admin/AdminUserDetail.vue
@@ -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 })
{
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 = {}
diff --git a/paskia/db/operations.py b/paskia/db/operations.py
index d20eb30..b273009 100644
--- a/paskia/db/operations.py
+++ b/paskia/db/operations.py
@@ -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.
diff --git a/paskia/db/structs.py b/paskia/db/structs.py
index d02ff10..abdd9ec 100644
--- a/paskia/db/structs.py
+++ b/paskia/db/structs.py
@@ -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
diff --git a/paskia/fastapi/admin.py b/paskia/fastapi/admin.py
index 6dd90f0..5eafa59 100644
--- a/paskia/fastapi/admin.py
+++ b/paskia/fastapi/admin.py
@@ -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
diff --git a/paskia/fastapi/user.py b/paskia/fastapi/user.py
index 4730766..932cf88 100644
--- a/paskia/fastapi/user.py
+++ b/paskia/fastapi/user.py
@@ -1,6 +1,5 @@
from uuid import UUID
-import base64url
from fastapi import (
Body,
FastAPI,
diff --git a/paskia/oidc_notify.py b/paskia/oidc_notify.py
index ca8eb78..6eeba4a 100644
--- a/paskia/oidc_notify.py
+++ b/paskia/oidc_notify.py
@@ -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
diff --git a/tests/conftest.py b/tests/conftest.py
index 9cbdd73..78a450c 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -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,
diff --git a/tests/test_admin.py b/tests/test_admin.py
index 7ff8fc0..9cc8b41 100644
--- a/tests/test_admin.py
+++ b/tests/test_admin.py
@@ -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(