Skip to content

Authentication

Picora supports two authentication modes:

ModeCarrierBest for
API KeyAuthorization: Bearer sk_live_…Server-side scripts, MCP tools, desktop apps (PicGo / Moraya)
OAuth 2.1Authorization: Bearer <access_token>Third-party web apps acting on a user’s behalf, first-party SSO

Both ride the same Authorization header — the format alone tells the server which path to take.

API Keys are 40-character secrets with the prefix sk_live_. Created in center.picora.me/integration, they are per-user, revocable, and scoped so you can delegate the minimum permission your tool needs.

v2 fine-grained scopes (v0.31+)

ScopeAllows
media.readList / download / read metadata of images / videos / audio
media.writeUpload + edit metadata
media.deletePermanent deletion (irreversible — pick carefully)
kb.readList KBs, fetch manifests, read documents
kb.writeCreate / update / delete documents inside a KB
account.readRead profile (email, nickname, plan)
usage.readRead storage / bandwidth quota counters

Two presets in the UI cover most tools:

  • AI / MCP read-onlymedia.read + kb.read + account.read + usage.read
  • Creator (read + write, no delete) — adds media.write + kb.write

Legacy v1 scopes

Keys created before v0.31 use three coarse tiers (read / read_write / read_write_delete). They continue to work — the server expands them transparently:

v1Equivalent v2 set
readmedia.read, kb.read, account.read, usage.read
read_write+ media.write, kb.write
read_write_delete+ media.delete

You can keep using your existing v1 Keys; new Keys created in the dashboard are v2.

Storage

  • Servers store only the SHA-256 hash of your Key. The plaintext is shown once at creation; copy it immediately.
  • Revoking a Key in the dashboard takes effect within seconds (a revoked:{keyHash} flag is written to a global cache, beating the 1-hour token cache TTL).

OAuth 2.1 + PKCE

For web apps acting on a user’s behalf, use OAuth Authorization Code grant + PKCE:

  1. Register your app at POST /oauth/register (RFC 7591 dynamic registration). New apps land in pending and need allowlist approval — the four built-in clients (Claude Desktop, Cursor, Continue, Claude.ai) are auto-approved.
  2. Direct the user to GET /oauth/authorize?client_id=…&redirect_uri=…&scope=…&code_challenge=…&code_challenge_method=S256&state=….
  3. Picora redirects to https://center.picora.me/oauth/consent for the human consent step.
  4. After approval Picora redirects to your redirect_uri with ?code=…&state=….
  5. Exchange the code at POST /oauth/token with code_verifier to get an access_token (RS256 JWT, 1 h) plus a rotating refresh_token (90 d).

First-party SSO (v0.30+)

Moraya Web is a Picora first-party application. When a logged-in Picora user reaches its consent URL, the consent UI is skipped and the authorization code is signed directly — provided the user’s plan satisfies the app’s required_plan gate. The OAuth dance otherwise follows the standard PKCE flow.

If you build a first-party Picora client, we’ll seed it into the oauth_clients table during deployment with is_first_party = 1 — no UI banner is shown to your users.

OIDC

When the requested scope contains openid, the token endpoint additionally returns an id_token (RS256 JWT). The discovery documents are at:

Common pitfalls

  • Mixing modes — passing apiKey and oauthToken to createPicoraClient() throws at construction; pick one.
  • Using a v1 Key with media.delete only — there is no v1 scope that maps to delete-only; either upgrade the Key to v2 (recreate) or keep the broader read_write_delete v1 tier.
  • CORS in browsers — only picora.me, picora.cn, web.moraya.app, center.picora.*, and localhost:5173 / :4321 are allowed origins for credentialed Web requests. Tools sending Authorization: Bearer sk_live_… get Access-Control-Allow-Origin: * automatically (no Cookie risk).
  • Refresh-token replay — if the OAuth server detects a previously-rotated refresh token being reused, the entire token chain for that client is revoked and the user receives an email alert. Always use the freshest token your refresh call returned.