Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
223429d51c |
+2
-2
@@ -14,12 +14,12 @@ For integrating Paskia with your app frontend, see [integration](Integration.md)
|
|||||||
|
|
||||||
### Public JSON API: /auth/api/*
|
### Public JSON API: /auth/api/*
|
||||||
|
|
||||||
| Method | Path | Used for | Responses |
|
| Method | Path | Used for | Expected responses |
|
||||||
|---:|---|---|---|
|
|---:|---|---|---|
|
||||||
| GET | /auth/api/settings | Paskia configuration: RP info, base paths, session cookie name | 200 |
|
| GET | /auth/api/settings | Paskia configuration: RP info, base paths, session cookie name | 200 |
|
||||||
| GET | /auth/api/user-info | Full user profile: info, credentials, sessions, permissions | 200/401 |
|
| GET | /auth/api/user-info | Full user profile: info, credentials, sessions, permissions | 200/401 |
|
||||||
| POST | /auth/api/logout | Terminate session and delete session cookie on the current host | 200 |
|
| POST | /auth/api/logout | Terminate session and delete session cookie on the current host | 200 |
|
||||||
| POST | [/auth/api/validate](api/validate.md) | Validate and renew the session cookie; query [perm](api/perm.md), [max_age](api/max-age.md) | 200/401/403 |
|
| POST | [/auth/api/validate](api/validate.md) | Validate and renew the session cookie; query [perm](api/perm.md), [max_age](api/max-age.md), [renew](api/validate.md#query-parameters) | 200/401/403 |
|
||||||
| GET | [/auth/api/forward](api/forward.md) | Forward-auth with reverse proxies; see [proxy guides](proxy/index.md), query [perm](api/perm.md), [max_age](api/max-age.md) | 204/401/403 empty, json or html|
|
| GET | [/auth/api/forward](api/forward.md) | Forward-auth with reverse proxies; see [proxy guides](proxy/index.md), query [perm](api/perm.md), [max_age](api/max-age.md) | 204/401/403 empty, json or html|
|
||||||
|
|
||||||
### User JSON API: /auth/api/user/*
|
### User JSON API: /auth/api/user/*
|
||||||
|
|||||||
+1
-1
@@ -122,7 +122,7 @@ This is useful for:
|
|||||||
- Background jobs that need to verify a stored session
|
- Background jobs that need to verify a stored session
|
||||||
- Check extra permissions, get user context or renew session
|
- Check extra permissions, get user context or renew session
|
||||||
|
|
||||||
Your backend can validate sessions directly by calling Paskia's validate endpoint [`/auth/api/validate`](api/validate.md). It generally expects client headers proxied as is, while on the URL you can specify exact requirements.
|
Your backend can validate sessions directly by calling Paskia's validate endpoint [`/auth/api/validate`](api/validate.md). It generally expects client headers proxied as is, while on the URL you can specify exact requirements. To verify a session without extending its lifetime or updating its IP / user-agent, pass `renew=0`.
|
||||||
|
|
||||||
Usually it is sufficient to simply forward the headers the client sent, assuming your proxy already preserved `Host` and set `X-Forwarded-For` (otherwise set them here with original host and IP). `User-Agent` should also be forwarded if available, omitted if not: do not let your backend HTTP client add its own header.
|
Usually it is sufficient to simply forward the headers the client sent, assuming your proxy already preserved `Host` and set `X-Forwarded-For` (otherwise set them here with original host and IP). `User-Agent` should also be forwarded if available, omitted if not: do not let your backend HTTP client add its own header.
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ See also the [API overview](../API.md) and the [integration guide](../Integratio
|
|||||||
|-----------|-------------|
|
|-----------|-------------|
|
||||||
| perm | Required permissions. See the [perm argument](perm.md). |
|
| perm | Required permissions. See the [perm argument](perm.md). |
|
||||||
| max_age | Require recent passkey use. See the [max_age argument](max-age.md). |
|
| max_age | Require recent passkey use. See the [max_age argument](max-age.md). |
|
||||||
|
| renew | Pass `renew=0` to skip renewal: does no session updates, auth check only. |
|
||||||
|
|
||||||
## Request headers
|
## Request headers
|
||||||
|
|
||||||
@@ -19,7 +20,7 @@ See also the [API overview](../API.md) and the [integration guide](../Integratio
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Host | Forwarded directly from the client | Verifying the session's bound host |
|
| Host | Forwarded directly from the client | Verifying the session's bound host |
|
||||||
| Cookie | Forwarded directly or just cookie `__Host-paskia` | Session ID |
|
| Cookie | Forwarded directly or just cookie `__Host-paskia` | Session ID |
|
||||||
| X-Forwarded-For | Real client IP | Recorded in session data instead of the backend/proxy IP; requires FORWARDED_ALLOW_IPS to trust the immediate peer |
|
| X-Forwarded-For | Real client IP | Recorded in logs and session data instead of the backend/proxy IP; requires FORWARDED_ALLOW_IPS to trust the immediate peer |
|
||||||
| User-Agent | Forward the original client UA if available; do not let your backend client add its own default | Recorded in session data only when the header is present; omitting it preserves the existing value |
|
| User-Agent | Forward the original client UA if available; do not let your backend client add its own default | Recorded in session data only when the header is present; omitting it preserves the existing value |
|
||||||
|
|
||||||
See [integration documentation for backend validate requests](../Integration.md) for more detailed instructions, in particular for forwarding of client-provided headers.
|
See [integration documentation for backend validate requests](../Integration.md) for more detailed instructions, in particular for forwarding of client-provided headers.
|
||||||
|
|||||||
@@ -103,9 +103,10 @@ async def validate_token(
|
|||||||
response: Response,
|
response: Response,
|
||||||
perm: list[str] = Query([]),
|
perm: list[str] = Query([]),
|
||||||
max_age: str | None = Query(None),
|
max_age: str | None = Query(None),
|
||||||
|
renew: bool = Query(True),
|
||||||
auth=AUTH_COOKIE,
|
auth=AUTH_COOKIE,
|
||||||
):
|
):
|
||||||
"""Validate session and return context. Refreshes session expiry."""
|
"""Validate session and return context. Refreshes session expiry by default."""
|
||||||
perm_groups = _parse_perm(perm)
|
perm_groups = _parse_perm(perm)
|
||||||
try:
|
try:
|
||||||
ctx = await authz.verify(
|
ctx = await authz.verify(
|
||||||
@@ -118,7 +119,7 @@ async def validate_token(
|
|||||||
# Global handler will clear cookie if 401
|
# Global handler will clear cookie if 401
|
||||||
raise
|
raise
|
||||||
renewed = False
|
renewed = False
|
||||||
if auth:
|
if auth and renew:
|
||||||
consumed = datetime.now(UTC) - ctx.session.validated
|
consumed = datetime.now(UTC) - ctx.session.validated
|
||||||
if not timedelta(0) < consumed < _REFRESH_INTERVAL:
|
if not timedelta(0) < consumed < _REFRESH_INTERVAL:
|
||||||
db.update_session(
|
db.update_session(
|
||||||
@@ -128,16 +129,18 @@ async def validate_token(
|
|||||||
validated=datetime.now(UTC),
|
validated=datetime.now(UTC),
|
||||||
ctx=ctx,
|
ctx=ctx,
|
||||||
)
|
)
|
||||||
session.set_session_cookie(response, auth)
|
|
||||||
renewed = True
|
renewed = True
|
||||||
_set_log_extra(request, ctx.session.key)
|
_set_log_extra(request, ctx.session.key)
|
||||||
return MsgspecResponse(
|
resp = MsgspecResponse(
|
||||||
ApiValidateResponse(
|
ApiValidateResponse(
|
||||||
valid=True,
|
valid=True,
|
||||||
renewed=renewed,
|
renewed=renewed,
|
||||||
ctx=userinfo.build_session_context(ctx),
|
ctx=userinfo.build_session_context(ctx),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
if renewed:
|
||||||
|
session.set_session_cookie(resp, auth)
|
||||||
|
return resp
|
||||||
|
|
||||||
|
|
||||||
@app.get("/check")
|
@app.get("/check")
|
||||||
|
|||||||
+53
-1
@@ -18,11 +18,13 @@ from uuid import UUID
|
|||||||
import httpx
|
import httpx
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from paskia import authcode
|
from paskia import authcode, db
|
||||||
from paskia.authsession import EXPIRES
|
from paskia.authsession import EXPIRES
|
||||||
from paskia.db import delete_session
|
from paskia.db import delete_session
|
||||||
from paskia.db.structs import Client
|
from paskia.db.structs import Client
|
||||||
|
from paskia.fastapi.api import _REFRESH_INTERVAL
|
||||||
from paskia.util import avatar, hostutil, oidjwt, permutil
|
from paskia.util import avatar, hostutil, oidjwt, permutil
|
||||||
|
from paskia.util.crypto import hash_secret
|
||||||
from paskia.util.passphrase import generate
|
from paskia.util.passphrase import generate
|
||||||
from tests.conftest import auth_headers, create_test_image_bytes, create_test_session
|
from tests.conftest import auth_headers, create_test_image_bytes, create_test_session
|
||||||
|
|
||||||
@@ -924,3 +926,53 @@ class TestValidateWithMaxAge:
|
|||||||
# This exercises the max_age path - but isn't defined in validate
|
# This exercises the max_age path - but isn't defined in validate
|
||||||
# Actually validate doesn't have max_age - this tests that unknown params are ignored
|
# Actually validate doesn't have max_age - this tests that unknown params are ignored
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
class TestValidateRenewParameter:
|
||||||
|
"""Tests for the renew query parameter on /auth/api/validate."""
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_validate_renew_false_skips_renewal(
|
||||||
|
self, client: httpx.AsyncClient, session_token: str, test_db
|
||||||
|
):
|
||||||
|
"""renew=0 should skip session renewal and leave metadata untouched."""
|
||||||
|
key = hash_secret("cookie", session_token)
|
||||||
|
old_validated = datetime.now(UTC) - _REFRESH_INTERVAL - timedelta(minutes=1)
|
||||||
|
db.update_session(key, validated=old_validated)
|
||||||
|
original_ua = test_db.sessions[key].user_agent
|
||||||
|
|
||||||
|
response = await client.post(
|
||||||
|
"/auth/api/validate?renew=0",
|
||||||
|
headers={
|
||||||
|
**auth_headers(session_token),
|
||||||
|
"Host": "localhost:4401",
|
||||||
|
"User-Agent": "different-ua",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["valid"] is True
|
||||||
|
assert data["renewed"] is False
|
||||||
|
assert "set-cookie" not in response.headers
|
||||||
|
assert test_db.sessions[key].validated == old_validated
|
||||||
|
assert test_db.sessions[key].user_agent == original_ua
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_validate_renew_true_renews_old_session(
|
||||||
|
self, client: httpx.AsyncClient, session_token: str, test_db
|
||||||
|
):
|
||||||
|
"""Explicit renew=1 should renew an aged session and return Set-Cookie."""
|
||||||
|
key = hash_secret("cookie", session_token)
|
||||||
|
old_validated = datetime.now(UTC) - _REFRESH_INTERVAL - timedelta(minutes=1)
|
||||||
|
db.update_session(key, validated=old_validated)
|
||||||
|
|
||||||
|
response = await client.post(
|
||||||
|
"/auth/api/validate?renew=1",
|
||||||
|
headers={**auth_headers(session_token), "Host": "localhost:4401"},
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["valid"] is True
|
||||||
|
assert data["renewed"] is True
|
||||||
|
assert "set-cookie" in response.headers
|
||||||
|
assert test_db.sessions[key].validated > old_validated
|
||||||
|
|||||||
Reference in New Issue
Block a user