Skip to content

Instantly share code, notes, and snippets.

@jollyjoker992
Created July 14, 2025 07:35
Show Gist options
  • Save jollyjoker992/7517ac8062871d7a7ff118f6fef76e55 to your computer and use it in GitHub Desktop.
Save jollyjoker992/7517ac8062871d7a7ff118f6fef76e55 to your computer and use it in GitHub Desktop.
Feral File — Unified Authentication Proposal

Feral File — Unified Authentication Proposal

Version 3 · 2025‑07‑14 – for team review

Goal: One crypto primitive (Ed25519) for everything (JWT, playlist signatures), while letting wallets (EVM secp256k1, Tezos ed25519/P‑256) and classic passkeys (P‑256) coexist. Resource services must verify tokens offline; only Auth‑Server mints them.


1 · Identity anchors

Actor class Trust anchor Example sub (JWT)
Human (web / mobile) Client‑generated Ed25519 key bound at first login did:key:z6Mk…
FF1 Device TPM‑sealed Ed25519 DeviceKey did:key:z6Mkhw…
Internal service Vault‑held Ed25519 key svc:search
Automation agent Vault‑held Ed25519 key agent:curate‑v1

Every public signing key is exposed in JWKS form so all verifiers share one parser.


2 · Token types

Token TTL Carries
Access 15 min subaudexpscopeactor_typejti
Refresh 7 days Opaque handle → mint new Access
Device 30 days Same as Access + device scopes (device:send)

All tokens are EdDSA‑signed JWTs; resource services verify signature + claims with cached JWKS — no hop back to Auth‑Server.


2.1 What is the jti?

The jti (JWT ID) is a unique identifier (e.g. a UUID-v7) included in each token to prevent replay attacks:

  • Uniqueness: Auth-Server generates a fresh jti for every issued token.
  • Replay guard: For short-lived flows (Client-assertion and Access tokens), Auth-Server stores the jti in Redis with a TTL of 2 × token lifetime. If it sees the same jti again, it rejects the request with 401 Unauthorized.
  • Auditability: jti can be logged to trace exactly which token was used for a specific API call.

This mechanism ensures that even if an attacker steals a valid token or assertion, reusing it is infeasible once the jti has been seen.


All tokens are EdDSA‑signed JWTs; resource services verify signature + claims with cached JWKS — no hop back to Auth‑Server.‑signed JWTs**; resource services verify signature + claims with cached JWKS — no hop back to Auth‑Server.


3 · Why Ed25519?

  • Compact (32 B pub, 64 B sig) → cheaper playlists
  • Constant‑time, audit‑friendly
  • Faster verify (~2× vs P‑256 on Intel N100)
  • Already used for DP‑1 capsules → one code path

4 · Human login & key‑bind flows

4.1 Common steps — OAuth2 + PKCE in detail

  1. Generate PKCE pair (client‑side)

    code_verifier  = 32 random bytes → BASE64URL
    code_challenge = BASE64URL( SHA‑256(code_verifier) )
    
  2. Kick off the authorisation request

    GET /authorize?
        response_type            = code
        client_id                = ff-web
        redirect_uri             = https://app.feralfile.com/callback
        scope                    = playlist:write follow:*
        code_challenge           = <code_challenge>
        code_challenge_method    = S256
        state                    = <128‑bit random>
    
  3. Auth‑Server renders login UI and returns a one‑time nonce (also feeds wallet/passkey challenge).

  4. Client completes credential proof and signs the same nonce with the freshly generated Ed‑key (see Branch A/B below).

  5. Auth‑Server validates proofs → issues an authorization code (TTL 60 s, single‑use) via 302 back to the redirect_uri:

    https://app.feralfile.com/callback?code=<auth_code>&state=<state>
    
  6. Client immediately exchanges the code:

    POST /token
        grant_type     = authorization_code
        code           = <auth_code>
        code_verifier  = <original 43‑byte verifier>
        client_id      = ff-web
        redirect_uri   = https://app.feralfile.com/callback
    

    Auth‑Server hashes code_verifier, checks it equals the stored code_challenge; mismatch ⇒ 400.

  7. Auth‑Server returns tokens differently for web vs mobile:

  • Web (browser‑based client_id = ff-web) — replies 200 OK with headers:

    Set-Cookie: ff_access=<JWT>; Max-Age=900; Path=/; Secure; HttpOnly; SameSite=Lax
    Set-Cookie: ff_refresh=<opaque>; Max-Age=604800; Path=/auth/token; Secure; HttpOnly; SameSite=Strict

    Response body:

    { "token_type": "cookie" }

    Access token travels automatically in subsequent HTTPS requests as the ff_access cookie. JavaScript cannot read or modify it (HttpOnly) → mitigates XSS token theft.

  • Mobile / CLI — keeps the previous JSON payload:

    {
      "access_token":  "<JWT – 15 min>",
      "refresh_token": "<opaque – 7 days>",
      "token_type":    "Bearer",
      "expires_in":    900
    }

Client storage

  • Web – no storage API needed; browser handles cookies. Silent refresh happens when the front‑end POSTs /token (cookies accompany the call).
  • Mobile/CLI – SDK caches tokens in OS secure storage and adds Authorization: Bearer header on each request. (memory / secure-storage); subsequent API requests add Authorization: Bearer <access_token>.
  1. Silent refresh: SDK calls /token with grant_type=refresh_token before expiry to roll tokens.

Why PKCE? If an attacker steals the short‑lived authorisation code (step 5) they still cannot finish step 6 because they do not know the secret code_verifier. This removes the need for a hidden client‑secret in SPA/mobile apps.

  1. After an Ed‑key is bound, the client switches to the lighter client‑credentials + client‑assertion flow (EdDSA‑signed JWT) and no longer needs /authorize.

4.2 Branch A · Wallet (chain‑native key — secp256k1 for EVM, ed25519/P‑256 for Tezos) Branch A · Wallet (chain‑native key — secp256k1 for EVM, ed25519/P‑256 for Tezos)

sequenceDiagram
    participant B as Browser
    participant W as Wallet
    participant A as Auth‑Server

    B->>W: personal_sign("Bind #nonce")
    W-->>B: wallet_sig
    Note over B: generate Ed25519 key (WebCrypto)
    B->>A: POST /bind-wallet {ed_pub, wallet_sig, ed_sig, nonce}
    A-->>B: 200 OK (did:key…)
Loading

ed_sig = Ed-key signs same nonce → prevents key‑injection.

4.3 Branch B · Passkey (P‑256)

sequenceDiagram
    participant M as Mobile App
    participant A as Auth‑Server

    M->>A: WebAuthn (P‑256) response
    A-->>M: nonce
    Note over M: generate Ed‑key in Secure Enclave
    M->>A: /bind-passkey {ed_pub, passkey_sig, ed_sig, nonce}
    A-->>M: did:key bound
Loading

Older browsers repeat this once; modern alg‑‑8 passkeys skip bind (they are Ed25519).

4.4 Credential storage & JWKS

Each user gets a JWKS‑compatible list at GET /v1/users/{uid}/jwks:

{"keys":[{"kid":"browser‑20250714","kty":"OKP","crv":"Ed25519","x":"11qY…","use":"sig"},]}

Verifiers use the same helper they use for global JWKS.


5 · Service ↔ Auth‑Server (client‑credentials)

  1. Service loads its Ed private key from Vault.
  2. Mints 60‑s client‑assertion JWT (jti = uuid‑v7).
  3. POST /token grant_type=client_credentials client_assertion=<jwt> scope=search:index.
  4. Replay guard — Auth‑Server stores each jti in Redis for 2× TTL → duplicate ⇒ 401.
  5. Auth‑Server returns 5‑min Access JWT (actor_type="service").

Resource API checks actor_type, signature, scopes; no DB hit.


6 · FF1 Device bootstrap

  1. Factory records pubkey → serial in Device Registry.
  2. First boot: FF1 signs nonce ➜ /auth/device {pub, sig}.
  3. Auth‑Server validates row, issues 30‑day Device JWT.
  4. Device stores JWT in RAM; refreshes when online.

7 · Backup & recovery strategy

Web Mobile
No backup by default – every browser generates its own Ed‑key; if lost, user re‑logs with wallet/passkey, binds a new key. Key stored in Secure Enclave / Keystore and syncs via OS backup.
Optional export – SDK can exportKey("pkcs8"), encrypt with passphrase (Argon2 + AES‑GCM) ➜ user downloads JSON/QR for cold backup.

App settings expose a “Trusted browsers” list (kid, added_at) with Revoke button.


## 8 · Scope & ACL chain

  1. Auth‑Server: granted = requested ∩ client_allow ∩ role.
  2. Resource middleware: RequireScope("playlist:write").
  3. Casbin (Orbit 1‑2) or OPA (Orbit 3) for row/attr rules.

9 · Orbit rollout

Orbit Target date Auth milestone
0 Q3‑2025 API‑GW verifies Ed25519 JWT; FF1 bootstrap live
1 Q4‑2025 Wallet + Passkey bind flows; Service client‑assertion; per‑user JWKS endpoint
2 Q1‑2026 Curator multi‑sig (playlist signatures[] use user JWKS)
3 Q3‑2026 OPA side‑car, geo/licence policies; secure‑element signing option

Key take‑aways

  • Client generates & owns the Ed25519 key → server never touches secrets.
  • Auth‑Server signs; everyone else verifies offline.
  • JWKS everywhere – global (/.well‑known/…) + per‑entity (/users/{uid}/jwks) = one parser.
  • Replay‑guard + 5‑min tokens keep leak blast‑radius tiny.

Ready for critique — highlight any edge‑case or integration concern the draft missed and we’ll fold it in.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment