Docs/Build an App/Security best practices

Security best practices

Your app holds tokens that spend other people's credits. Go through this list before you submit for review.

The authorization flow

Always send and check state

Generate a random state per attempt, store it in the user's session, and reject the callback if it does not match. This blocks cross-site request forgery that would connect a victim to an attacker's account.

Use PKCE, even with a client secret

PKCE (S256) is required for public clients and recommended for all. It makes a stolen code useless without the verifier. Generate a new verifier for every authorization.

Register exact redirect URIs

Redirect URIs must match exactly. Register only the URIs you use, on https, on domains you control. Do not add a redirect page that forwards to arbitrary URLs.

Request minimal scopes

Each scope is something an attacker gets if your storage leaks. See least privilege.

Secrets and tokens

Never ship the client secret to a browser or mobile app

Anything in a JavaScript bundle, mobile binary, desktop app or CLI can be extracted. Those apps are public clients: use token_endpoint_auth_method=none with PKCE, or run the token exchange on your server. If a secret leaks, rotate it in Dashboard > Developer apps right away.

Store tokens encrypted, server side

Keep access and refresh tokens in your database, encrypted at rest with a key that is not in the same database. Do not put them in localStorage, cookies readable by JavaScript, URLs or logs.

Revoke when your user deletes their account

When a user deletes their account in your product, or disconnects RocketVerifier from your settings page, call /api/oauth/revoke with the refresh token, then delete the stored tokens.

Refresh tokens

Persist rotated refresh tokens atomically

Every refresh returns a new refresh token and kills the old one. Save the new pair in one write before you use it. If you lose the new refresh token (a crash between the response and the write), the user has to connect again.

Serialize refreshes per connection

Two workers that refresh the same connection at the same time will send the same refresh token twice. The second one looks like token theft and revokes the whole grant. Take a lock per user (a database row lock or a Redis lock) around the refresh, and re-read the tokens after you get the lock in case another worker already refreshed.

Pseudocode
with lock(f"rv-refresh:{user_id}"):
    tokens = db.load_tokens(user_id)          # re-read inside the lock
    if tokens.expires_at > now() + 60:
        return tokens                          # another worker already refreshed
    new = oauth.refresh(tokens.refresh_token)
    db.save_tokens(user_id, new)               # one write, before first use
    return new

The Node.js SDK already refreshes only once for concurrent calls on the same client instance. You still need a lock when several processes share a connection.

Treat invalid_grant as disconnected

A refresh that fails with invalid_grant means the user disconnected, the grant was revoked, or the refresh token expired. Do not retry. Delete the tokens and show the Connect RocketVerifier button again.

Webhooks

Verify grant.revoked signatures

Set a webhook URL on your app and check X-RocketVerifier-Signature against the raw body with your app's webhook secret before you act on the event. Reject timestamps older than 5 minutes. See Verifying Signatures.

Make the handler idempotent

Webhooks can be delivered more than once. Deduplicate on event_id, and make deleting tokens that are already gone a no-op.

User experience

Show which account is connected

Call GET /v1/me after connecting and show the email, the account (personal or organization) and the credit balance, with a Disconnect button next to it.

Explain 402 instead of retrying

A 402 means the connected account is out of credits or hit the monthly cap the user set for your app. Tell the user and link them to their RocketVerifier billing page. Retrying will not help.

More in Build an App