Building an OAuth app against Nodaro

Use this skill when:

If instead you just need server-to-server access to your OWN account, use API tokens (/v1/api-tokens) — simpler. See the using-nodaro-sdk skill.

Overview

Nodaro implements OAuth 2.0 authorization-code flow (RFC 6749) + token revocation (RFC 7009). The flow:

Browser → ThirdParty → Nodaro consent → ThirdParty server → Access token → API calls

7 steps:

  1. User clicks “Connect to Nodaro” on your site
  2. You redirect to Nodaro’s /oauth/authorize?...
  3. User signs in (if needed) and clicks “Allow”
  4. Nodaro redirects back to your redirect_uri with ?code=...&state=...
  5. Your server exchanges the code at /v1/oauth/token (with client_id + client_secret)
  6. Nodaro returns access_token, scope, expires_in
  7. Your server uses the token in Authorization: Bearer <token> headers

Step 1: Register your app

Visit <nodaro-instance>/settings/developer-apps and click “New developer app”. Fill in:

On Save: you get clientId (app_<32hex>) AND clientSecret (sec_<64hex>). The secret is shown ONCE. Store it securely (encrypted env var, secrets manager).

You can rotate the secret at any time via the “Rotate secret” button — invalidates the old one immediately.

Step 2: Scope vocabulary

Eleven scopes, all of which exist in your app’s scopes_requested:

Scope What it grants Routes gated
workflows:read Read user’s workflows GET /v1/projects/:projectId/workflows, GET /v1/workflows, GET /v1/workflows/:id, GET /v1/workflows/:id/export
workflows:write Create / modify workflows POST /v1/projects/:projectId/workflows, POST /v1/workflows, PATCH /v1/workflows/:id, DELETE /v1/workflows/:id, POST /v1/workflows/import, POST /v1/workflows/:parentId/sub-workflows
workflows:execute Run workflows POST /v1/workflows/:id/run; prompt-wizard MCP tools
jobs:read Read job status / results GET /v1/jobs/status, GET /v1/jobs/:id, GET /v1/jobs/:id/status, GET /v1/jobs, POST /v1/jobs/batch-status
assets:read Read user’s uploaded assets (reserved)
assets:write Upload assets (reserved)
credits:read See user’s credit balance (reserved)
apps:read Read public apps (reserved)
pipelines:read Read Story-to-Video pipelines GET /v1/pipelines/*
pipelines:execute Run / branch pipeline stages POST /v1/pipelines/:id/branch and run routes
pipelines:approve Approve pipeline stage output pipeline approval routes; the scene-helper routes (POST /v1/pipelines/:id/entities/:sceneId/helpers/*)

Request only what you need. Users see the requested scopes on the consent screen; over-asking causes drop-off.

Step 3: Build the authorization redirect

When the user clicks “Connect to Nodaro” on your site:

https://nodaro.example.com/oauth/authorize?
  client_id=app_xxxxx&
  redirect_uri=https://yourapp.com/oauth/callback&
  response_type=code&
  scope=workflows:read+workflows:execute&
  state=<RANDOM_CSRF_TOKEN>

Required params:

// Example: building the URL
const state = crypto.randomBytes(16).toString("hex")
await db.oauthState.set(state, { userId, expiresAt: Date.now() + 600_000 })

const params = new URLSearchParams({
  client_id: process.env.NODARO_CLIENT_ID!,
  redirect_uri: "https://yourapp.com/oauth/callback",
  response_type: "code",
  scope: "workflows:read workflows:execute",
  state,
})
res.redirect(`https://nodaro.example.com/oauth/authorize?${params}`)

Step 4: Exchange code for token (server-side)

The user clicks “Allow” → Nodaro redirects to: https://yourapp.com/oauth/callback?code=ndr_code_<48hex>&state=<your_state>

ALWAYS verify state first. Then exchange on your server (NOT browser — secret would leak):

Via @nodaro/sdk SDK

import { createClient, StaticTokenAuth } from "@nodaro/sdk"

const client = createClient({
  baseUrl: "https://nodaro.example.com",
  auth: new StaticTokenAuth(""),  // /v1/oauth/token is public
})

const tokens = await client.oauth.exchangeCode({
  client_id: process.env.NODARO_CLIENT_ID!,
  client_secret: process.env.NODARO_CLIENT_SECRET!,
  code: req.query.code as string,
  redirect_uri: "https://yourapp.com/oauth/callback",
})
// tokens.access_token  → "ndr_app_<64hex>"
// tokens.token_type    → "Bearer"
// tokens.scope         → "workflows:read workflows:execute"
// tokens.expires_in    → 7776000  (90 days in seconds)

Via curl / fetch

curl -X POST https://nodaro.example.com/v1/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "authorization_code",
    "client_id": "app_...",
    "client_secret": "sec_...",
    "code": "ndr_code_...",
    "redirect_uri": "https://yourapp.com/oauth/callback"
  }'

Notes:

Step 5: Use the token

const userClient = createClient({
  baseUrl: "https://nodaro.example.com",
  auth: new StaticTokenAuth(tokens.access_token),
})

const projects = await userClient.projects.list()
// Or:
const exec = await userClient.workflows.run(workflowId, { nodeIds: ["text-1"] })

Tokens live 90 days. After expiry: re-prompt the user for consent (re-do /oauth/authorize). No refresh tokens in the MVP — design choice for simplicity.

Step 6: Store tokens securely

Step 7: Revocation

When the user disconnects:

await client.oauth.revoke(token)

Or curl:

curl -X POST https://nodaro.example.com/v1/oauth/revoke \
  -H "Content-Type: application/json" \
  -d '{ "token": "ndr_app_..." }'

Per RFC 7009: always returns 200. Subsequent API calls with the revoked token return 401. Users can also revoke from /settings/developer-apps on the Nodaro side.

Common errors

Error Status Cause Fix
invalid_client 401 Wrong client_id or client_secret Verify creds. Old secret dead after rotation.
invalid_grant 400 Code expired (>10 min), reused, or redirect_uri mismatch New code; ensure URIs are byte-for-byte identical
invalid_scope 400 Requested scope not in scopes_requested Edit your app to widen scopes_requested first
invalid_redirect_uri 400 URI in /authorize doesn’t match registered list Add or fix in app settings

Plus standard API errors after token use:

The SDK’s ForbiddenError exposes missingScope directly:

catch (err) {
  if (err instanceof ForbiddenError && err.missingScope === "workflows:execute") {
    // Re-prompt user for consent including workflows:execute
    redirectToAuthorize([currentScopes, "workflows:execute"])
  }
}

Security checklist

Differences from RFC 6749 / 7009

Reference