OAuth 2.0
Connect external applications to Spedy using OAuth 2.0 with PKCE -- for AI tools, IDE extensions, and custom integrations.
Spedy includes a full OAuth 2.0 Authorization Server for connecting external applications. This is the primary way AI tools like Claude Desktop, Cursor, and other MCP-capable clients authenticate with Spedy.
The OAuth server implements RFC 6749 (Authorization Code Flow), RFC 7636 (PKCE), RFC 7591 (Dynamic Client Registration), RFC 8414 (Server Metadata), and RFC 9728 (Protected Resource Metadata).
Discovery
OAuth clients can discover Spedy's endpoints automatically using standard metadata endpoints:
GET /.well-known/oauth-authorization-server
GET /.well-known/oauth-protected-resourceThese are public endpoints that return the authorization server's capabilities, supported grant types, and endpoint URLs. They respect multi-tenant subdomains -- the metadata URLs are scoped to the requesting organization.
Dynamic Client Registration
External applications register themselves via RFC 7591 Dynamic Client Registration. This endpoint requires authentication -- clients must present a valid JWT.
POST /api/v1/oauth/registerRequest Body
| Field | Type | Required | Description |
|---|---|---|---|
| client_name | string | Yes | Display name for the application |
| redirect_uris | string[] | Yes | Allowed redirect URIs (must be HTTPS, except loopback) |
| client_uri | string | No | Homepage URL of the application |
| logo_uri | string | No | Logo URL for the consent screen |
Example Request
{
"client_name": "My IDE Extension",
"redirect_uris": ["https://my-extension.example.com/callback"]
}Example Response
{
"client_id": "abc123def456...",
"client_secret": "spd_...",
"client_name": "My IDE Extension",
"redirect_uris": ["https://my-extension.example.com/callback"],
"token_endpoint_auth_method": "client_secret_post",
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"]
}The client_secret is only returned once at registration time. Store it securely. Clients registered through DCR are confidential clients and must include the client_secret in token exchange requests.
Rate limit: 5 registrations per hour per IP.
Authorization Flow
Spedy uses the Authorization Code Flow with PKCE (S256). Plain code challenges are not supported.
1. Redirect the user to authorize
GET /api/v1/oauth/authorize| Parameter | Required | Description |
|---|---|---|
| response_type | Yes | Must be code |
| client_id | Yes | Your client ID |
| redirect_uri | Yes | Must match a registered redirect URI |
| state | Yes | CSRF token (minimum 32 characters) |
| code_challenge | Yes | PKCE S256 challenge |
| code_challenge_method | No | Must be S256 (default) |
| scope | No | Space-separated scopes (default: read write) |
| resource | No | Resource indicator (RFC 8707) |
The user is redirected to Spedy's consent page. If they have an active session, they can approve without re-entering credentials.
2. Exchange the authorization code
POST /api/v1/oauth/token| Field | Type | Required | Description |
|---|---|---|---|
| grant_type | string | Yes | authorization_code |
| code | string | Yes | The authorization code from the callback |
| redirect_uri | string | Yes | Must match the original request |
| client_id | string | Yes | Your client ID |
| client_secret | string | Conditional | Required for confidential clients |
| code_verifier | string | Yes | PKCE code verifier |
Example Response
{
"access_token": "eyJ...",
"token_type": "Bearer",
"expires_in": 900,
"refresh_token": "eyJ...",
"scope": "read write"
}3. Refresh the access token
POST /api/v1/oauth/token| Field | Type | Required | Description |
|---|---|---|---|
| grant_type | string | Yes | refresh_token |
| refresh_token | string | Yes | The refresh token |
| client_id | string | Yes | Your client ID |
| organization_id | string | No | Switch to a different organization |
When organization_id is provided, the new access token is scoped to that organization. The user must have an active membership in the target organization, and the client must be authorized for it.
Token Revocation
POST /api/v1/oauth/revoke| Field | Type | Required | Description |
|---|---|---|---|
| token | string | Yes | The access token to revoke |
Per RFC 7009, this endpoint always returns 200 OK, even for invalid tokens.
OAuth tokens are also automatically revoked when a user logs out or resets their password.
Organization Switching
Users who belong to multiple organizations can switch context without re-authenticating:
GET /api/v1/oauth/me/organizations?client_id={client_id}This returns the list of organizations the user belongs to that have authorized the given client. Use the organization_id parameter in the refresh token request to switch.
Scopes
| Scope | Description |
|---|---|
read | Read access to boards, tickets, wiki, and other resources |
write | Create and modify resources |
admin | Administrative operations |
Default scopes are read write if none are requested.
Security
- PKCE required -- only S256 is supported, plain is rejected
- Client secrets -- confidential clients use SHA256-hashed secrets with timing-safe comparison
- State parameter -- minimum 32 characters required for CSRF protection
- Redirect URI validation -- only pre-registered URIs are accepted; HTTPS required (except loopback)
- Token revocation -- tokens are revoked on logout and password reset. Both access tokens and refresh tokens are stored server-side for stateful revocation, ensuring that revoked tokens cannot be reused even if the JWT signature is still valid
- Rate limiting -- all OAuth endpoints are rate-limited per IP