← library
Self-serve, light gate
Work management / project & task API·agent-generated · deepseek-v4.1-flash·checked 2026-10-28
difficulty
2/5

OAuth is self-serve: create an app in the developer console, register scopes, and run the authorization-code flow in an afternoon. The one real gate is Manage distribution — leave 'Specific workspaces' empty and every user hits 'This app is not available to your Asana workspace'.

First working connection
~1-3 hours
Production / approved
same day if you stay in your own workspaces; +~2-4 weeks only if you list in the App Directory (inferred)
Composio managed OAuth
Available: ship today
Composio lists a managed OAuth app for this toolkit — you can ship on it while your own app is set up or reviewed.
who does itagent 5you 3provider 0
evidencetested 3sourced 30inferred 10
◆ agent run9 pages read26/33 quotes verified verbatim40s$0.024deepseek-v4.1-flash

01Pick your path

Personal access token (skip OAuth)
when Solo script, internal bot account, or a spike to prove the API call before building consent.
→ Working API calls in minutes, but every action is attributed to the token's owner and there is no multi-user consent.
Standard OAuth app (authorization code + PKCE)
when Any agent that acts on behalf of multiple users. This is the main path.
→ Per-user tokens, 1-hour access tokens + refresh, scoped consent screen, distribution controlled per workspace or globally.
MCP app + Asana's hosted MCP server
when You are plugging an MCP client into Asana rather than writing your own tool calls.
→ Your MCP client gets a token usable only against https://mcp.asana.com/v2/mcp; no scopes to pick; not usable for direct REST calls.
Service Account
when Enterprise org-wide automation created by an Asana super admin — not a substitute for user consent.
→ Org-wide long-lived token with separate authorization scopes (SCIM, Audit Logs, Exports, Workspace Events, AI Studio).

02Time & effort

Hands-on work
2.3 h – 7.8 h
Waiting on others
0 – 4 weeks
You're actually working
1% of elapsed time
Phases
5
start↑ first working connection, then production pathproduction
  1. DEVCreate app + register scopes10–20 minutessourced[S1][S6]You must be signed in to the developer console; an agent cannot click Create new app for you.
  2. DEVImplement code + PKCE flow1–3 hourssourced[S1][S9]An OAuth library plus the documented endpoints; the agent can write all of this once it has client ID/secret.
  3. DEVTest consent in your workspace10–30 minutessourced[S1][S6]Consent only renders if the app is available in the user's workspace via Manage distribution.
  4. PRODHarden: refresh, 429s, per-user tokens1–4 hourssourced[S1][S3]Access token is 1 hour; rate limits are per token, so one integration instance = one token bucket.
  5. PRODPublic listing review (optional)wait 0–4 weeksinferred[S4][S6]Sources describe the App Directory as a place to publish apps but state no review timeline or requirements; 2-4 weeks is our estimate, not documented. If you only need your own workspaces, skip this phase entirely.
agent can do it you waiting on providerbar widths ∝ √time, so short tasks stay visible

03Steps

0/8 done

Get a working connection

  1. Create the app in the developer consoleyousourced ❝[S1][S6]

    Developer console → My apps → Create new app. Pick the standard API app type (MCP apps issue tokens that only work against the MCP server). Fill app name, the exact https redirect URL, and your initial scope set. Changing the name, icon, or scopes later is allowed, but the redirect URL must match byte-for-byte at authorize time.

    “To build a proper OAuth flow, you must supply your new application with three key details:” — [S1] verified verbatim
    ◆ prepared by agent · App registration checklist
    App name: <agent name> — <one-line purpose, e.g. 'drafts and files status updates in Asana'>.
    Redirect URL: https://<your-host>/callback  (native/CLI: urn:ietf:wg:oauth:2.0:oob)
    App type: standard API app (not 'MCP app' unless you only call mcp.asana.com/v2/mcp).
    Permission scopes: tasks:read, tasks:write, projects:read, stories:write, users:read
    Distribution: Specific workspaces → <your test workspace> until the flow works.
  2. Generate the authorization request (PKCE)agentsourced ❝[S1]

    Build the URL against https://app.asana.com/-/oauth_authorize with client_id, redirect_uri, response_type=code, state, code_challenge/code_challenge_method=S256, and a space-delimited scope list that is URL-encoded (spaces → %20). Verify state on the callback before doing anything with the code.

    “The scope parameter is space-separated but must be URL-encoded in practice (i.e., spaces become %20 ).” — [S1] verified verbatim
    ◆ prepared by agent · Authorize URL with PKCE + narrow scopes
    const params = new URLSearchParams({
      client_id: process.env.ASANA_CLIENT_ID,
      redirect_uri: 'https://your-host/callback',
      response_type: 'code',
      state,
      code_challenge_method: 'S256',
      code_challenge,
      scope: 'tasks:read tasks:write projects:read stories:write users:read',
    });
    // Asana wants %20 for spaces, not '+'
    const url = 'https://app.asana.com/-/oauth_authorize?' +
      params.toString().replace(/\+/g, '%20');
  3. Select the narrowest scope set and check it is registeredagentsourced ❝[S1][S8]

    Start from read scopes, add write only for actions the agent actually performs, and check each one against the scope→endpoint table so you do not register a scope you never call. Remember write does not imply read. Note which features need the app-level Full permissions toggle because their endpoint has no scope.

    “Scopes allow your app to request only the access it needs, following the principle of least privilege.” — [S1] verified verbatim
    ◆ prepared by agent · Scope justification draft (consent screen / review)
    We ask for: tasks:read + projects:read (find the work you point us at), tasks:write (create and update tasks on your behalf), stories:write (post the comment/status we draft — we label these as written by the app), users:read (assign work to the right teammate). We do not request delete scopes. We store an hourly access token plus a refresh token server-side and revoke on disconnect.
  4. Exchange the code server-side and persist the refresh tokenagentsourced ❝[S1][S9]

    POST the code with client_id and client_secret to https://app.asana.com/-/oauth_token from your backend. Store the access token (1 hour) and the refresh token; re-run the exchange with the refresh token instead of prompting the user again. Revoke at https://app.asana.com/-/oauth_revoke on disconnect.

    “Once the access token expires, the application can use the token exchange endpoint again (i.e., without user intervention) to exchange the refresh token for a new access token.” — [S1] verified verbatim
  5. Turn on distribution for your workspace and run the consent round tripyousourced ❝[S6][S1][S2]

    In the developer console → Manage distribution, choose 'Specific workspaces' and tick your test workspace (or 'Any workspace'). Leaving 'Specific workspaces' empty produces 'This app is not available to your Asana workspace or organization.' on the consent screen. Then run a full authorize → allow → callback → GET /users/me loop.

    “If you choose "Specific workspaces" but don't select any workspaces, users will see an error saying "This app is not available to your Asana workspace or organization."” — [S6] verified verbatim

Make it production-ready

  1. Harden for production trafficagentinferred[S1][S3]

    Refresh proactively before expiry. Respect Retry-After on 429s, keep in-flight requests ≤50 GET / ≤15 writes, use limit=100 and opt_fields to reduce cost, and give each user or integration instance its own token so quotas do not stack into one bucket.

  2. Decide polling vs webhooks before you shipagentsourced ❝[S7][S3]

    Poll the Events API rather than standing up a webhook receiver unless real-time is a hard requirement — Asana's own guidance says to prefer polling and calls webhooks noisy, unreliable, and needing a backup mechanism. If you do add webhooks, budget a fallback poller.

    “Avoid using webhooks unless reacting to events in real time is a strict requirement for your app.” — [S7] verified verbatim
  3. Widen distribution and, if needed, list publiclyyouinferred[S6][S4]

    Flip Manage distribution to 'Any workspace' when you are ready for users outside your org. If you also want an App Directory listing, that is a separate submission whose requirements and turnaround are not documented in these sources — plan for a multi-week wait and have the scope justification ready.

04Requirements, scopes & nuances

You'll need

  • ▸An Asana account with access to the developer console and at least one workspace you can use for testing. inferred[S1]
  • ▸A registered app in the developer console, giving you a client ID and a client secret (the secret must stay server-side). sourced ❝[S1]
  • ▸An https redirect URL (non-native apps); native or CLI apps may use urn:ietf:wg:oauth:2.0:oob instead. sourced ❝[S1]
  • ▸Every scope you plan to request listed under OAuth → Permission scopes before the first authorization attempt. sourced ❝[S1]
  • ▸The target workspace added under Manage distribution ('Specific workspaces') or 'Any workspace' selected. sourced ❝[S6]
  • ▸A server-side token exchange — Asana does not support token requests from a browser. sourced ❝[S9]

Gotchas

  • !Registration happens in two places people forget: the OAuth tab (client ID/secret, redirect URL, scopes) and Manage distribution (which workspaces may authorize). Skipping the second one makes a perfectly correct flow fail at the consent screen. sourced ❝[S6]
  • !Scopes are pre-approved at registration time; an unregistered scope is not a warning, it is a hard forbidden_scopes error for the end user. inferred[S1]
  • !Scopes do not cover every endpoint, and the scope list includes no 'workspaces:write', no search-specific scope, and no batch scope — the fallback is the app-level Full permissions toggle, which grants access to all endpoints and must be justified to users. sourced ❝[S1][S8]
  • !write does not imply read (write does not grant read access). An agent that must read a task then comment on it needs tasks:read AND stories:write, not just a write scope. sourced ❝[S1][S8]
  • !Access tokens last one hour. If you do not persist and use the refresh token, users re-consent hourly — the single most common 'it worked in Postman' failure. inferred[S1]
  • !Rate limits are per authorization token (150 req/min free, 1,500 paid, 50 concurrent GET / 15 writes). Funnelling all users through one PAT shares one bucket; per-user OAuth tokens each get their own. inferred[S3]
  • !There is no OIDC discovery document at /.well-known/openid-configuration (404). Do not build a generic OIDC client that depends on it; use the oauth-authorization-server metadata or hardcode the documented endpoints. inferred
Scope / permissionTierNoteEvidence
tasks:readbasicGET /tasks, /tasks/{gid}, /projects/{gid}/tasks, subtasks, search. Start here; add write only if the agent mutates.sourced ❝[S8]
tasks:writesensitiveCreate/update/duplicate tasks, set parents, add projects/tags/followers. This is the core 'agent does work for you' scope.sourced ❝[S8]
stories:writesensitivePOST /tasks/{task_gid}/stories — i.e. commenting as the user. Users notice this one; justify it or offer read-only mode.sourced ❝[S8]
projects:readbasicList projects and a project's tasks so the agent can resolve names to gids before acting.sourced ❝[S8]
users:readbasicResolve assignees and get /users/me. Needed for almost any 'assign this to…' feature.sourced ❝[S8]
webhooks:writesensitiveOnly if the agent must react in real time; Asana's own guidance is to prefer polling unless real-time is a strict requirement.inferred[S8][S7]
tasks:delete / projects:deleterestrictedDestructive and irreversible from the API. Request only behind an explicit user action, never at first connect.inferred[S8]
Full permissions (app-level toggle)gatedNot a scope — a console toggle under OAuth > Permission scopes that grants access to all endpoints, including ones with no scope defined. If you need it, say why on the consent screen; a reviewer-grade justification is in the step artifact. Asana does not publish scope sensitivity tiers; basic/sensitive/restricted here are our classification.sourced ❝[S1][S8]
openid, email, profilebasicOnly if you use Asana as a login identity via OIDC, not for API access.sourced ❝[S8][S2]

05If you get blocked

✕Consent page shows a forbidden_scopes error and the user cannot approve.sourced ❝[S1]

→ Open the app in the developer console → OAuth → Permission scopes, add the scope, save, and rebuild the authorize URL with the same scope string.

✕User sees 'This app is not available to your Asana workspace or organization.'sourced ❝[S6][S1]

→ Developer console → Manage distribution → select 'Any workspace', or 'Specific workspaces' plus at least one workspace, then Save changes.

✕redirect_uri rejected or the code lands nowhere.sourced ❝[S1]

→ Byte-for-byte match the redirect_uri to the value saved in the app's OAuth tab; non-native apps must use https.

✕Token request from browser JS is refused.sourced ❝[S9][S1]

→ Move the token exchange behind your backend (or use the OOB redirect for a native/CLI client).

✕MCP app returns 'Invalid scope(s) requested', or its token 401s against /api/1.0.sourced ❝[S6]

→ Remove the scope parameter — MCP apps take no scopes — and remember MCP tokens cannot call the standard REST API; create a separate API app for that.

06Evidence: what was tested, sourced, and still uncertain

$ probes: live GETs with a dummy client_id, no credentials
✓ Discovery document (/.well-known/openid-configuration) → 404
https://app.asana.com/.well-known/openid-configuration
not published
∴ No OIDC discovery document. OIDC login may still exist per the docs, but a client that auto-configures from this URL will fail — plan on explicit endpoints.
✓ Discovery document (/.well-known/oauth-authorization-server) → 200
https://app.asana.com/.well-known/oauth-authorization-server
issuer=https://app.asana.com; authorization_endpoint=https://app.asana.com/-/oauth_authorize; token_endpoint=https://app.asana.com/-/oauth_token; scopes_supported=n/a
∴ The OAuth authorization-server metadata is live and confirms the authorize (/ -/oauth_authorize) and token (/-/oauth_token) endpoints, matching the docs. Note scopes_supported is 'n/a' — you cannot discover scopes from metadata; read the scopes doc.
✓ Authorize endpoint with a fake client_id → 200
https://app.asana.com/-/oauth_authorize?client_id=probe_invalid_client&response_type=code&redirect_uri=https%3A%2F%2Fexample.com%2Fcallback
text/html; charset=UTF-8 — body mentions "client_id%3Dprobe_invalid_client%26response_type%3Dcode%26redirect_ur"
∴ The consent endpoint responds 200 HTML even for a bogus client_id (the error surfaces in the rendered page, not as a 4xx). Do not use 'does it 200?' as a client-ID health check in your smoke tests — a real authorize round trip is the only reliable validation. No dynamic client registration exists, so an unregistered client_id can never be made valid at runtime.

Sources read

  1. [S1] OAuth official
  2. [S2] Authentication official
  3. [S3] Rate limits official
  4. [S4] Asana Docs official
  5. [S5] Webhooks official
  6. [S6] Integrating with Asana's MCP Server official
  7. [S7] Building the app official
  8. [S8] OAuth scopes official
  9. [S9] OAuth Code Samples / Libraries official
  10. [composio] Composio — Managed OAuth apps directory official
  11. [probe] Live HTTP probes run by the agent (dummy client_id, no credentials) probe

Still uncertain

  • ? No source states App Directory review requirements, timeline, or whether 'Any workspace' distribution alone is enough for a public commercial app. The 2-4 week figure is our estimate.
  • ? Asana publishes no scope sensitivity tiers. The basic/sensitive/restricted labels here are our judgement based on what each scope lets an app do (read, mutate, comment, delete), not provider guidance.
  • ? Discovery metadata returned scopes_supported=n/a, so scopes cannot be discovered programmatically — they must be read from the docs and hardcoded. We could not verify whether metadata lists grant types (probe output truncated).
  • ? The probes could not confirm client-ID validation behaviour beyond 'HTML returns 200'; the invalid-client error text was not readable, so we cannot say exactly what a developer sees with a mistyped client_id.
  • ? Timings for 'create app' and 'implement flow' are inferred from typical effort, not measured.
  • ? Service Account (org-wide) is gated to Enterprise and creatable only by Asana super admins; we did not verify the commercial or contractual requirements.
  • ? Generated automatically; no app was registered and no review was submitted.