-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp.py
More file actions
133 lines (113 loc) · 4.96 KB
/
Copy pathapp.py
File metadata and controls
133 lines (113 loc) · 4.96 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
"""Runnable passkeys demo.
pip install "fastapi-passkeys" uvicorn
uvicorn examples.app:app --reload
Then open http://localhost:8000 and register / sign in with a real authenticator
(Touch ID, Windows Hello, a security key, your phone). This is a demo: it uses the
in-memory repository and a single hard-coded user, so data resets on restart.
"""
from __future__ import annotations
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from fastapi_passkeys import AuthenticationResult, Passkeys, PasskeyConfig, PasskeyUser
from fastapi_passkeys.contrib import InMemoryChallengeStore, InMemoryCredentialRepository
DEMO_USER = PasskeyUser(id="demo-user", name="ada@example.com", display_name="Ada Lovelace")
async def get_user(_: Request) -> PasskeyUser:
return DEMO_USER
async def on_authenticated(_: Request, result: AuthenticationResult) -> dict:
return {"status": "signed-in", "userId": result.user_id}
passkeys = Passkeys(
config=PasskeyConfig(
rp_id="localhost",
rp_name="fastapi-passkeys demo",
expected_origins=["http://localhost:8000"],
),
credential_repository=InMemoryCredentialRepository(),
challenge_store=InMemoryChallengeStore(),
get_user=get_user,
on_authenticated=on_authenticated,
)
app = FastAPI(title="fastapi-passkeys demo")
app.include_router(passkeys.router, prefix="/auth/passkeys")
passkeys.install_exception_handlers(app)
@app.get("/", response_class=HTMLResponse)
async def index() -> str:
return _PAGE
_PAGE = """<!doctype html>
<html><head><meta charset="utf-8"><title>fastapi-passkeys demo</title>
<style>body{font-family:system-ui;max-width:40rem;margin:4rem auto;padding:0 1rem}
button{font-size:1rem;padding:.6rem 1rem;margin:.25rem 0;cursor:pointer}
pre{background:#f4f4f5;padding:1rem;border-radius:.5rem;white-space:pre-wrap}</style>
</head><body>
<h1>fastapi-passkeys demo</h1>
<button onclick="register()">Register a passkey</button>
<button onclick="authenticate()">Sign in</button>
<pre id="out">Ready.</pre>
<script>
const out = document.getElementById('out');
const log = (m) => out.textContent = (typeof m === 'string' ? m : JSON.stringify(m, null, 2));
const b64urlToBuf = (s) => {
s = s.replace(/-/g, '+').replace(/_/g, '/');
const pad = '='.repeat((4 - (s.length % 4)) % 4);
const bin = atob(s + pad);
const buf = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);
return buf.buffer;
};
const bufToB64url = (buf) => {
const bytes = new Uint8Array(buf);
let bin = '';
for (const b of bytes) bin += String.fromCharCode(b);
return btoa(bin).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');
};
async function register() {
try {
const begin = await (await fetch('/auth/passkeys/register/begin', {method: 'POST'})).json();
const opts = begin.publicKey;
opts.challenge = b64urlToBuf(opts.challenge);
opts.user.id = b64urlToBuf(opts.user.id);
(opts.excludeCredentials || []).forEach(c => c.id = b64urlToBuf(c.id));
const cred = await navigator.credentials.create({publicKey: opts});
const body = {
state: begin.state,
deviceName: 'Demo device',
credential: {
id: cred.id, rawId: bufToB64url(cred.rawId), type: cred.type,
response: {
clientDataJSON: bufToB64url(cred.response.clientDataJSON),
attestationObject: bufToB64url(cred.response.attestationObject),
transports: cred.response.getTransports ? cred.response.getTransports() : [],
},
clientExtensionResults: cred.getClientExtensionResults(),
},
};
log(await (await fetch('/auth/passkeys/register/finish',
{method: 'POST', headers: {'content-type': 'application/json'}, body: JSON.stringify(body)})).json());
} catch (e) { log('Error: ' + e); }
}
async function authenticate() {
try {
const begin = await (await fetch('/auth/passkeys/authenticate/begin',
{method: 'POST', headers: {'content-type': 'application/json'}, body: '{}'})).json();
const opts = begin.publicKey;
opts.challenge = b64urlToBuf(opts.challenge);
(opts.allowCredentials || []).forEach(c => c.id = b64urlToBuf(c.id));
const cred = await navigator.credentials.get({publicKey: opts});
const body = {
state: begin.state,
credential: {
id: cred.id, rawId: bufToB64url(cred.rawId), type: cred.type,
response: {
clientDataJSON: bufToB64url(cred.response.clientDataJSON),
authenticatorData: bufToB64url(cred.response.authenticatorData),
signature: bufToB64url(cred.response.signature),
userHandle: cred.response.userHandle ? bufToB64url(cred.response.userHandle) : null,
},
clientExtensionResults: cred.getClientExtensionResults(),
},
};
log(await (await fetch('/auth/passkeys/authenticate/finish',
{method: 'POST', headers: {'content-type': 'application/json'}, body: JSON.stringify(body)})).json());
} catch (e) { log('Error: ' + e); }
}
</script>
</body></html>"""