Identify sessions by session key - a hash that can be safely shared.
This commit is contained in:
@@ -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"
|
||||||
|
|||||||
@@ -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 = {}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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,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,6 +1,5 @@
|
|||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
import base64url
|
|
||||||
from fastapi import (
|
from fastapi import (
|
||||||
Body,
|
Body,
|
||||||
FastAPI,
|
FastAPI,
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user