Endpoints
Complete reference for every OAuth2 endpoint.
Canonical base URL:
https://auth.vacationtracker.ioThe original host https://api.app.vacationtracker.io/oauth/* stays a permanent compatibility mapping onto the same routes, used by the legacy flow. Everything below works identically on both hosts; examples use the canonical URL.
GET /.well-known/oauth-authorization-server
RFC 8414 authorization server metadata. OAuth libraries with RFC 8414 support discover every other endpoint and capability from this URL automatically, with no hard-coded URLs.
Success response (200)
{
"issuer": "https://cognito-idp.eu-central-1.amazonaws.com/<user-pool-id>",
"authorization_endpoint": "https://auth.vacationtracker.io/oauth/authorize",
"token_endpoint": "https://auth.vacationtracker.io/oauth/token",
"registration_endpoint": "https://auth.vacationtracker.io/oauth/register",
"userinfo_endpoint": "https://auth.vacationtracker.io/oauth/me",
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"code_challenge_methods_supported": ["S256"],
"token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post", "none"],
"scopes_supported": ["openid", "email", "profile", "mcp:workspace:read", "mcp:users:read", "mcp:leaves:read", "zapier:webhooks"]
}issuer is the Cognito user pool URL so strict MCP clients that cross-check the JWT iss claim against metadata don't reject tokens.
POST /oauth/register
Dynamic Client Registration (RFC 7591). Registers a public client without human intervention. Used by MCP clients like Claude Desktop and Cursor on first launch.
Request body
{
"client_name": "My Desktop App",
"redirect_uris": ["http://127.0.0.1:53126/callback"]
}| Field | Required | Description |
|---|---|---|
client_name | Yes | Human-readable name shown on the consent screen |
redirect_uris | Yes | Array of allowed redirect URIs. Loopback (http://127.0.0.1:{port}) is matched port-agnostically per RFC 8252; all other URIs must match exactly. |
grant_types | No | Defaults to ["authorization_code", "refresh_token"]. Only these two are supported. |
response_types | No | Defaults to ["code"]. |
token_endpoint_auth_method | No | Defaults to none (public client). Other values are rejected. |
Success response (201)
{
"client_id": "6a8cd456-...",
"client_name": "My Desktop App",
"redirect_uris": ["http://127.0.0.1:53126/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "none"
}No client_secret is returned. PKCE replaces it.
Rate limits
The endpoint is rate-limited per source IP to prevent abuse. Clients registering once on install stay well under the limit; repeated re-registration from the same IP will be throttled.
Error response (400)
{ "error": "invalid_redirect_uri", "error_description": "…" }| Error code | Cause |
|---|---|
invalid_client_metadata | Missing client_name or malformed payload |
invalid_redirect_uri | redirect_uris missing, empty, or contains a URL that doesn't conform to the registration rules |
invalid_token_endpoint_auth_method | Value other than none requested |
GET /oauth/authorize
Initiates the authorization code flow. Redirect the user's browser here.
Request parameters (query string)
| Parameter | Required | Description |
|---|---|---|
client_id | Yes | Your registered OAuth client ID |
redirect_uri | Yes | Must match a registered redirect URI |
state | Yes | Opaque CSRF token, returned unchanged on the callback |
scope | No | Space-separated scopes. Defaults to openid |
response_type | No | Must be code. Defaults to code |
code_challenge | Required for public clients, optional for confidential | Base64url-encoded SHA-256 hash of the verifier |
code_challenge_method | Required when code_challenge is present | Must be S256 |
Success response
HTTP/1.1 302 Found
Location: https://app.vacationtracker.io/oauth/authorize?session={sessionId}
Cache-Control: no-storeThe user lands on the Vacation Tracker login and consent page. After consent, they're redirected back to your redirect_uri with a code and the state you sent.
Error response (400)
{
"error": "invalid_client | invalid_request | invalid_scope | unsupported_response_type",
"error_description": "Human-readable description"
}| Error code | Cause |
|---|---|
invalid_client | Unknown client_id |
invalid_request | Missing parameter, unregistered redirect_uri, or code_challenge_method not S256 |
invalid_scope | Requested scope not allowed for this client |
unsupported_response_type | response_type is not code |
POST /oauth/token
Exchanges an authorization code for tokens, or refreshes an existing token.
Client authentication
| Client type | Method |
|---|---|
| Public | client_id in body; no secret; PKCE code_verifier required |
| Confidential (body) | client_id + client_secret as form fields |
| Confidential (Basic) | Authorization: Basic base64(client_id:client_secret) |
Confidential clients can also include code_verifier. If Step 1 sent a code_challenge, the server validates it.
Content types
application/x-www-form-urlencoded(standard)application/json
Authorization code exchange
| Parameter | Required | Description |
|---|---|---|
grant_type | Yes | authorization_code |
code | Yes | The authorization code from the callback |
redirect_uri | Yes | Must match the URI used in the authorization request |
client_id | Yes | Via body or Basic auth |
client_secret | Confidential only | Via body or Basic auth |
code_verifier | Required if code_challenge was sent at authorize | Original PKCE verifier |
Success response (200):
{
"access_token": "eyJ...",
"id_token": "eyJ...",
"refresh_token": "eyJ...",
"token_type": "Bearer",
"expires_in": 3600
}Token refresh
| Parameter | Required | Description |
|---|---|---|
grant_type | Yes | refresh_token |
refresh_token | Yes | The refresh token from a previous token response |
client_id | Yes | Via body or Basic auth |
client_secret | Confidential only | Via body or Basic auth |
Success response (200):
{
"access_token": "eyJ...",
"refresh_token": "eyJ...",
"token_type": "Bearer",
"expires_in": 3600
}The same refresh token is echoed back. No id_token is included in refresh responses. Refresh tokens are not rotated.
Error response (400)
{
"error": "invalid_client | invalid_grant | unsupported_grant_type | server_error",
"error_description": "Human-readable description"
}| Error code | Cause |
|---|---|
invalid_client | Unknown client_id, incorrect client_secret for a confidential client, or PKCE required but missing |
invalid_grant | Authorization code is invalid, expired, or already used. redirect_uri / client_id mismatch. Refresh token invalid/expired. PKCE verifier does not match the challenge. |
unsupported_grant_type | Only authorization_code and refresh_token are supported |
server_error | Unexpected server-side error |
Response headers
All token responses include:
Cache-Control: no-store
Pragma: no-cacheGET /oauth/me
Returns basic profile information for the authenticated user.
Authentication
Requires a valid access token in the Authorization header:
Authorization: Bearer {access_token}Success response (200)
{
"id": "vt-user-123",
"name": "Jane Doe",
"email": "jane.doe@example.com"
}| Field | Type | Description |
|---|---|---|
id | string | Vacation Tracker user ID |
name | string | User's full name |
email | string | User's email address |
Error responses
| Status | Error code | Cause |
|---|---|---|
| 401 | unauthorized | Missing or invalid Authorization header |
| 401 | invalid_token | Token is missing required claims |
Token lifetimes
| Token | Lifetime |
|---|---|
| Access token | 60 minutes |
| ID token | 60 minutes |
| Refresh token | 365 days |
| Authorization code | 10 minutes (single-use) |