Skip to content

HomeDocsDeveloperOAuth authorization

Developer

OAuth authorization

How Reqio's MCP server uses OAuth 2.1 with PKCE to authorize AI agents, issue opaque tokens, and enforce per-project audience binding.

The Reqio MCP server uses OAuth 2.1 with PKCE (RFC 7636 S256) to authorize agent connections. There are no API keys to paste. The agent discovers all endpoint URLs automatically via standard discovery documents, registers itself, and opens a browser consent screen where you approve the scopes you want to grant.

Access tokens are opaque and database-backed, not JWTs. Every token is audience-bound to a single project. The server re-reads live project membership on every tool call, so revoking a team member's access takes effect immediately without any token revocation step.

Token format
Opaque, cryptographically random, base64url-encoded. Only its SHA-256 hash is stored.
Verification
Database lookup on every call. No offline (stateless) verification.
Audience binding
Each token stores the project resource URL (https://reqio.app/p/{projectId}) as its audience. A mismatch returns TOKEN_AUDIENCE_MISMATCH.
Access token lifetime
1 hour (3600 seconds).
Refresh token lifetime
30 days, rotating on every use.
Authorization code lifetime
Single-use, approximately 60 seconds.

The handshake

  1. Discover the authorization server and resource server

    MCP clients read two well-known documents before any other step:

    GET /.well-known/oauth-authorization-server HTTP/1.1
    Host: reqio.app
    {
      "issuer": "https://reqio.app",
      "authorization_endpoint": "https://reqio.app/oauth/authorize",
      "token_endpoint": "https://reqio.app/api/oauth/token",
      "registration_endpoint": "https://reqio.app/api/oauth/register",
      "revocation_endpoint": "https://reqio.app/api/oauth/revoke",
      "scopes_supported": ["backlog:read", "status:write", "..."],
      "response_types_supported": ["code"],
      "grant_types_supported": ["authorization_code", "refresh_token"],
      "code_challenge_methods_supported": ["S256"],
      "token_endpoint_auth_methods_supported": ["none"]
    }
    GET /.well-known/oauth-protected-resource HTTP/1.1
    Host: reqio.app
    {
      "resource": "https://reqio.app",
      "authorization_servers": ["https://reqio.app"],
      "scopes_supported": ["backlog:read", "..."],
      "bearer_methods_supported": ["header"]
    }

    Because the Authorization Server and Resource Server are co-located, both resource and authorization_servers[0] point to the same origin. When a tool call arrives without a valid token, the server responds with a 401 and a WWW-Authenticate header pointing at the protected-resource document, which is how MCP clients locate discovery and begin the flow automatically:

    HTTP/1.1 401 Unauthorized
    WWW-Authenticate: Bearer realm="reqio",
      resource_metadata="https://reqio.app/.well-known/oauth-protected-resource"
  2. Register the client (RFC 7591)

    MCP agents register themselves dynamically before initiating authorization. Registration is open (no approval queue) and rate-limited.

    POST /api/oauth/register HTTP/1.1
    Host: reqio.app
    Content-Type: application/json
     
    {
      "redirect_uris": ["http://localhost:PORT/callback"],
      "client_name": "My Agent"
    }

    The response includes a client_id the agent uses in subsequent requests. No client_secret is issued: Reqio uses public clients only (token_endpoint_auth_methods_supported: ["none"]). The registered client is inert until a human approves a consent screen, which shows the client_id and the redirect host, not the self-declared client_name, so you can see exactly what is being connected.

  3. Build the authorization URL and redirect

    https://reqio.app/oauth/authorize
      ?response_type=code
      &client_id=CLIENT_ID
      &redirect_uri=http%3A%2F%2Flocalhost%3APORT%2Fcallback
      &scope=backlog%3Aread+status%3Awrite
      &state=RANDOM_STATE
      &code_challenge=BASE64URL_SHA256_OF_VERIFIER
      &code_challenge_method=S256

    PKCE S256 is required. code_challenge is BASE64URL(SHA256(code_verifier)) with no padding. state should be a random value the agent verifies on return.

  4. Approve the consent screen

    The Reqio consent screen shows the project name, the client ID and redirect host of the agent, and each requested scope in plain English with a description of what it allows. Scopes that exceed your plan entitlement or your role in the project are blocked and will not appear: you cannot accidentally grant more authority than you hold.

  5. Exchange the code for tokens

    After approval, the browser is redirected to the agent's redirect_uri with code and state parameters. The agent exchanges the code at the token endpoint:

    POST /api/oauth/token HTTP/1.1
    Host: reqio.app
    Content-Type: application/x-www-form-urlencoded
     
    grant_type=authorization_code
    &code=AUTH_CODE
    &redirect_uri=http%3A%2F%2Flocalhost%3APORT%2Fcallback
    &client_id=CLIENT_ID
    &code_verifier=CODE_VERIFIER

    The endpoint also accepts application/json. On success:

    {
      "access_token": "OPAQUE_TOKEN",
      "token_type": "Bearer",
      "expires_in": 3600,
      "refresh_token": "OPAQUE_REFRESH_TOKEN",
      "scope": "backlog:read status:write"
    }

    Authorization codes are single-use and short-lived. PKCE verification runs on every exchange; a mismatched verifier is rejected.

  6. Make authenticated MCP calls

    The agent sends the access token as a Bearer credential on every request to the project's MCP endpoint (https://reqio.app/p/{projectId}/mcp). The server verifies the token, checks its audience and scope, then re-runs assertCan against live project membership before executing the tool. See Available tools for the full per-call pipeline.

Token refresh

POST /api/oauth/token HTTP/1.1
Host: reqio.app
Content-Type: application/x-www-form-urlencoded
 
grant_type=refresh_token
&refresh_token=REFRESH_TOKEN
&client_id=CLIENT_ID

Each refresh issues a new access token and a new refresh token. The old refresh token is immediately revoked.

Refresh token reuse is treated as a compromise signal

If a previously rotated refresh token is presented again (reuse detection), the server revokes the entire successor chain rather than issuing new tokens. This forces the agent to re-authorize from scratch, on the assumption that a rotated token being replayed means it leaked.

Token revocation (RFC 7009)

POST /api/oauth/revoke HTTP/1.1
Host: reqio.app
Content-Type: application/x-www-form-urlencoded
 
token=TOKEN_TO_REVOKE
&client_id=CLIENT_ID

The endpoint always returns 200, even for unknown tokens, to avoid leaking whether a given token exists. Revocation deletes the token row; the agent loses access on the next tool call. You can also revoke any active grant from Dashboard → Settings → Connected apps by clicking Revoke next to the connection.

Scopes

Each scope maps to a required OAuth 2.1 scope string and an internal capability from the project permission matrix (packages/core/src/config/permissions.json). The server checks both independently on every call: the scope says what the agent was consented to attempt, assertCan says whether the acting user's role actually holds that capability.

Read-only scopes are available to every connected project on every plan:

backlog:readscope
Read feature requests, stats.
conversations:readscope
List and read the private inbox.
widget:readscope
Read widget and branding config.
members:readscope
List project members and invitations.
email:readscope
Read email settings.
identity:readscope
Read identity integration status. Never exposes the secret.
requesters:readscope
Read the tracked-requester roster.
notifications:readscope
Read the notification history.
analytics:readscope
Read the team activity feed and per-member analytics. Restricted to OWNER and SUPERVISOR roles at the capability layer.
ship:readscope
Read the ship queue (completed requests still marked "coming in the next update"). Narrower than backlog:read: excludes SUPPORT-role connections at the capability layer.

Write scopes require mcpWrite: true on the project owner's plan. Every plan, including Free, has mcpWrite: true (Free at a lower daily call quota), so write access is not itself plan-gated - only the per-plan daily quota differs. See MCP server for the quota table.

backlog:writescope
Create new feature requests on the team's behalf.
status:writescope
Change request status.
notes:writescope
Set or clear internal developer notes.
comments:writescope
Post comments on requests.
comments:deletescope
Permanently delete comments.
features:deletescope
Permanently delete feature requests.
conversations:writescope
Reply to inbox threads; convert reports to features; re-triage or mark threads read.
broadcasts:writescope
Send project-wide announcements.
widget:writescope
Update widget and branding config.
members:writescope
Invite members, change roles, revoke or resend invitations. Restricted to OWNER and SUPERVISOR roles.
members:deletescope
Remove members. Restricted to OWNER and SUPERVISOR roles.
project:writescope
Rename the project.
email:writescope
Update email settings.
identity:writescope
Toggle the widget's post-submit email affordance. Never generates or rotates the signing secret.

Authority model

Token scopes define what an agent is permitted to attempt. The project permission matrix (assertCan) defines whether the acting user actually holds that permission. Both checks must pass on every call.

Removing a member neuters their agent immediately

Granting members:write to an agent acting as a DEVELOPER-role member is denied at the capability check, even if the scope was consented, because inviteMembers and removeMembers are OWNER/SUPERVISOR-only in the permission matrix. Removing a member from the project has the same immediate effect on their agent: the next tool call fails the assertCan check and returns FORBIDDEN, without any explicit token revocation step.