From 3177dc42f554e1286e00d152698ce1b76328e0aa Mon Sep 17 00:00:00 2001 From: lewis Date: Sat, 20 Dec 2025 17:36:33 +0200 Subject: [PATCH] did:web support, including our own internal kind --- ...9e571af810234fa61368f58dd72e1d111cc5.json} | 12 +- Cargo.lock | 10 + Cargo.toml | 1 + TODO.md | 44 ++- frontend/src/lib/api.ts | 6 + frontend/src/routes/Register.svelte | 142 +++++++- ...0251222_add_did_web_migration_tracking.sql | 2 + src/api/identity/account.rs | 186 ++++++---- src/api/identity/did.rs | 303 ++++++++++++---- src/api/identity/plc/sign.rs | 6 + src/api/identity/plc/submit.rs | 6 + tests/did_web.rs | 324 ++++++++++++++++++ tests/identity.rs | 11 +- 13 files changed, 888 insertions(+), 165 deletions(-) rename .sqlx/{query-b2c53e6a278c4549c99a5b98cc7ca77fc1e9cd39a591c1d8ec1ca41adfffa3a6.json => query-9bd55935253b57b1b7e2d2bf69509e571af810234fa61368f58dd72e1d111cc5.json} (56%) create mode 100644 migrations/20251222_add_did_web_migration_tracking.sql create mode 100644 tests/did_web.rs diff --git a/.sqlx/query-b2c53e6a278c4549c99a5b98cc7ca77fc1e9cd39a591c1d8ec1ca41adfffa3a6.json b/.sqlx/query-9bd55935253b57b1b7e2d2bf69509e571af810234fa61368f58dd72e1d111cc5.json similarity index 56% rename from .sqlx/query-b2c53e6a278c4549c99a5b98cc7ca77fc1e9cd39a591c1d8ec1ca41adfffa3a6.json rename to .sqlx/query-9bd55935253b57b1b7e2d2bf69509e571af810234fa61368f58dd72e1d111cc5.json index 21229ef..664e26c 100644 --- a/.sqlx/query-b2c53e6a278c4549c99a5b98cc7ca77fc1e9cd39a591c1d8ec1ca41adfffa3a6.json +++ b/.sqlx/query-9bd55935253b57b1b7e2d2bf69509e571af810234fa61368f58dd72e1d111cc5.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, did FROM users WHERE handle = $1", + "query": "SELECT id, did, migrated_to_pds FROM users WHERE handle = $1", "describe": { "columns": [ { @@ -12,6 +12,11 @@ "ordinal": 1, "name": "did", "type_info": "Text" + }, + { + "ordinal": 2, + "name": "migrated_to_pds", + "type_info": "Text" } ], "parameters": { @@ -21,8 +26,9 @@ }, "nullable": [ false, - false + false, + true ] }, - "hash": "b2c53e6a278c4549c99a5b98cc7ca77fc1e9cd39a591c1d8ec1ca41adfffa3a6" + "hash": "9bd55935253b57b1b7e2d2bf69509e571af810234fa61368f58dd72e1d111cc5" } diff --git a/Cargo.lock b/Cargo.lock index 9690e7e..66dc046 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -929,6 +929,15 @@ dependencies = [ "cfg_aliases", ] +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + [[package]] name = "btree-range-map" version = "0.7.2" @@ -6175,6 +6184,7 @@ dependencies = [ "base32", "base64 0.22.1", "bcrypt", + "bs58", "bytes", "chrono", "cid", diff --git a/Cargo.toml b/Cargo.toml index c58edad..b2feb92 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,6 +55,7 @@ tower-http = { version = "0.6", features = ["fs", "cors"] } hickory-resolver = { version = "0.24", features = ["tokio-runtime"] } metrics = "0.24" metrics-exporter-prometheus = { version = "0.16", default-features = false, features = ["http-listener"] } +bs58 = "0.5.1" [features] external-infra = [] [dev-dependencies] diff --git a/TODO.md b/TODO.md index 0900b66..01f5ded 100644 --- a/TODO.md +++ b/TODO.md @@ -9,6 +9,21 @@ So like... make the thing unique, make it cool. - [ ] Unique "brand" style both unauthed and authed - [ ] Better documentation on how to sub out the entire frontend for whatever the users want +### Passkeys and 2FA +Modern passwordless authentication using WebAuthn/FIDO2, plus TOTP for defense in depth. + +- [ ] passkeys table (id, did, credential_id, public_key, sign_count, created_at, last_used, friendly_name) +- [ ] user_totp table (did, secret_encrypted, verified, created_at, last_used) +- [ ] WebAuthn registration challenge generation and attestation verification +- [ ] TOTP secret generation with QR code setup flow +- [ ] Backup codes (hashed, one-time use) with recovery flow +- [ ] OAuth authorize flow: password -> 2FA (if enabled) -> passkey (as alternative) +- [ ] Passkey-only account creation (no password) +- [ ] Settings UI for managing passkeys, TOTP, backup codes +- [ ] Trusted devices option (remember this browser) +- [ ] Rate limit 2FA attempts +- [ ] Re-auth for sensitive actions (email change, adding new auth methods) + ### Delegated accounts Accounts controlled by other accounts rather than having their own password. When logging in as a delegated account, OAuth asks you to authenticate with a linked controller account. Uses OAuth scopes as the permission model. @@ -26,20 +41,19 @@ Accounts controlled by other accounts rather than having their own password. Whe - [ ] Log all actions with both actor DID and controller DID - [ ] Audit log view for delegated account owners -### Passkeys and 2FA -Modern passwordless authentication using WebAuthn/FIDO2, plus TOTP for defense in depth. +### Migration tool +Seamless account migration built into the UI, inspired by pdsmoover. Users shouldn't need external tools or brain surgery on half-done account states. -- [ ] passkeys table (id, did, credential_id, public_key, sign_count, created_at, last_used, friendly_name) -- [ ] user_totp table (did, secret_encrypted, verified, created_at, last_used) -- [ ] WebAuthn registration challenge generation and attestation verification -- [ ] TOTP secret generation with QR code setup flow -- [ ] Backup codes (hashed, one-time use) with recovery flow -- [ ] OAuth authorize flow: password → 2FA (if enabled) → passkey (as alternative) -- [ ] Passkey-only account creation (no password) -- [ ] Settings UI for managing passkeys, TOTP, backup codes -- [ ] Trusted devices option (remember this browser) -- [ ] Rate limit 2FA attempts -- [ ] Re-auth for sensitive actions (email change, adding new auth methods) +- [ ] Add `migratingTo` parameter to `deactivateAccount` endpoint +- [ ] For self-hosted did:web users: set `migrated_to_pds`, update DID doc serviceEndpoint +- [ ] "Migrated" account state for self-hosted did:web: can authenticate but no repo operations +- [ ] Migrated did:web user UI: minimal dashboard with "update forwarding PDS" setting, or full migration wizard to handle PDS 2 -> PDS 3 moves automatically +- [ ] Outbound UI wizard: new PDS URL -> export repo -> guide account creation -> complete migration +- [ ] Inbound UI wizard: login to old PDS -> choose handle -> import -> PLC token flow +- [ ] Support `createAccount` with existing DID + service auth token +- [ ] Progress tracking with resume capability +- [ ] Scheduled automatic backups (CAR export) +- [ ] One-click restore from backup ### Plugin system Extensible architecture allowing third-party plugins to add functionality, like minecraft mods or browser extensions. @@ -74,7 +88,9 @@ Records that only authorized parties can see and decrypt. Requires key federatio ## Completed -Core ATProto: Health, describeServer, all session endpoints, full repo CRUD, applyWrites, blob upload, importRepo, firehose with cursor replay, CAR export, blob sync, crawler notifications, handle resolution, PLC operations, did:web, full admin API, moderation reports. +Core ATProto: Health, describeServer, all session endpoints, full repo CRUD, applyWrites, blob upload, importRepo, firehose with cursor replay, CAR export, blob sync, crawler notifications, handle resolution, PLC operations, full admin API, moderation reports. + +did:web support: Self-hosted did:web (subdomain format `did:web:handle.pds.com`), external/BYOD did:web, DID document serving via `/.well-known/did.json`, migration tracking for did:web users who leave (serviceEndpoint redirect), clear registration warnings about did:web trade-offs vs did:plc. OAuth 2.1: Authorization server metadata, JWKS, PAR, authorize endpoint with login UI, token endpoint (auth code + refresh), revocation, introspection, DPoP, PKCE S256, client metadata validation, private_key_jwt verification. diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 6ba0b42..dc56561 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -71,11 +71,15 @@ export interface InviteCode { export type VerificationChannel = 'email' | 'discord' | 'telegram' | 'signal' +export type DidType = 'plc' | 'web' | 'web-external' + export interface CreateAccountParams { handle: string email: string password: string inviteCode?: string + didType?: DidType + did?: string verificationChannel?: VerificationChannel discordId?: string telegramUsername?: string @@ -109,6 +113,8 @@ export const api = { email: params.email, password: params.password, inviteCode: params.inviteCode, + didType: params.didType, + did: params.did, verificationChannel: params.verificationChannel, discordId: params.discordId, telegramUsername: params.telegramUsername, diff --git a/frontend/src/routes/Register.svelte b/frontend/src/routes/Register.svelte index e601d59..53d841e 100644 --- a/frontend/src/routes/Register.svelte +++ b/frontend/src/routes/Register.svelte @@ -1,7 +1,7 @@