An encrypted personal cloud built on the AT Protocol.

Authentication#

Login (OAuth)#

Default login flow. Uses AT Protocol OAuth 2.0 with DPoP (Demonstrating Proof-of-Possession) and a loopback redirect server for the browser-based authorization.

sequenceDiagram
    participant User
    participant CLI
    participant Browser
    participant PDS/AS

    User->>CLI: opake login <handle>
    CLI->>CLI: Resolve PDS from handle (public API → DID doc)

    CLI->>PDS/AS: GET /.well-known/oauth-protected-resource
    PDS/AS-->>CLI: { authorization_servers: [<as_url>] }
    CLI->>PDS/AS: GET /.well-known/oauth-authorization-server
    PDS/AS-->>CLI: AS metadata (endpoints, PAR, DPoP algs)

    CLI->>CLI: Generate DPoP keypair (P-256), PKCE S256, state

    CLI->>PDS/AS: POST /oauth/par (DPoP proof, PKCE challenge, redirect_uri)
    PDS/AS-->>CLI: { request_uri, expires_in }

    CLI->>CLI: Start loopback server on 127.0.0.1
    CLI->>Browser: Open authorization URL
    Browser->>PDS/AS: User authorizes
    PDS/AS->>Browser: Redirect to 127.0.0.1/callback?code=...&state=...
    Browser->>CLI: GET /callback?code=...&state=...

    CLI->>CLI: Verify state (CSRF check)
    CLI->>PDS/AS: POST /oauth/token (code, PKCE verifier, DPoP proof)
    PDS/AS-->>CLI: { access_token, refresh_token, sub }

    CLI->>CLI: Save OAuthSession (tokens + DPoP key)
    CLI->>CLI: Load or generate X25519 keypair

    CLI->>PDS/AS: com.atproto.repo.putRecord (publicKey/self, DPoP auth)
    PDS/AS-->>CLI: { uri, cid }

    CLI->>User: Logged in as <handle> (OAuth)

The loopback server times out after expires_in seconds (from the PAR response). If OAuth discovery fails, the CLI falls back to legacy password authentication with a warning.

Login (Legacy)#

Password-based authentication via createSession. Used when --legacy is passed or when the PDS doesn't support OAuth discovery.

sequenceDiagram
    participant User
    participant CLI
    participant PDS

    User->>CLI: opake login <handle> --legacy
    CLI->>CLI: Resolve PDS from handle
    CLI->>User: Password prompt (or OPAKE_PASSWORD env)
    User-->>CLI: password

    CLI->>PDS: com.atproto.server.createSession
    PDS-->>CLI: { did, handle, accessJwt, refreshJwt }

    CLI->>CLI: Save account config + session tokens
    CLI->>CLI: Load or generate X25519 keypair

    CLI->>PDS: com.atproto.repo.putRecord (publicKey/self)
    PDS-->>CLI: { uri, cid }

    CLI->>User: Logged in as <handle>

The putRecord call is idempotent — same key, same record. Safe to call on every login.

Token Refresh#

Transparent to the user. The XRPC client detects expired tokens and refreshes automatically. The refresh path depends on the session variant.

Legacy Refresh#

sequenceDiagram
    participant CLI
    participant PDS

    CLI->>PDS: Any XRPC call (expired accessJwt)
    PDS-->>CLI: 400 ExpiredToken

    CLI->>PDS: com.atproto.server.refreshSession (refreshJwt)
    PDS-->>CLI: { accessJwt, refreshJwt } (new tokens)

    CLI->>CLI: Update stored session

    CLI->>PDS: Retry original XRPC call (new accessJwt)
    PDS-->>CLI: Success

OAuth Refresh#

sequenceDiagram
    participant CLI
    participant AS

    CLI->>AS: Any XRPC call (expired access_token, DPoP proof)
    AS-->>CLI: 400 ExpiredToken

    CLI->>AS: POST /oauth/token (grant_type=refresh_token, DPoP proof)
    AS-->>CLI: { access_token, refresh_token } (new tokens)

    CLI->>CLI: Update stored OAuthSession

    CLI->>AS: Retry original XRPC call (new access_token, DPoP proof)
    AS-->>CLI: Success

DPoP nonces are captured from every response and included in subsequent proofs. If the server returns a use_dpop_nonce error, the client retries once with the fresh nonce.