OAuth2 OpenID Connect provider support etc. #3
@@ -53,7 +53,7 @@ function handleAuthenticated(result) {
|
||||
postToParent({
|
||||
type: 'auth-success',
|
||||
authenticated: true,
|
||||
sessionToken: result.session_token
|
||||
exchangeCode: result.exchange_code
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -144,7 +144,7 @@ async function registerPasskey() {
|
||||
}
|
||||
|
||||
try {
|
||||
await setSessionCookie(result)
|
||||
await exchangeCode(result)
|
||||
} catch (error) {
|
||||
loading.value = false
|
||||
const message = error?.message || 'Failed to establish session'
|
||||
@@ -156,15 +156,13 @@ async function registerPasskey() {
|
||||
setTimeout(() => { loading.value = false; goHome() }, 800)
|
||||
}
|
||||
|
||||
async function setSessionCookie(result) {
|
||||
if (!result?.session_token) {
|
||||
throw new Error('Registration response missing session_token')
|
||||
async function exchangeCode(result) {
|
||||
if (!result?.exchange_code) {
|
||||
throw new Error('Registration response missing exchange_code')
|
||||
}
|
||||
return await apiJson('/auth/api/set-session', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${result.session_token}`
|
||||
}
|
||||
headers: { 'Authorization': `Bearer ${result.exchange_code}` }
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -196,7 +196,7 @@ async function startRemoteAuth() {
|
||||
} else if (msg.status === 'authenticated') {
|
||||
// Success
|
||||
completed.value = true
|
||||
emit('authenticated', { session_token: msg.session_token })
|
||||
emit('authenticated', { exchange_code: msg.exchange_code })
|
||||
break
|
||||
} else if (msg.status === 'denied') {
|
||||
// Explicitly denied by the authenticating device
|
||||
|
||||
@@ -181,7 +181,7 @@ async function authenticateUser() {
|
||||
emit('authenticated', result)
|
||||
return
|
||||
}
|
||||
try { await setSessionCookie(result) } catch (error) {
|
||||
try { await exchangeCode(result) } catch (error) {
|
||||
loading.value = false
|
||||
const message = error?.message || 'Failed to establish session'
|
||||
showMessage(message, 'error', 4000)
|
||||
@@ -212,13 +212,13 @@ function openProfile() {
|
||||
if (profileWindow) profileWindow.focus()
|
||||
}
|
||||
|
||||
async function setSessionCookie(result) {
|
||||
if (!result?.session_token) {
|
||||
console.error('setSessionCookie called with missing session_token:', result)
|
||||
throw new Error('Authentication response missing session_token')
|
||||
async function exchangeCode(result) {
|
||||
if (!result?.exchange_code) {
|
||||
console.error('exchangeCode called with missing exchange_code:', result)
|
||||
throw new Error('Authentication response missing exchange_code')
|
||||
}
|
||||
return await fetchJson('/auth/api/set-session', {
|
||||
method: 'POST', headers: { Authorization: `Bearer ${result.session_token}` }
|
||||
method: 'POST', headers: { 'Authorization': `Bearer ${result.exchange_code}` }
|
||||
})
|
||||
}
|
||||
|
||||
@@ -233,7 +233,7 @@ function switchToLocal() {
|
||||
async function handleRemoteAuthenticated(result) {
|
||||
showMessage('Authenticated from another device!', 'success', 2000)
|
||||
try {
|
||||
await setSessionCookie(result)
|
||||
await exchangeCode(result)
|
||||
} catch (error) {
|
||||
const message = error?.message || 'Failed to establish session'
|
||||
showMessage(message, 'error', 4000)
|
||||
|
||||
@@ -41,21 +41,21 @@ export const useAuthStore = defineStore('auth', {
|
||||
}, effectiveDuration)
|
||||
}
|
||||
},
|
||||
async setSessionCookie(result) {
|
||||
if (!result?.session_token) {
|
||||
console.error('setSessionCookie called with missing session_token:', result)
|
||||
throw new Error('Authentication response missing session_token')
|
||||
async exchangeCode(result) {
|
||||
if (!result?.exchange_code) {
|
||||
console.error('exchangeCode called with missing exchange_code:', result)
|
||||
throw new Error('Authentication response missing exchange_code')
|
||||
}
|
||||
return await apiJson('/auth/api/set-session', {
|
||||
method: 'POST',
|
||||
headers: {'Authorization': `Bearer ${result.session_token}`},
|
||||
headers: { 'Authorization': `Bearer ${result.exchange_code}` },
|
||||
})
|
||||
},
|
||||
async register() {
|
||||
this.isLoading = true
|
||||
try {
|
||||
const result = await register()
|
||||
await this.setSessionCookie(result)
|
||||
await this.exchangeCode(result)
|
||||
await this.loadUserInfo()
|
||||
this.selectView()
|
||||
return result
|
||||
@@ -68,7 +68,7 @@ export const useAuthStore = defineStore('auth', {
|
||||
try {
|
||||
const result = await authenticate()
|
||||
|
||||
await this.setSessionCookie(result)
|
||||
await this.exchangeCode(result)
|
||||
await this.loadUserInfo()
|
||||
this.selectView()
|
||||
|
||||
|
||||
+44
-23
@@ -1,8 +1,8 @@
|
||||
"""
|
||||
OIDC authorization code management.
|
||||
Authorization code management for OIDC and cookie exchange flows.
|
||||
|
||||
Authorization codes are short-lived (60 seconds) and stored in-memory only.
|
||||
Similar to remote auth, these are not persisted to the database.
|
||||
Codes are short-lived (60 seconds) and stored in-memory only.
|
||||
Two separate stores maintain full isolation between OIDC and cookie flows.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -20,26 +20,30 @@ _logger = logging.getLogger(__name__)
|
||||
AUTH_CODE_LIFETIME = timedelta(seconds=60)
|
||||
|
||||
|
||||
class AuthCode(msgspec.Struct):
|
||||
"""A pending authorization code for OIDC or native auth."""
|
||||
class OIDCCode(msgspec.Struct):
|
||||
"""An OIDC authorization code pending token exchange.
|
||||
|
||||
PKCE uses S256 only (verified at auth time).
|
||||
"""
|
||||
|
||||
session_key: str
|
||||
created: datetime
|
||||
oidc: OIDC | None = None # OIDC-specific fields (None for native auth)
|
||||
|
||||
|
||||
class OIDC(msgspec.Struct):
|
||||
"""OIDC verification data carried authenticate->token that is not stored in Session."""
|
||||
|
||||
redirect_uri: str
|
||||
scope: str
|
||||
nonce: str
|
||||
code_challenge: str
|
||||
code_challenge_method: str
|
||||
|
||||
|
||||
# Public interface - auth codes in-memory store
|
||||
codes: dict[str, AuthCode] = {}
|
||||
class CookieCode(msgspec.Struct):
|
||||
"""A cookie exchange code for setting session cookie after WebSocket auth."""
|
||||
|
||||
session_key: str
|
||||
created: datetime
|
||||
|
||||
|
||||
# Separate stores for each code type
|
||||
oidc_codes: dict[str, OIDCCode] = {}
|
||||
cookie_codes: dict[str, CookieCode] = {}
|
||||
|
||||
# Background cleanup task
|
||||
_cleanup_task: asyncio.Task | None = None
|
||||
@@ -77,16 +81,33 @@ async def _cleanup_loop():
|
||||
|
||||
def _cleanup_expired():
|
||||
oldest = datetime.now(UTC) - AUTH_CODE_LIFETIME
|
||||
for code, auth_code in list(codes.items()):
|
||||
for code, auth_code in list(oidc_codes.items()):
|
||||
if auth_code.created < oldest:
|
||||
del codes[code]
|
||||
del oidc_codes[code]
|
||||
for code, auth_code in list(cookie_codes.items()):
|
||||
if auth_code.created < oldest:
|
||||
del cookie_codes[code]
|
||||
|
||||
|
||||
def store(auth_code: AuthCode) -> str:
|
||||
"""Store an authorization code and return the code string.
|
||||
def store_oidc(code: OIDCCode) -> str:
|
||||
"""Store an OIDC authorization code and return the code string."""
|
||||
token = secrets.token_urlsafe(12)
|
||||
oidc_codes[token] = code
|
||||
return token
|
||||
|
||||
Caller must construct AuthCode with their own timestamp.
|
||||
"""
|
||||
code = secrets.token_urlsafe(12)
|
||||
codes[code] = auth_code
|
||||
return code
|
||||
|
||||
def consume_oidc(token: str) -> OIDCCode | None:
|
||||
"""Consume an OIDC code, returning it if valid. Atomic removal."""
|
||||
return oidc_codes.pop(token, None)
|
||||
|
||||
|
||||
def store_cookie(code: CookieCode) -> str:
|
||||
"""Store a cookie exchange code and return the code string."""
|
||||
token = secrets.token_urlsafe(12)
|
||||
cookie_codes[token] = code
|
||||
return token
|
||||
|
||||
|
||||
def consume_cookie(token: str) -> CookieCode | None:
|
||||
"""Consume a cookie exchange code, returning it if valid. Atomic removal."""
|
||||
return cookie_codes.pop(token, None)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import logging
|
||||
import secrets
|
||||
from uuid import UUID
|
||||
|
||||
import base64url
|
||||
@@ -12,6 +13,7 @@ from paskia.db import Org as OrgDC
|
||||
from paskia.db import Permission as PermDC
|
||||
from paskia.db import Role as RoleDC
|
||||
from paskia.db import User as UserDC
|
||||
from paskia.db.structs import OIDClient
|
||||
from paskia.fastapi import authz
|
||||
from paskia.fastapi.response import MsgspecResponse
|
||||
from paskia.fastapi.session import AUTH_COOKIE
|
||||
@@ -919,3 +921,121 @@ async def admin_delete_permission(
|
||||
|
||||
db.delete_permission(permission_uuid, ctx=ctx)
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
# -------------------- OIDC Clients --------------------
|
||||
|
||||
|
||||
@app.get("/oidc-clients")
|
||||
async def admin_list_oidc_clients(request: Request, auth=AUTH_COOKIE):
|
||||
"""List all OIDC clients (master admin only)."""
|
||||
ctx = await authz.verify(
|
||||
auth,
|
||||
["auth:admin"],
|
||||
host=request.headers.get("host"),
|
||||
match=permutil.has_all,
|
||||
)
|
||||
if not master_admin(ctx):
|
||||
raise authz.AuthException(
|
||||
status_code=403,
|
||||
detail="Only master admin can manage OIDC clients",
|
||||
mode="forbidden",
|
||||
)
|
||||
|
||||
clients = db.data().oid_clients.values()
|
||||
return MsgspecResponse(
|
||||
[
|
||||
{
|
||||
"uuid": str(client.uuid),
|
||||
"name": client.name,
|
||||
"redirect_uris": client.redirect_uris,
|
||||
"created_at": format_datetime(client.created_at),
|
||||
}
|
||||
for client in clients
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@app.post("/oidc-clients")
|
||||
async def admin_create_oidc_client(
|
||||
request: Request,
|
||||
payload: dict = Body(...),
|
||||
auth=AUTH_COOKIE,
|
||||
):
|
||||
"""Create a new OIDC client (master admin only)."""
|
||||
ctx = await authz.verify(
|
||||
auth,
|
||||
["auth:admin"],
|
||||
host=request.headers.get("host"),
|
||||
match=permutil.has_all,
|
||||
max_age="5m",
|
||||
)
|
||||
if not master_admin(ctx):
|
||||
raise authz.AuthException(
|
||||
status_code=403,
|
||||
detail="Only master admin can manage OIDC clients",
|
||||
mode="forbidden",
|
||||
)
|
||||
|
||||
name = payload.get("name", "").strip()
|
||||
redirect_uris = payload.get("redirect_uris", [])
|
||||
|
||||
if not name:
|
||||
raise ValueError("Client name is required")
|
||||
if not redirect_uris:
|
||||
raise ValueError("At least one redirect URI is required")
|
||||
if not isinstance(redirect_uris, list):
|
||||
raise ValueError("redirect_uris must be a list")
|
||||
|
||||
# Validate redirect URIs
|
||||
for uri in redirect_uris:
|
||||
if not isinstance(uri, str) or not uri.startswith("http"):
|
||||
raise ValueError(f"Invalid redirect URI: {uri}")
|
||||
|
||||
# Generate a secure client secret
|
||||
client_secret = secrets.token_urlsafe(32)
|
||||
|
||||
# Create the client
|
||||
client, _ = OIDClient.create(
|
||||
name=name,
|
||||
redirect_uris=redirect_uris,
|
||||
client_secret=client_secret,
|
||||
)
|
||||
|
||||
db.create_oid_client(client, ctx=ctx)
|
||||
|
||||
return {
|
||||
"status": "ok",
|
||||
"client_id": str(client.uuid),
|
||||
"client_secret": client_secret,
|
||||
"message": "Save the client_secret now - it cannot be retrieved later",
|
||||
}
|
||||
|
||||
|
||||
@app.delete("/oidc-clients/{client_uuid}")
|
||||
async def admin_delete_oidc_client(
|
||||
client_uuid: UUID,
|
||||
request: Request,
|
||||
auth=AUTH_COOKIE,
|
||||
):
|
||||
"""Delete an OIDC client (master admin only)."""
|
||||
ctx = await authz.verify(
|
||||
auth,
|
||||
["auth:admin"],
|
||||
host=request.headers.get("host"),
|
||||
match=permutil.has_all,
|
||||
max_age="5m",
|
||||
)
|
||||
if not master_admin(ctx):
|
||||
raise authz.AuthException(
|
||||
status_code=403,
|
||||
detail="Only master admin can manage OIDC clients",
|
||||
mode="forbidden",
|
||||
)
|
||||
|
||||
try:
|
||||
db.delete_oid_client(client_uuid, ctx=ctx)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
|
||||
return {"status": "ok"}
|
||||
|
||||
+24
-37
@@ -3,7 +3,6 @@ from contextlib import suppress
|
||||
from datetime import UTC, datetime, timedelta
|
||||
|
||||
from fastapi import (
|
||||
Body,
|
||||
Depends,
|
||||
FastAPI,
|
||||
HTTPException,
|
||||
@@ -68,34 +67,6 @@ async def general_exception_handler(
|
||||
return JSONResponse(status_code=500, content={"detail": "Internal server error"})
|
||||
|
||||
|
||||
@app.post("/exchange")
|
||||
async def exchange_code(
|
||||
request: Request,
|
||||
response: Response,
|
||||
code: str = Body(..., embed=True),
|
||||
):
|
||||
"""Exchange a session code for setting the session cookie.
|
||||
|
||||
Called by frontend after WebSocket authentication.
|
||||
The code is ephemeral (60s TTL) and can only be used once.
|
||||
"""
|
||||
auth_code = authcode.codes.pop(code, None)
|
||||
if not auth_code:
|
||||
raise HTTPException(status_code=400, detail="Invalid or expired code")
|
||||
|
||||
secret = auth_code.session_key
|
||||
|
||||
# Verify the session exists
|
||||
host = hostutil.normalize_host(request.headers.get("host", ""))
|
||||
ctx = db.data().session_ctx(secret, host)
|
||||
if not ctx:
|
||||
raise HTTPException(status_code=400, detail="Session not found")
|
||||
|
||||
# Set the session cookie
|
||||
session.set_session_cookie(response, secret)
|
||||
return {"status": "ok", "user": str(ctx.user.uuid)}
|
||||
|
||||
|
||||
@app.post("/validate")
|
||||
async def validate_token(
|
||||
request: Request,
|
||||
@@ -294,13 +265,29 @@ async def api_logout(request: Request, response: Response, auth=AUTH_COOKIE):
|
||||
async def api_set_session(
|
||||
request: Request, response: Response, auth=Depends(bearer_auth)
|
||||
):
|
||||
"""Exchange an auth code for setting the session cookie.
|
||||
|
||||
Called by frontend after WebSocket authentication.
|
||||
The code is ephemeral (60s TTL) and can only be used once.
|
||||
"""
|
||||
if not auth or not auth.credentials:
|
||||
raise HTTPException(401, "Bearer token required")
|
||||
ctx = db.data().session_ctx(auth.credentials, request.headers.get("host"))
|
||||
raise HTTPException(400, "Bearer token required")
|
||||
|
||||
# Verify host is provided
|
||||
host = hostutil.normalize_host(request.headers.get("host", ""))
|
||||
if not host:
|
||||
raise HTTPException(400, "Host header required")
|
||||
|
||||
a = authcode.consume_cookie(auth.credentials)
|
||||
if not a:
|
||||
raise HTTPException(401, "Code expired or already used")
|
||||
|
||||
secret = a.session_key
|
||||
|
||||
# Verify the session exists
|
||||
ctx = db.data().session_ctx(secret, host)
|
||||
if not ctx:
|
||||
raise HTTPException(401, "Session expired")
|
||||
session.set_session_cookie(response, auth.credentials)
|
||||
return {
|
||||
"message": "Session cookie set successfully",
|
||||
"user": str(ctx.user.uuid),
|
||||
}
|
||||
raise HTTPException(401, f"Session not found on {host}")
|
||||
|
||||
session.set_session_cookie(response, secret)
|
||||
return {"status": "ok", "user": str(ctx.user.uuid)}
|
||||
|
||||
+25
-35
@@ -165,15 +165,15 @@ async def _handle_authorization_code(
|
||||
)
|
||||
|
||||
# Consume auth code (atomic delete + return)
|
||||
auth_code = authcode.codes.pop(code, None)
|
||||
if not auth_code:
|
||||
oidc_code = authcode.consume_oidc(code)
|
||||
if not oidc_code:
|
||||
return JSONResponse(
|
||||
{"error": "invalid_grant", "error_description": "Code expired or invalid"},
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
# Look up the OIDC session by token
|
||||
session = _oidc_session_by_token(auth_code.session_key, client.uuid)
|
||||
session = _oidc_session_by_token(oidc_code.session_key, client.uuid)
|
||||
if not session:
|
||||
return JSONResponse(
|
||||
{
|
||||
@@ -183,40 +183,30 @@ async def _handle_authorization_code(
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
# Verify redirect_uri matches (OIDC only)
|
||||
if auth_code.oidc and redirect_uri and redirect_uri != auth_code.oidc.redirect_uri:
|
||||
# Verify redirect_uri matches
|
||||
if redirect_uri and redirect_uri != oidc_code.redirect_uri:
|
||||
return JSONResponse(
|
||||
{"error": "invalid_grant", "error_description": "redirect_uri mismatch"},
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
# Verify PKCE (OIDC only)
|
||||
if auth_code.oidc:
|
||||
if not code_verifier:
|
||||
return JSONResponse(
|
||||
{
|
||||
"error": "invalid_grant",
|
||||
"error_description": "Missing code_verifier",
|
||||
},
|
||||
status_code=400,
|
||||
)
|
||||
method = auth_code.oidc.code_challenge_method
|
||||
if method != "S256":
|
||||
return JSONResponse(
|
||||
{
|
||||
"error": "invalid_grant",
|
||||
"error_description": "Only S256 code_challenge_method is supported",
|
||||
},
|
||||
status_code=400,
|
||||
)
|
||||
if not _verify_pkce(code_verifier, auth_code.oidc.code_challenge):
|
||||
return JSONResponse(
|
||||
{
|
||||
"error": "invalid_grant",
|
||||
"error_description": "Invalid code_verifier",
|
||||
},
|
||||
status_code=400,
|
||||
)
|
||||
# Verify PKCE (S256 only, enforced at auth time)
|
||||
if not code_verifier:
|
||||
return JSONResponse(
|
||||
{
|
||||
"error": "invalid_grant",
|
||||
"error_description": "Missing code_verifier",
|
||||
},
|
||||
status_code=400,
|
||||
)
|
||||
if not _verify_pkce(code_verifier, oidc_code.code_challenge):
|
||||
return JSONResponse(
|
||||
{
|
||||
"error": "invalid_grant",
|
||||
"error_description": "Invalid code_verifier",
|
||||
},
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
# Get user from session
|
||||
user = db.data().users.get(session.user_uuid)
|
||||
@@ -233,10 +223,10 @@ async def _handle_authorization_code(
|
||||
request,
|
||||
user,
|
||||
client_id,
|
||||
auth_code.session_key,
|
||||
oidc_code.session_key,
|
||||
sid,
|
||||
auth_code.oidc.nonce if auth_code.oidc else None,
|
||||
auth_code.oidc.scope if auth_code.oidc else None,
|
||||
oidc_code.nonce,
|
||||
oidc_code.scope,
|
||||
credential_uuid=session.credential_uuid,
|
||||
)
|
||||
|
||||
|
||||
@@ -10,12 +10,14 @@ Endpoints:
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from datetime import UTC, datetime
|
||||
from uuid import UUID
|
||||
|
||||
import base64url
|
||||
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
||||
|
||||
from paskia import db, remoteauth
|
||||
from paskia import authcode, db, remoteauth
|
||||
from paskia.authcode import CookieCode
|
||||
from paskia.authsession import expires
|
||||
from paskia.fastapi.session import AUTH_COOKIE, infodict
|
||||
from paskia.fastapi.wschat import authenticate_and_login
|
||||
@@ -183,7 +185,7 @@ async def websocket_remote_auth_request(ws: WebSocket):
|
||||
"user": str(result_data["user_uuid"]),
|
||||
}
|
||||
if result_data.get("session_token"):
|
||||
response["session_token"] = result_data["session_token"]
|
||||
response["exchange_code"] = result_data["session_token"]
|
||||
if result_data.get("reset_token"):
|
||||
response["reset_token"] = result_data["reset_token"]
|
||||
await ws.send_json(response)
|
||||
@@ -310,7 +312,7 @@ async def websocket_remote_auth_permit(ws: WebSocket, auth=AUTH_COOKIE):
|
||||
|
||||
# Handle authenticate request (no PoW needed - already validated during lookup)
|
||||
if msg.get("authenticate") and request is not None:
|
||||
ctx, session_token = await authenticate_and_login(ws, auth)
|
||||
ctx, secret = await authenticate_and_login(ws, auth)
|
||||
|
||||
reset_token = None
|
||||
|
||||
@@ -324,11 +326,19 @@ async def websocket_remote_auth_permit(ws: WebSocket, auth=AUTH_COOKIE):
|
||||
user=str(ctx.user.uuid),
|
||||
)
|
||||
|
||||
# Create exchange code for the session (don't expose raw secret)
|
||||
exchange_code = authcode.store_cookie(
|
||||
CookieCode(
|
||||
session_key=secret,
|
||||
created=datetime.now(UTC),
|
||||
)
|
||||
)
|
||||
|
||||
# Complete the remote auth request (notifies the waiting device)
|
||||
cred = db.data().credentials[ctx.session.credential_uuid]
|
||||
completed = await remoteauth.instance.complete_request(
|
||||
token=request.key,
|
||||
session_token=session_token,
|
||||
session_token=exchange_code,
|
||||
user_uuid=ctx.user.uuid,
|
||||
credential_uuid=cred.uuid,
|
||||
reset_token=reset_token,
|
||||
|
||||
+9
-12
@@ -6,7 +6,7 @@ from uuid import UUID
|
||||
from fastapi import FastAPI, WebSocket
|
||||
|
||||
from paskia import authcode, db
|
||||
from paskia.authcode import OIDC, AuthCode
|
||||
from paskia.authcode import CookieCode, OIDCCode
|
||||
from paskia.authsession import get_reset
|
||||
from paskia.config import SESSION_LIFETIME
|
||||
from paskia.db.structs import Session
|
||||
@@ -213,18 +213,15 @@ async def websocket_authenticate(
|
||||
sign_count=new_sign_count,
|
||||
)
|
||||
# Create auth code (in-memory only)
|
||||
auth_code = AuthCode(
|
||||
oidc_code = OIDCCode(
|
||||
session_key=token,
|
||||
created=now,
|
||||
oidc=OIDC(
|
||||
redirect_uri=redirect_uri,
|
||||
scope=scope,
|
||||
nonce=nonce,
|
||||
code_challenge=code_challenge,
|
||||
code_challenge_method=code_challenge_method or "S256",
|
||||
),
|
||||
redirect_uri=redirect_uri,
|
||||
scope=scope,
|
||||
nonce=nonce,
|
||||
code_challenge=code_challenge,
|
||||
)
|
||||
code = authcode.store(auth_code)
|
||||
code = authcode.store_oidc(oidc_code)
|
||||
|
||||
# Build redirect URL
|
||||
params = {"code": code}
|
||||
@@ -243,11 +240,11 @@ async def websocket_authenticate(
|
||||
|
||||
# Create exchange code (ephemeral, 60s TTL)
|
||||
now = datetime.now(UTC)
|
||||
auth_code = AuthCode(
|
||||
cookie_code = CookieCode(
|
||||
session_key=secret,
|
||||
created=now,
|
||||
)
|
||||
exchange_code = authcode.store(auth_code)
|
||||
exchange_code = authcode.store_cookie(cookie_code)
|
||||
|
||||
await ws.send_json(
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user