Project renamed to Paskia.
This commit is contained in:
+3
-4
@@ -3,7 +3,6 @@ dist/
|
|||||||
.*
|
.*
|
||||||
!.gitignore
|
!.gitignore
|
||||||
*.lock
|
*.lock
|
||||||
passkey-auth.sqlite
|
paskia.sqlite
|
||||||
/passkey/frontend-build
|
/paskia/frontend-build
|
||||||
/test_*.py
|
/paskia/_version.py
|
||||||
passkey/_version.py
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# PassKey Auth API Documentation
|
# Paskia API Documentation
|
||||||
|
|
||||||
This document lists the HTTP and WebSocket endpoints exposed by the PassKey Auth
|
This document lists the HTTP and WebSocket endpoints exposed by the Paskia
|
||||||
service and how they behave depending on whether a dedicated authentication host
|
service and how they behave depending on whether a dedicated authentication host
|
||||||
(`--auth-host` / environment `PASSKEY_AUTH_HOST`) is configured.
|
(`--auth-host` / environment `PASSKEY_AUTH_HOST`) is configured.
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# PasskeyAuth
|
# Paskia
|
||||||
|
|
||||||
A minimal FastAPI WebAuthn server with WebSocket support for passkey registration. This project demonstrates WebAuthn registration flow with Resident Keys (discoverable credentials) using modern Python tooling.
|
A minimal FastAPI WebAuthn server with WebSocket support for passkey registration. This project demonstrates WebAuthn registration flow with Resident Keys (discoverable credentials) using modern Python tooling.
|
||||||
|
|
||||||
@@ -31,24 +31,23 @@ uv pip install -e .[dev]
|
|||||||
|
|
||||||
### Run (new CLI)
|
### Run (new CLI)
|
||||||
|
|
||||||
`passkey-auth` now provides subcommands:
|
`paskia` now provides subcommands:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
passkey-auth serve [host:port] [--options]
|
paskia serve [host:port] [--options]
|
||||||
passkey-auth dev [--options]
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Examples (fish shell shown):
|
Examples (fish shell shown):
|
||||||
|
|
||||||
```fish
|
```fish
|
||||||
# Production style (no reload)
|
# Production style (no reload)
|
||||||
passkey-auth serve
|
paskia serve
|
||||||
passkey-auth serve 0.0.0.0:8080 --rp-id example.com --origin https://example.com
|
paskia serve 0.0.0.0:8080 --rp-id example.com --origin https://example.com
|
||||||
|
|
||||||
# Development (auto-reload)
|
# Development (auto-reload via scripts/dev.py)
|
||||||
passkey-auth dev # localhost:4401
|
python scripts/dev.py # localhost:4401
|
||||||
passkey-auth dev :5500 # localhost on port 5500
|
python scripts/dev.py :5500 # localhost on port 5500
|
||||||
passkey-auth dev 127.0.0.1 # host only, default port 4401
|
python scripts/dev.py 127.0.0.1 # host only, default port 4401
|
||||||
```
|
```
|
||||||
|
|
||||||
Available options (both subcommands):
|
Available options (both subcommands):
|
||||||
@@ -61,7 +60,7 @@ Available options (both subcommands):
|
|||||||
|
|
||||||
### Legacy Invocation
|
### Legacy Invocation
|
||||||
|
|
||||||
If you previously used `python -m passkey.fastapi --dev --host ...`, switch to the new form above. The old flags `--host`, `--port`, and `--dev` are replaced by the `[host:port]` positional and the `dev` subcommand.
|
If you previously used `python -m paskia.fastapi --dev --host ...`, switch to the new form above. The old flags `--host`, `--port`, and `--dev` are replaced by using `scripts/dev.py` for development mode.
|
||||||
|
|
||||||
## Usage (Web)
|
## Usage (Web)
|
||||||
|
|
||||||
@@ -90,8 +89,8 @@ hatch run ruff format .
|
|||||||
### Project Structure
|
### Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
passkeyauth/
|
paskia/
|
||||||
├── passkeyauth/
|
├── paskia/
|
||||||
│ ├── __init__.py
|
│ ├── __init__.py
|
||||||
│ └── main.py # FastAPI server with WebSocket support
|
│ └── main.py # FastAPI server with WebSocket support
|
||||||
├── static/
|
├── static/
|
||||||
|
|||||||
+3
-3
@@ -1,6 +1,6 @@
|
|||||||
# PasskeyAuth E2E Tests
|
# Paskia E2E Tests
|
||||||
|
|
||||||
End-to-end tests for PasskeyAuth using [Playwright](https://playwright.dev/) with Chrome's **Virtual Authenticator**.
|
End-to-end tests for Paskia using [Playwright](https://playwright.dev/) with Chrome's **Virtual Authenticator**.
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ npm test
|
|||||||
```
|
```
|
||||||
|
|
||||||
This will:
|
This will:
|
||||||
1. Start a fresh PasskeyAuth server with a test database
|
1. Start a fresh Paskia server with a test database
|
||||||
2. Run all E2E tests against it
|
2. Run all E2E tests against it
|
||||||
3. Clean up the server when done
|
3. Clean up the server when done
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "passkey-auth-e2e",
|
"name": "paskia-e2e",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "E2E tests for PasskeyAuth using Playwright with Virtual Authenticator",
|
"description": "E2E tests for Paskia using Playwright with Virtual Authenticator",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "bunx playwright test",
|
"test": "bunx playwright test",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { defineConfig, devices } from '@playwright/test'
|
import { defineConfig, devices } from '@playwright/test'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Playwright configuration for PasskeyAuth E2E tests.
|
* Playwright configuration for Paskia E2E tests.
|
||||||
* Uses Chrome's Virtual Authenticator for automated passkey testing.
|
* Uses Chrome's Virtual Authenticator for automated passkey testing.
|
||||||
*
|
*
|
||||||
* Run with: bun run test
|
* Run with: bun run test
|
||||||
@@ -23,7 +23,7 @@ export default defineConfig({
|
|||||||
globalTeardown: './tests/global-teardown.ts',
|
globalTeardown: './tests/global-teardown.ts',
|
||||||
|
|
||||||
use: {
|
use: {
|
||||||
// Base URL for the passkey-auth server
|
// Base URL for the Paskia server
|
||||||
baseURL: process.env.BASE_URL || 'http://localhost:4401',
|
baseURL: process.env.BASE_URL || 'http://localhost:4401',
|
||||||
|
|
||||||
// Collect trace on failure for debugging
|
// Collect trace on failure for debugging
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export default async function globalSetup() {
|
|||||||
|
|
||||||
// Start the server using Node's spawn
|
// Start the server using Node's spawn
|
||||||
const serverProcess = spawn('uv', [
|
const serverProcess = spawn('uv', [
|
||||||
'run', 'passkey-auth', 'serve', ':4401',
|
'run', 'paskia', 'serve', ':4401',
|
||||||
'--rp-id', 'localhost',
|
'--rp-id', 'localhost',
|
||||||
'--origin', 'http://localhost:4401'
|
'--origin', 'http://localhost:4401'
|
||||||
], {
|
], {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
} from './fixtures/passkey-helpers'
|
} from './fixtures/passkey-helpers'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* E2E tests for PasskeyAuth using Chrome's Virtual Authenticator.
|
* E2E tests for Paskia using Chrome's Virtual Authenticator.
|
||||||
*
|
*
|
||||||
* These tests exercise the complete WebAuthn flow:
|
* These tests exercise the complete WebAuthn flow:
|
||||||
* 1. Registration via WebSocket using bootstrap reset token
|
* 1. Registration via WebSocket using bootstrap reset token
|
||||||
|
|||||||
+2
-2
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>PassKey Auth - Dev Mode</title>
|
<title>Paskia - Dev Mode</title>
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
color-scheme: light dark; /* Automatic themes by browser */
|
color-scheme: light dark; /* Automatic themes by browser */
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<header>
|
<header>
|
||||||
<h1>🔐 PassKey Auth</h1>
|
<h1>🔐 Paskia</h1>
|
||||||
<p class="subtitle">Development server demonstration page.</p>
|
<p class="subtitle">Development server demonstration page.</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export default defineConfig(({ command }) => ({
|
|||||||
closeBundle() {
|
closeBundle() {
|
||||||
if (command !== 'build') return
|
if (command !== 'build') return
|
||||||
|
|
||||||
const outDir = resolve(__dirname, '../passkey/frontend-build')
|
const outDir = resolve(__dirname, '../paskia/frontend-build')
|
||||||
const moves = [
|
const moves = [
|
||||||
{ from: 'auth.html', to: 'auth/index.html' },
|
{ from: 'auth.html', to: 'auth/index.html' },
|
||||||
{ from: 'admin.html', to: 'admin/index.html' },
|
{ from: 'admin.html', to: 'admin/index.html' },
|
||||||
@@ -69,7 +69,7 @@ export default defineConfig(({ command }) => ({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
outDir: '../passkey/frontend-build',
|
outDir: '../paskia/frontend-build',
|
||||||
emptyOutDir: true,
|
emptyOutDir: true,
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
input: {
|
input: {
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
from paskia.sansio import Passkey
|
||||||
|
|
||||||
|
__all__ = ["Passkey"]
|
||||||
@@ -14,7 +14,7 @@ from importlib.resources import files
|
|||||||
__ALL__ = ["AAGUID", "filter"]
|
__ALL__ = ["AAGUID", "filter"]
|
||||||
|
|
||||||
# Path to the AAGUID JSON file
|
# Path to the AAGUID JSON file
|
||||||
AAGUID_FILE = files("passkey") / "aaguid" / "combined_aaguid.json"
|
AAGUID_FILE = files("paskia") / "aaguid" / "combined_aaguid.json"
|
||||||
AAGUID: dict[str, dict] = json.loads(AAGUID_FILE.read_text(encoding="utf-8"))
|
AAGUID: dict[str, dict] = json.loads(AAGUID_FILE.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
|
||||||
@@ -11,11 +11,11 @@ independent of any web framework:
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from .config import SESSION_LIFETIME
|
from paskia.config import SESSION_LIFETIME
|
||||||
from .db import ResetToken, Session
|
from paskia.db import ResetToken, Session
|
||||||
from .globals import db, passkey
|
from paskia.globals import db, passkey
|
||||||
from .util import hostutil
|
from paskia.util import hostutil
|
||||||
from .util.tokens import create_token, reset_key, session_key
|
from paskia.util.tokens import create_token, reset_key, session_key
|
||||||
|
|
||||||
EXPIRES = SESSION_LIFETIME
|
EXPIRES = SESSION_LIFETIME
|
||||||
|
|
||||||
@@ -12,9 +12,9 @@ from datetime import datetime, timezone
|
|||||||
|
|
||||||
import uuid7
|
import uuid7
|
||||||
|
|
||||||
from . import authsession, globals
|
from paskia import authsession, globals
|
||||||
from .db import Org, Permission, Role, User
|
from paskia.db import Org, Permission, Role, User
|
||||||
from .util import hostutil, passphrase, tokens
|
from paskia.util import hostutil, passphrase, tokens
|
||||||
|
|
||||||
|
|
||||||
def _init_logger() -> logging.Logger:
|
def _init_logger() -> logging.Logger:
|
||||||
@@ -26,9 +26,8 @@ from sqlalchemy.dialects.sqlite import BLOB
|
|||||||
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
||||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
||||||
|
|
||||||
from ..config import SESSION_LIFETIME
|
from paskia.config import SESSION_LIFETIME
|
||||||
from ..globals import db
|
from paskia.db import (
|
||||||
from . import (
|
|
||||||
Credential,
|
Credential,
|
||||||
DatabaseInterface,
|
DatabaseInterface,
|
||||||
Org,
|
Org,
|
||||||
@@ -39,8 +38,9 @@ from . import (
|
|||||||
SessionContext,
|
SessionContext,
|
||||||
User,
|
User,
|
||||||
)
|
)
|
||||||
|
from paskia.globals import db
|
||||||
|
|
||||||
DB_PATH = "sqlite+aiosqlite:///passkey-auth.sqlite"
|
DB_PATH = "sqlite+aiosqlite:///paskia.sqlite"
|
||||||
|
|
||||||
|
|
||||||
def _normalize_dt(value: datetime | None) -> datetime | None:
|
def _normalize_dt(value: datetime | None) -> datetime | None:
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
from paskia.fastapi.mainapp import app
|
||||||
|
|
||||||
|
__all__ = ["app"]
|
||||||
@@ -126,7 +126,7 @@ def main():
|
|||||||
logging.basicConfig(level=logging.INFO, format="%(message)s", force=True)
|
logging.basicConfig(level=logging.INFO, format="%(message)s", force=True)
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
prog="passkey-auth", description="Passkey authentication server"
|
prog="paskia", description="Paskia authentication server"
|
||||||
)
|
)
|
||||||
sub = parser.add_subparsers(dest="command", required=True)
|
sub = parser.add_subparsers(dest="command", required=True)
|
||||||
|
|
||||||
@@ -167,41 +167,41 @@ def main():
|
|||||||
host = port = uds = all_ifaces = None # type: ignore
|
host = port = uds = all_ifaces = None # type: ignore
|
||||||
|
|
||||||
# Export configuration via environment for lifespan initialization in each process
|
# Export configuration via environment for lifespan initialization in each process
|
||||||
os.environ.setdefault("PASSKEY_RP_ID", args.rp_id)
|
os.environ.setdefault("PASKIA_RP_ID", args.rp_id)
|
||||||
if args.rp_name:
|
if args.rp_name:
|
||||||
os.environ["PASSKEY_RP_NAME"] = args.rp_name
|
os.environ["PASKIA_RP_NAME"] = args.rp_name
|
||||||
if args.origin:
|
if args.origin:
|
||||||
os.environ["PASSKEY_ORIGIN"] = args.origin
|
os.environ["PASKIA_ORIGIN"] = args.origin
|
||||||
if getattr(args, "auth_host", None):
|
if getattr(args, "auth_host", None):
|
||||||
os.environ["PASSKEY_AUTH_HOST"] = args.auth_host
|
os.environ["PASKIA_AUTH_HOST"] = args.auth_host
|
||||||
else:
|
else:
|
||||||
# Preserve pre-set env variable if CLI option omitted
|
# Preserve pre-set env variable if CLI option omitted
|
||||||
args.auth_host = os.environ.get("PASSKEY_AUTH_HOST")
|
args.auth_host = os.environ.get("PASKIA_AUTH_HOST")
|
||||||
|
|
||||||
if args.auth_host:
|
if args.auth_host:
|
||||||
validate_auth_host(args.auth_host, args.rp_id)
|
validate_auth_host(args.auth_host, args.rp_id)
|
||||||
from passkey.util import hostutil as _hostutil # local import
|
from paskia.util import hostutil as _hostutil # local import
|
||||||
|
|
||||||
_hostutil.reload_config()
|
_hostutil.reload_config()
|
||||||
|
|
||||||
# One-time initialization + bootstrap before starting any server processes.
|
# One-time initialization + bootstrap before starting any server processes.
|
||||||
# Lifespan in worker processes will call globals.init with bootstrap disabled.
|
# Lifespan in worker processes will call globals.init with bootstrap disabled.
|
||||||
from passkey import globals as _globals # local import
|
from paskia import globals as _globals # local import
|
||||||
|
|
||||||
asyncio.run(
|
asyncio.run(
|
||||||
_globals.init(
|
_globals.init(
|
||||||
rp_id=args.rp_id,
|
rp_id=args.rp_id,
|
||||||
rp_name=args.rp_name,
|
rp_name=args.rp_name,
|
||||||
origin=args.origin,
|
origin=args.origin,
|
||||||
default_admin=os.getenv("PASSKEY_DEFAULT_ADMIN") or None,
|
default_admin=os.getenv("PASKIA_DEFAULT_ADMIN") or None,
|
||||||
default_org=os.getenv("PASSKEY_DEFAULT_ORG") or None,
|
default_org=os.getenv("PASKIA_DEFAULT_ORG") or None,
|
||||||
bootstrap=True,
|
bootstrap=True,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Handle recover-admin command (no server start)
|
# Handle recover-admin command (no server start)
|
||||||
if args.command == "reset":
|
if args.command == "reset":
|
||||||
from passkey.fastapi import reset as reset_cmd # local import
|
from paskia.fastapi import reset as reset_cmd # local import
|
||||||
|
|
||||||
exit_code = reset_cmd.run(getattr(args, "query", None))
|
exit_code = reset_cmd.run(getattr(args, "query", None))
|
||||||
raise SystemExit(exit_code)
|
raise SystemExit(exit_code)
|
||||||
@@ -211,15 +211,15 @@ def main():
|
|||||||
"log_level": "info",
|
"log_level": "info",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Dev mode: enable reload when PASSKEY_DEVMODE is set
|
# Dev mode: enable reload when PASKIA_DEVMODE is set
|
||||||
devmode = os.environ.get("PASSKEY_DEVMODE") == "1"
|
devmode = os.environ.get("PASKIA_DEVMODE") == "1"
|
||||||
if devmode:
|
if devmode:
|
||||||
# Security: dev mode must run on localhost:4402 to prevent
|
# Security: dev mode must run on localhost:4402 to prevent
|
||||||
# accidental public exposure of the Vite dev server
|
# accidental public exposure of the Vite dev server
|
||||||
if host != "localhost" or port != 4402:
|
if host != "localhost" or port != 4402:
|
||||||
raise SystemExit(f"Dev mode requires localhost:4402, got {host}:{port}")
|
raise SystemExit(f"Dev mode requires localhost:4402, got {host}:{port}")
|
||||||
run_kwargs["reload"] = True
|
run_kwargs["reload"] = True
|
||||||
run_kwargs["reload_dirs"] = ["passkey"]
|
run_kwargs["reload_dirs"] = ["paskia"]
|
||||||
|
|
||||||
if uds:
|
if uds:
|
||||||
run_kwargs["uds"] = uds
|
run_kwargs["uds"] = uds
|
||||||
@@ -233,12 +233,12 @@ def main():
|
|||||||
if devmode:
|
if devmode:
|
||||||
run_kwargs["host"] = "::"
|
run_kwargs["host"] = "::"
|
||||||
run_kwargs["port"] = port
|
run_kwargs["port"] = port
|
||||||
uvicorn.run("passkey.fastapi:app", **run_kwargs)
|
uvicorn.run("paskia.fastapi:app", **run_kwargs)
|
||||||
else:
|
else:
|
||||||
# Production: run separate servers for IPv4 and IPv6
|
# Production: run separate servers for IPv4 and IPv6
|
||||||
from uvicorn import Config, Server # noqa: E402 local import
|
from uvicorn import Config, Server # noqa: E402 local import
|
||||||
|
|
||||||
from passkey.fastapi import (
|
from paskia.fastapi import (
|
||||||
app as fastapi_app, # noqa: E402 local import
|
app as fastapi_app, # noqa: E402 local import
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -261,7 +261,7 @@ def main():
|
|||||||
|
|
||||||
asyncio.run(serve_both())
|
asyncio.run(serve_both())
|
||||||
else:
|
else:
|
||||||
uvicorn.run("passkey.fastapi:app", **run_kwargs)
|
uvicorn.run("paskia.fastapi:app", **run_kwargs)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
@@ -5,9 +5,11 @@ from uuid import UUID, uuid4
|
|||||||
from fastapi import Body, FastAPI, HTTPException, Request, Response
|
from fastapi import Body, FastAPI, HTTPException, Request, Response
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
|
|
||||||
from ..authsession import reset_expires
|
from paskia.authsession import reset_expires
|
||||||
from ..globals import db
|
from paskia.fastapi import authz
|
||||||
from ..util import (
|
from paskia.fastapi.session import AUTH_COOKIE
|
||||||
|
from paskia.globals import db
|
||||||
|
from paskia.util import (
|
||||||
frontend,
|
frontend,
|
||||||
hostutil,
|
hostutil,
|
||||||
passphrase,
|
passphrase,
|
||||||
@@ -16,9 +18,7 @@ from ..util import (
|
|||||||
tokens,
|
tokens,
|
||||||
useragent,
|
useragent,
|
||||||
)
|
)
|
||||||
from ..util.tokens import encode_session_key, session_key
|
from paskia.util.tokens import encode_session_key, session_key
|
||||||
from . import authz
|
|
||||||
from .session import AUTH_COOKIE
|
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
|
|
||||||
@@ -13,21 +13,19 @@ from fastapi import (
|
|||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
from fastapi.security import HTTPBearer
|
from fastapi.security import HTTPBearer
|
||||||
|
|
||||||
from passkey.util import frontend
|
from paskia.authsession import (
|
||||||
|
|
||||||
from ..authsession import (
|
|
||||||
EXPIRES,
|
EXPIRES,
|
||||||
get_reset,
|
get_reset,
|
||||||
get_session,
|
get_session,
|
||||||
refresh_session_token,
|
refresh_session_token,
|
||||||
session_expiry,
|
session_expiry,
|
||||||
)
|
)
|
||||||
from ..globals import db
|
from paskia.fastapi import authz, session, user
|
||||||
from ..globals import passkey as global_passkey
|
from paskia.fastapi.session import AUTH_COOKIE
|
||||||
from ..util import hostutil, htmlutil, passphrase, userinfo
|
from paskia.globals import db
|
||||||
from ..util.tokens import session_key
|
from paskia.globals import passkey as global_passkey
|
||||||
from . import authz, session, user
|
from paskia.util import frontend, hostutil, htmlutil, passphrase, userinfo
|
||||||
from .session import AUTH_COOKIE
|
from paskia.util.tokens import session_key
|
||||||
|
|
||||||
bearer_auth = HTTPBearer(auto_error=True)
|
bearer_auth = HTTPBearer(auto_error=True)
|
||||||
|
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
from fastapi import Request, Response
|
from fastapi import Request, Response
|
||||||
from fastapi.responses import RedirectResponse
|
from fastapi.responses import RedirectResponse
|
||||||
|
|
||||||
from passkey.util import hostutil, passphrase
|
from paskia.util import hostutil, passphrase
|
||||||
|
|
||||||
|
|
||||||
def is_ui_path(path: str) -> bool:
|
def is_ui_path(path: str) -> bool:
|
||||||
@@ -2,7 +2,7 @@ import logging
|
|||||||
|
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
|
|
||||||
from ..util import permutil, sessionutil
|
from paskia.util import permutil, sessionutil
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -6,10 +6,9 @@ from fastapi import FastAPI, HTTPException, Request, Response
|
|||||||
from fastapi.responses import RedirectResponse
|
from fastapi.responses import RedirectResponse
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
|
||||||
from passkey.util import frontend, hostutil, passphrase
|
from paskia.fastapi import admin, api, auth_host, ws
|
||||||
|
from paskia.fastapi.session import AUTH_COOKIE
|
||||||
from . import admin, api, auth_host, ws
|
from paskia.util import frontend, hostutil, passphrase
|
||||||
from .session import AUTH_COOKIE
|
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
@@ -19,15 +18,15 @@ async def lifespan(app: FastAPI): # pragma: no cover - startup path
|
|||||||
We populate configuration from environment variables (set by the CLI entrypoint)
|
We populate configuration from environment variables (set by the CLI entrypoint)
|
||||||
so that uvicorn reload / multiprocess workers inherit the settings.
|
so that uvicorn reload / multiprocess workers inherit the settings.
|
||||||
"""
|
"""
|
||||||
from .. import globals
|
from paskia import globals
|
||||||
|
|
||||||
rp_id = os.getenv("PASSKEY_RP_ID", "localhost")
|
rp_id = os.getenv("PASKIA_RP_ID", "localhost")
|
||||||
rp_name = os.getenv("PASSKEY_RP_NAME") or None
|
rp_name = os.getenv("PASKIA_RP_NAME") or None
|
||||||
origin = os.getenv("PASSKEY_ORIGIN") or None
|
origin = os.getenv("PASKIA_ORIGIN") or None
|
||||||
default_admin = (
|
default_admin = (
|
||||||
os.getenv("PASSKEY_DEFAULT_ADMIN") or None
|
os.getenv("PASKIA_DEFAULT_ADMIN") or None
|
||||||
) # still passed for context
|
) # still passed for context
|
||||||
default_org = os.getenv("PASSKEY_DEFAULT_ORG") or None
|
default_org = os.getenv("PASKIA_DEFAULT_ORG") or None
|
||||||
try:
|
try:
|
||||||
# CLI (__main__) performs bootstrap once; here we skip to avoid duplicate work
|
# CLI (__main__) performs bootstrap once; here we skip to avoid duplicate work
|
||||||
await globals.init(
|
await globals.init(
|
||||||
@@ -56,7 +55,7 @@ app.mount("/auth/api/admin/", admin.app)
|
|||||||
app.mount("/auth/api/", api.app)
|
app.mount("/auth/api/", api.app)
|
||||||
app.mount("/auth/ws/", ws.app)
|
app.mount("/auth/ws/", ws.app)
|
||||||
|
|
||||||
# In dev mode (PASSKEY_DEVMODE=1), Vite serves assets directly; skip static files mount
|
# In dev mode (PASKIA_DEVMODE=1), Vite serves assets directly; skip static files mount
|
||||||
if not frontend.is_dev_mode():
|
if not frontend.is_dev_mode():
|
||||||
app.mount(
|
app.mount(
|
||||||
"/auth/assets/",
|
"/auth/assets/",
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
"""CLI support for creating user credential reset links.
|
"""CLI support for creating user credential reset links.
|
||||||
|
|
||||||
Usage (via main CLI):
|
Usage (via main CLI):
|
||||||
passkey-auth reset [query]
|
paskia reset [query]
|
||||||
|
|
||||||
If query is omitted, the master admin (first Administration role user in
|
If query is omitted, the master admin (first Administration role user in
|
||||||
an organization granting auth:admin) is targeted. Otherwise query is
|
an organization granting auth:admin) is targeted. Otherwise query is
|
||||||
@@ -15,10 +15,10 @@ from __future__ import annotations
|
|||||||
import asyncio
|
import asyncio
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from passkey import authsession as _authsession
|
from paskia import authsession as _authsession
|
||||||
from passkey import globals as _g
|
from paskia import globals as _g
|
||||||
from passkey.util import hostutil, passphrase
|
from paskia.util import hostutil, passphrase
|
||||||
from passkey.util import tokens as _tokens
|
from paskia.util import tokens as _tokens
|
||||||
|
|
||||||
|
|
||||||
async def _resolve_targets(query: str | None):
|
async def _resolve_targets(query: str | None):
|
||||||
@@ -10,7 +10,7 @@ Generic session management functions have been moved to authsession.py
|
|||||||
|
|
||||||
from fastapi import Cookie, Request, Response, WebSocket
|
from fastapi import Cookie, Request, Response, WebSocket
|
||||||
|
|
||||||
from ..authsession import EXPIRES
|
from paskia.authsession import EXPIRES
|
||||||
|
|
||||||
AUTH_COOKIE_NAME = "__Host-auth"
|
AUTH_COOKIE_NAME = "__Host-auth"
|
||||||
AUTH_COOKIE = Cookie(None, alias=AUTH_COOKIE_NAME)
|
AUTH_COOKIE = Cookie(None, alias=AUTH_COOKIE_NAME)
|
||||||
@@ -10,16 +10,16 @@ from fastapi import (
|
|||||||
)
|
)
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
|
|
||||||
from ..authsession import (
|
from paskia.authsession import (
|
||||||
delete_credential,
|
delete_credential,
|
||||||
expires,
|
expires,
|
||||||
get_session,
|
get_session,
|
||||||
)
|
)
|
||||||
from ..globals import db
|
from paskia.fastapi import authz, session
|
||||||
from ..util import hostutil, passphrase, tokens
|
from paskia.fastapi.session import AUTH_COOKIE
|
||||||
from ..util.tokens import decode_session_key, session_key
|
from paskia.globals import db
|
||||||
from . import authz, session
|
from paskia.util import hostutil, passphrase, tokens
|
||||||
from .session import AUTH_COOKIE
|
from paskia.util.tokens import decode_session_key, session_key
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
|
|
||||||
@@ -5,12 +5,12 @@ from uuid import UUID
|
|||||||
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
||||||
from webauthn.helpers.exceptions import InvalidAuthenticationResponse
|
from webauthn.helpers.exceptions import InvalidAuthenticationResponse
|
||||||
|
|
||||||
from ..authsession import create_session, get_reset, get_session
|
from paskia.authsession import create_session, get_reset, get_session
|
||||||
from ..globals import db, passkey
|
from paskia.fastapi import authz
|
||||||
from ..util import passphrase
|
from paskia.fastapi.session import AUTH_COOKIE, infodict
|
||||||
from ..util.tokens import create_token, session_key
|
from paskia.globals import db, passkey
|
||||||
from . import authz
|
from paskia.util import passphrase
|
||||||
from .session import AUTH_COOKIE, infodict
|
from paskia.util.tokens import create_token, session_key
|
||||||
|
|
||||||
|
|
||||||
# WebSocket error handling decorator
|
# WebSocket error handling decorator
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Admin</title>
|
||||||
|
<script type="module" crossorigin src="/auth/assets/admin-D8zxJOk4.js"></script>
|
||||||
|
<link rel="modulepreload" crossorigin href="/auth/assets/_plugin-vue_export-helper-R4vr2A9I.js">
|
||||||
|
<link rel="modulepreload" crossorigin href="/auth/assets/helpers-CU0-cyzg.js">
|
||||||
|
<link rel="modulepreload" crossorigin href="/auth/assets/AccessDenied-guOGfNm-.js">
|
||||||
|
<link rel="stylesheet" crossorigin href="/auth/assets/_plugin-vue_export-helper-Bx2cFCEC.css">
|
||||||
|
<link rel="stylesheet" crossorigin href="/auth/assets/AccessDenied-TAST_piX.css">
|
||||||
|
<link rel="stylesheet" crossorigin href="/auth/assets/admin-DIOoLLHy.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="admin-app"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
|||||||
|
import{_ as z,G as E,r as g,c as y,o as L,d,e as h,i as f,f as n,t as _,n as N,R as O,C as Y,Y as w,V as T,p as D}from"./_plugin-vue_export-helper-R4vr2A9I.js";const q={class:"app-shell"},G={key:0,class:"global-status",style:{display:"block"}},J={class:"view-root"},W={key:0,class:"surface surface--tight"},j={class:"view-header center"},H={key:0,class:"user-line"},K={class:"view-lede"},Q={class:"section-block"},X={class:"section-body center"},Z={class:"button-row center"},ee=["disabled"],te=["disabled"],ae=["disabled"],se=["disabled"],ne={__name:"RestrictedAuth",props:{mode:{type:String,default:"login",validator:o=>["login","reauth","forbidden"].includes(o)}},emits:["authenticated","forbidden","logout","back","home","auth-error"],setup(o,{expose:V,emit:$}){const v=o,m=$,i=E({show:!1,message:"",type:"info"}),b=g(!0),t=g(!1),S=g(null),r=g(null),l=g("initial");let p=null;const u=y(()=>!!r.value?.authenticated),k=y(()=>b.value?!1:v.mode==="reauth"?!0:l.value!=="forbidden"),U=y(()=>v.mode==="reauth"?"🔐 Additional Authentication":l.value==="forbidden"?"🚫 Forbidden":`🔐 ${S.value?.rp_name||location.origin}`),B=y(()=>v.mode==="reauth"?"Please verify your identity to continue with this action.":l.value==="forbidden"?"You lack the required permissions.":"Please sign in with your passkey."),F=y(()=>r.value?.user?.user_name||"User");function c(e,a="info",s=3e3){i.show=!0,i.message=e,i.type=a,p&&clearTimeout(p),s>0&&(p=setTimeout(()=>{i.show=!1},s))}async function I(){try{const e=await Y();if(S.value=e,e?.rp_name){const a=v.mode==="reauth"?"Verify Identity":u.value?"Forbidden":"Sign In";document.title=`${e.rp_name} · ${a}`}}catch(e){console.warn("Unable to load settings",e)}}async function M(){try{r.value=await w("/auth/api/user-info",{method:"POST"}),u.value&&v.mode!=="reauth"?(l.value="forbidden",m("forbidden",r.value)):l.value="login"}catch(e){console.error("Failed to load user info",e),e.status!==401&&e.status!==403&&c(T(e),"error",4e3),r.value=null,l.value="login"}}async function A(){if(!k.value||t.value)return;t.value=!0,c("Starting authentication…","info");let e;try{e=await D.authenticate()}catch(a){t.value=!1;const s=a?.message||"Passkey authentication cancelled",P=s==="Passkey authentication cancelled";c(s,P?"info":"error",4e3),m("auth-error",{message:s,cancelled:P});return}try{await x(e)}catch(a){t.value=!1;const s=a?.message||"Failed to establish session";c(s,"error",4e3),m("auth-error",{message:s,cancelled:!1});return}t.value=!1,m("authenticated",e)}async function C(){if(!t.value){t.value=!0;try{await w("/auth/api/logout",{method:"POST"}),r.value=null,l.value="login",c("Logged out. You can sign in with a different account.","info",3e3)}catch(e){c(T(e),"error",4e3)}finally{t.value=!1}m("logout")}}function R(){const e=window.open("/auth/","passkey_auth_profile");e&&e.focus()}async function x(e){if(!e?.session_token)throw console.error("setSessionCookie called with missing session_token:",e),new Error("Authentication response missing session_token");return await w("/auth/api/set-session",{method:"POST",headers:{Authorization:`Bearer ${e.session_token}`}})}return L(async()=>{await I(),await M(),b.value=!1}),V({showMessage:c,isAuthenticated:u,userInfo:r}),(e,a)=>(h(),d("div",q,[i.show?(h(),d("div",G,[n("div",{class:N(["status",i.type])},_(i.message),3)])):f("",!0),n("main",J,[b.value?f("",!0):(h(),d("div",W,[n("header",j,[n("h1",null,_(U.value),1),u.value?(h(),d("p",H,"👤 "+_(F.value),1)):f("",!0),n("p",K,_(B.value),1)]),n("section",Q,[n("div",X,[n("div",Z,[O(e.$slots,"actions",{loading:t.value,canAuthenticate:k.value,isAuthenticated:u.value,authenticate:A,logout:C,mode:o.mode},()=>[n("button",{class:"btn-secondary",disabled:t.value,onClick:a[0]||(a[0]=s=>e.$emit("back"))},"Back",8,ee),k.value?(h(),d("button",{key:0,class:"btn-primary",disabled:t.value,onClick:A},_(t.value?o.mode==="reauth"?"Verifying…":"Signing in…":o.mode==="reauth"?"Verify":"Login"),9,te)):f("",!0),u.value&&o.mode!=="reauth"?(h(),d("button",{key:1,class:"btn-danger",disabled:t.value,onClick:C},"Logout",8,ae)):f("",!0),u.value&&o.mode!=="reauth"?(h(),d("button",{key:2,class:"btn-primary",disabled:t.value,onClick:R},"Profile",8,se)):f("",!0)])])])])]))])]))}},ie=z(ne,[["__scopeId","data-v-d00079a6"]]);export{ie as R};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
.button-row.center[data-v-d00079a6]{display:flex;justify-content:center;gap:.75rem}.user-line[data-v-d00079a6]{margin:.5rem 0 0;font-weight:500;color:var(--color-text)}main.view-root[data-v-d00079a6]{min-height:100vh;align-items:center;justify-content:center;padding:2rem 1rem}.surface.surface--tight[data-v-d00079a6]{max-width:520px;margin:0 auto;width:100%;display:flex;flex-direction:column;gap:1.75rem}
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
|||||||
|
.view-lede[data-v-0cc830bd]{margin:0;color:var(--color-text-muted);font-size:1rem}.section-header[data-v-0cc830bd]{display:flex;flex-direction:column;gap:.4rem}.section-description[data-v-0cc830bd]{margin:0;color:var(--color-text-muted)}.empty-state[data-v-0cc830bd]{margin:0;color:var(--color-text-muted);text-align:center;padding:1rem 0}.logout-button[data-v-0cc830bd]{align-self:flex-start}.logout-row[data-v-0cc830bd]{gap:1rem}.logout-row.single[data-v-0cc830bd]{justify-content:flex-start}.logout-note[data-v-0cc830bd]{margin:.75rem 0 0;color:var(--color-text-muted);font-size:.875rem}@media(max-width:720px){.logout-button[data-v-0cc830bd]{width:100%}}.host-view[data-v-88828278]{padding:3rem 1.5rem 4rem}.host-actions[data-v-88828278]{display:flex;flex-direction:column;gap:.75rem}.host-actions .button-row[data-v-88828278]{gap:.75rem;flex-wrap:wrap}.host-actions .button-row button[data-v-88828278]{flex:0 0 auto}.note[data-v-88828278],.empty-state[data-v-88828278]{margin:0;color:var(--color-text-muted)}@media(max-width:600px){.host-actions .button-row[data-v-88828278]{flex-direction:column}.host-actions .button-row button[data-v-88828278]{width:100%}}
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
|||||||
|
import{c as o,W as d,o as i,h as s,e as m,u as l,v as h}from"./_plugin-vue_export-helper-R4vr2A9I.js";import{R as p}from"./RestrictedAuth-BIGLs28V.js";import{g as n}from"./helpers-CU0-cyzg.js";const f={__name:"RestrictedForward",setup(w){const a=o(()=>d()),r=o(()=>{const t=document.documentElement.getAttribute("data-mode");return t==="reauth"?"reauth":t==="forbidden"?"forbidden":"login"});function c(){location.reload()}function u(){const e=a.value||"/auth/";window.location.pathname!==e&&history.replaceState(null,"",e),window.location.href=e}return i(()=>{window.addEventListener("keydown",e=>{e.key==="Escape"&&n()})}),(e,t)=>(m(),s(p,{mode:r.value,onAuthenticated:c,onBack:l(n),onHome:u},null,8,["mode","onBack"]))}};h(f).mount("#app");
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
function f(r){if(!r)return"Never";const s=new Date(r),u=s-new Date,e=u>0,a=Math.abs(u),n=Math.round(a/(1e3*60)),o=Math.round(a/(1e3*60*60)),t=Math.round(a/(1e3*60*60*24));return a<1e3*60?"Now":n<=60?e?`In ${n} minute${n===1?"":"s"}`:n===1?"a minute ago":`${n} minutes ago`:o<=24?e?`In ${o} hour${o===1?"":"s"}`:o===1?"an hour ago":`${o} hours ago`:t<=14?e?`In ${t} day${t===1?"":"s"}`:t===1?"a day ago":`${t} days ago`:s.toLocaleDateString(void 0,{year:"numeric",month:"long",day:"numeric"})}const c=()=>history.back()||window.close();export{f,c as g};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
.center[data-v-4f202f9a]{text-align:center}.button-row.center[data-v-4f202f9a]{display:flex;justify-content:center}.section-body[data-v-4f202f9a]{gap:1.25rem}.name-edit span[data-v-4f202f9a]{color:var(--color-text-muted);font-size:.9rem}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
import{_ as M,G as F,r as i,c as v,W as b,o as U,d as c,e as u,i as V,f as t,t as g,n as $,z,A as E,S as I,C as N,q as R,X as D,V as K,p as O,v as j}from"./_plugin-vue_export-helper-R4vr2A9I.js";const q={class:"app-shell"},G={key:0,class:"global-status",style:{display:"block"}},H={class:"view-root"},J={class:"surface surface--tight",style:{"max-width":"560px",margin:"0 auto",width:"100%"}},L={class:"view-header",style:{"text-align":"center"}},W={class:"view-lede"},X={key:0,class:"section-block"},Y={key:1,class:"section-block"},Q={class:"section-body center"},Z={key:2,class:"section-block"},ee={class:"section-body"},se={class:"name-edit"},te=["disabled"],ae=["disabled"],ne={__name:"ResetApp",setup(ie){const o=F({show:!1,message:"",type:"info"}),d=i(!0),n=i(!1),r=i(""),x=i(null),p=i(null),f=i(""),m=i("");let h=null;const P=v(()=>p.value?.session_type||"your enrollment"),S=v(()=>d.value?"Preparing your secure enrollment…":y.value?`Finish up ${P.value}. You may edit the name below if needed, and it will be saved to your passkey.`:"This reset link is no longer valid.");v(()=>b());const y=v(()=>!!(r.value&&p.value));function l(e,s="info",a=3e3){o.show=!0,o.message=e,o.type=s,h&&clearTimeout(h),a>0&&(h=setTimeout(()=>{o.show=!1},a))}async function T(){try{const e=await N();x.value=e,e?.rp_name&&(document.title=`${e.rp_name} · Passkey Setup`)}catch(e){console.warn("Unable to load settings",e)}}async function C(){if(r.value)try{p.value=await R(`/auth/api/user-info?reset=${encodeURIComponent(r.value)}`,{method:"POST"}),f.value=p.value?.user?.user_name||""}catch(e){console.error("Failed to load user info",e);const s=e instanceof D?e.data?.detail||"Reset link is invalid or expired.":K(e);m.value=s,l(s,"error",0)}}async function _(){if(!y.value||n.value)return;n.value=!0,l("Starting passkey registration…","info");let e;try{const s=f.value.trim()||null;e=await O.register(r.value,s)}catch(s){n.value=!1;const a=s?.message||"Passkey registration cancelled",k=a==="Passkey registration cancelled";l(k?a:`Registration failed: ${a}`,k?"info":"error",4e3);return}try{await A(e)}catch(s){n.value=!1;const a=s?.message||"Failed to establish session";l(a,"error",4e3);return}l("Passkey registered successfully!","success",800),setTimeout(()=>{n.value=!1,w()},800)}async function A(e){if(!e?.session_token)throw new Error("Registration response missing session_token");return await R("/auth/api/set-session",{method:"POST",headers:{Authorization:`Bearer ${e.session_token}`}})}function w(){const e=b.value||"/auth/";window.location.pathname!==e&&history.replaceState(null,"",e),window.location.reload()}function B(){const e=window.location.pathname.split("/").filter(Boolean);if(!e.length)return"";const s=e[e.length-1],a=e.slice(0,-1);return a.length>1||a.length===1&&a[0]!=="auth"||!s.includes(".")?"":s}return U(async()=>{if(r.value=B(),await T(),!r.value){const e="Reset link is missing or malformed.";m.value=e,l(e,"error",0),d.value=!1;return}await C(),d.value=!1}),(e,s)=>(u(),c("div",q,[o.show?(u(),c("div",G,[t("div",{class:$(["status",o.type])},g(o.message),3)])):V("",!0),t("main",H,[t("div",J,[t("header",L,[s[1]||(s[1]=t("h1",null,"🔑 Registration",-1)),t("p",W,g(S.value),1)]),d.value?(u(),c("section",X,[...s[2]||(s[2]=[t("div",{class:"section-body center"},[t("p",null,"Loading reset details…")],-1)])])):y.value?(u(),c("section",Z,[t("div",ee,[t("label",se,[s[3]||(s[3]=t("span",null,"👤 Name",-1)),z(t("input",{type:"text","onUpdate:modelValue":s[0]||(s[0]=a=>f.value=a),disabled:n.value,maxlength:"64",onKeyup:I(_,["enter"])},null,40,te),[[E,f.value]])]),t("button",{class:"btn-primary",disabled:n.value,onClick:_},g(n.value?"Registering…":"Register Passkey"),9,ae)])])):(u(),c("section",Y,[t("div",Q,[t("p",null,g(m.value),1),t("div",{class:"button-row center",style:{"justify-content":"center"}},[t("button",{class:"btn-secondary",onClick:w},"Return to sign-in")])])]))])])]))}},oe=M(ne,[["__scopeId","data-v-4f202f9a"]]);j(oe).mount("#app");
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
import{c as r,o as c,h as i,e as d,v as u}from"./_plugin-vue_export-helper-R4vr2A9I.js";import{R as p}from"./RestrictedAuth-BIGLs28V.js";const h={__name:"RestrictedApi",setup(m){const a=r(()=>{const n=new URLSearchParams(window.location.hash.slice(1)).get("mode");return n==="reauth"?"reauth":n==="forbidden"?"forbidden":"login"});function t(e){window.parent&&window.parent!==window&&window.parent.postMessage(e,"*")}function s(e){t({type:"auth-success",authenticated:!0,sessionToken:e.session_token})}function o(){t({type:"auth-back"})}return c(()=>{t({type:"auth-ready"}),window.addEventListener("keydown",e=>{e.key==="Escape"&&o()})}),(e,n)=>(d(),i(p,{mode:a.value,onAuthenticated:s,onBack:o},null,8,["mode"]))}};u(h).mount("#app");
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Auth Profile</title>
|
||||||
|
<script type="module" crossorigin src="/auth/assets/auth-a0yJ_sei.js"></script>
|
||||||
|
<link rel="modulepreload" crossorigin href="/auth/assets/_plugin-vue_export-helper-R4vr2A9I.js">
|
||||||
|
<link rel="modulepreload" crossorigin href="/auth/assets/helpers-CU0-cyzg.js">
|
||||||
|
<link rel="modulepreload" crossorigin href="/auth/assets/AccessDenied-guOGfNm-.js">
|
||||||
|
<link rel="stylesheet" crossorigin href="/auth/assets/_plugin-vue_export-helper-Bx2cFCEC.css">
|
||||||
|
<link rel="stylesheet" crossorigin href="/auth/assets/AccessDenied-TAST_piX.css">
|
||||||
|
<link rel="stylesheet" crossorigin href="/auth/assets/auth-CBojJKUK.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<html style="background: transparent">
|
||||||
|
<script type="module" crossorigin src="/auth/assets/restricted-DVCvYFGN.js"></script>
|
||||||
|
<link rel="modulepreload" crossorigin href="/auth/assets/_plugin-vue_export-helper-R4vr2A9I.js">
|
||||||
|
<link rel="modulepreload" crossorigin href="/auth/assets/RestrictedAuth-BIGLs28V.js">
|
||||||
|
<link rel="stylesheet" crossorigin href="/auth/assets/_plugin-vue_export-helper-Bx2cFCEC.css">
|
||||||
|
<link rel="stylesheet" crossorigin href="/auth/assets/RestrictedAuth-CMHKrNJh.css">
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<div id="app"></div>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Access Restricted</title>
|
||||||
|
<script type="module" crossorigin src="/auth/assets/forward-BHNzlQhM.js"></script>
|
||||||
|
<link rel="modulepreload" crossorigin href="/auth/assets/_plugin-vue_export-helper-R4vr2A9I.js">
|
||||||
|
<link rel="modulepreload" crossorigin href="/auth/assets/RestrictedAuth-BIGLs28V.js">
|
||||||
|
<link rel="modulepreload" crossorigin href="/auth/assets/helpers-CU0-cyzg.js">
|
||||||
|
<link rel="stylesheet" crossorigin href="/auth/assets/_plugin-vue_export-helper-Bx2cFCEC.css">
|
||||||
|
<link rel="stylesheet" crossorigin href="/auth/assets/RestrictedAuth-CMHKrNJh.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Complete Passkey Setup</title>
|
||||||
|
<script type="module" crossorigin src="/auth/assets/reset-YnZxhnI5.js"></script>
|
||||||
|
<link rel="modulepreload" crossorigin href="/auth/assets/_plugin-vue_export-helper-R4vr2A9I.js">
|
||||||
|
<link rel="stylesheet" crossorigin href="/auth/assets/_plugin-vue_export-helper-Bx2cFCEC.css">
|
||||||
|
<link rel="stylesheet" crossorigin href="/auth/assets/reset-DXzuKgh6.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
from typing import Generic, TypeVar
|
from typing import Generic, TypeVar
|
||||||
|
|
||||||
from .db import DatabaseInterface
|
from paskia.db import DatabaseInterface
|
||||||
from .sansio import Passkey
|
from paskia.sansio import Passkey
|
||||||
|
|
||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ from webauthn.helpers.structs import (
|
|||||||
UserVerificationRequirement,
|
UserVerificationRequirement,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .db import Credential
|
from paskia.db import Credential
|
||||||
|
|
||||||
|
|
||||||
class Passkey:
|
class Passkey:
|
||||||
@@ -14,7 +14,7 @@ DEV_SERVER = "http://localhost:4403"
|
|||||||
def _resolve_static_dir() -> Path:
|
def _resolve_static_dir() -> Path:
|
||||||
# Try packaged path via importlib.resources (works for wheel/installed).
|
# Try packaged path via importlib.resources (works for wheel/installed).
|
||||||
try: # pragma: no cover - trivial path resolution
|
try: # pragma: no cover - trivial path resolution
|
||||||
pkg_dir = resources.files("passkey") / "frontend-build"
|
pkg_dir = resources.files("paskia") / "frontend-build"
|
||||||
fs_path = Path(str(pkg_dir))
|
fs_path = Path(str(pkg_dir))
|
||||||
if fs_path.is_dir():
|
if fs_path.is_dir():
|
||||||
return fs_path
|
return fs_path
|
||||||
@@ -34,7 +34,7 @@ def file(*parts: str) -> Path:
|
|||||||
|
|
||||||
def is_dev_mode() -> bool:
|
def is_dev_mode() -> bool:
|
||||||
"""Check if we're running in dev mode (Vite frontend server)."""
|
"""Check if we're running in dev mode (Vite frontend server)."""
|
||||||
return os.environ.get("PASSKEY_DEVMODE") == "1"
|
return os.environ.get("PASKIA_DEVMODE") == "1"
|
||||||
|
|
||||||
|
|
||||||
async def read(filepath: str) -> tuple[bytes, int, dict[str, str]]:
|
async def read(filepath: str) -> tuple[bytes, int, dict[str, str]]:
|
||||||
@@ -4,9 +4,9 @@ import os
|
|||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from urllib.parse import urlparse, urlsplit
|
from urllib.parse import urlparse, urlsplit
|
||||||
|
|
||||||
from ..globals import passkey as global_passkey
|
from paskia.globals import passkey as global_passkey
|
||||||
|
|
||||||
_AUTH_HOST_ENV = "PASSKEY_AUTH_HOST"
|
_AUTH_HOST_ENV = "PASKIA_AUTH_HOST"
|
||||||
|
|
||||||
|
|
||||||
def _default_origin_scheme() -> str:
|
def _default_origin_scheme() -> str:
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import secrets
|
import secrets
|
||||||
|
|
||||||
from .wordlist import words
|
from paskia.util.wordlist import words
|
||||||
|
|
||||||
N_WORDS = 5
|
N_WORDS = 5
|
||||||
|
|
||||||
@@ -3,9 +3,9 @@
|
|||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
from fnmatch import fnmatchcase
|
from fnmatch import fnmatchcase
|
||||||
|
|
||||||
from ..globals import db
|
from paskia.globals import db
|
||||||
from .hostutil import normalize_host
|
from paskia.util.hostutil import normalize_host
|
||||||
from .tokens import session_key
|
from paskia.util.tokens import session_key
|
||||||
|
|
||||||
__all__ = ["has_any", "has_all", "session_context"]
|
__all__ = ["has_any", "has_all", "session_context"]
|
||||||
|
|
||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
from ..db import SessionContext
|
from paskia.db import SessionContext
|
||||||
from .timeutil import parse_duration
|
from paskia.util.timeutil import parse_duration
|
||||||
|
|
||||||
|
|
||||||
def check_session_age(ctx: SessionContext, max_age: str | None) -> bool:
|
def check_session_age(ctx: SessionContext, max_age: str | None) -> bool:
|
||||||
@@ -2,7 +2,7 @@ import base64
|
|||||||
import hashlib
|
import hashlib
|
||||||
import secrets
|
import secrets
|
||||||
|
|
||||||
from .passphrase import is_well_formed
|
from paskia.util.passphrase import is_well_formed
|
||||||
|
|
||||||
|
|
||||||
def create_token() -> str:
|
def create_token() -> str:
|
||||||
@@ -2,12 +2,10 @@
|
|||||||
|
|
||||||
from datetime import timezone
|
from datetime import timezone
|
||||||
|
|
||||||
from passkey.util import useragent
|
from paskia import aaguid
|
||||||
|
from paskia.authsession import session_key
|
||||||
from .. import aaguid
|
from paskia.globals import db
|
||||||
from ..authsession import session_key
|
from paskia.util import hostutil, permutil, tokens, useragent
|
||||||
from ..globals import db
|
|
||||||
from . import hostutil, permutil, tokens
|
|
||||||
|
|
||||||
|
|
||||||
def _format_datetime(dt):
|
def _format_datetime(dt):
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
from .sansio import Passkey
|
|
||||||
|
|
||||||
__all__ = ["Passkey"]
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
from .mainapp import app
|
|
||||||
|
|
||||||
__all__ = ["app"]
|
|
||||||
+5
-5
@@ -3,7 +3,7 @@ requires = ["hatchling", "hatch-vcs"]
|
|||||||
build-backend = "hatchling.build"
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "passkey"
|
name = "paskia"
|
||||||
dynamic = ["version"]
|
dynamic = ["version"]
|
||||||
description = "Passkey Authentication for Web Services"
|
description = "Passkey Authentication for Web Services"
|
||||||
authors = [
|
authors = [
|
||||||
@@ -26,7 +26,7 @@ requires-python = ">=3.10"
|
|||||||
source = "vcs"
|
source = "vcs"
|
||||||
|
|
||||||
[tool.hatch.build.hooks.vcs]
|
[tool.hatch.build.hooks.vcs]
|
||||||
version-file = "passkey/_version.py"
|
version-file = "paskia/_version.py"
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
@@ -40,11 +40,11 @@ line-length = 88
|
|||||||
[tool.ruff.lint]
|
[tool.ruff.lint]
|
||||||
select = ["E", "F", "I", "N", "W", "UP"]
|
select = ["E", "F", "I", "N", "W", "UP"]
|
||||||
ignore = ["E501"] # Line too long
|
ignore = ["E501"] # Line too long
|
||||||
isort.known-first-party = ["passkey"]
|
isort.known-first-party = ["paskia"]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
passkey-auth = "passkey.fastapi.__main__:main"
|
paskia = "paskia.fastapi.__main__:main"
|
||||||
|
|
||||||
[tool.hatch.build]
|
[tool.hatch.build]
|
||||||
artifacts = ["passkey/frontend-build"]
|
artifacts = ["paskia/frontend-build"]
|
||||||
targets.sdist.hooks.custom.path = "scripts/build-frontend.py"
|
targets.sdist.hooks.custom.path = "scripts/build-frontend.py"
|
||||||
|
|||||||
Reference in New Issue
Block a user