Vacation Tracker Docs
OAuth2 Authorization

Authorization Code Flow

Step-by-step walkthrough of Vacation Tracker's OAuth2 Authorization Code Grant flow with PKCE.

The Authorization Code Grant is the only supported OAuth2 flow. It works for both public clients (desktop, mobile, or browser apps that can't keep a secret) and confidential clients (server-side apps). PKCE is required for public clients, and optional but recommended for confidential ones.

Flow diagram

Step 0 (public clients only): Register via DCR

Public clients like desktop apps, CLI tools, and MCP clients register themselves on first use. One request returns a client_id you can reuse forever:

curl -X POST https://auth.vacationtracker.io/oauth/register \
  -H "Content-Type: application/json" \
  -d '{
    "client_name": "My Desktop App",
    "redirect_uris": ["http://127.0.0.1:53126/callback"]
  }'

Response:

{
  "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"
}

Confidential clients skip this step. Your client_id and client_secret are issued manually by the Vacation Tracker team.

See Endpoints → POST /oauth/register for the full request/response contract and rate limits.

Step 1: Generate a PKCE pair and redirect to authorize

Before sending the user to the authorization endpoint, generate a code verifier (43–128 URL-safe random characters) and derive a code challenge from it using SHA-256 plus base64url.

code_verifier=$(openssl rand 64 | openssl base64 -A | tr -d '=' | tr '+/' '-_')
code_challenge=$(printf '%s' "$code_verifier" \
  | openssl dgst -binary -sha256 \
  | openssl base64 -A | tr -d '=' | tr '+/' '-_')

All three produce an 86-character verifier from 64 bytes (512 bits) of entropy. That's well above RFC 7636's 256-bit recommendation and under the 128-character cap. The challenge is the base64url-encoded SHA-256 of the verifier.

Store the code_verifier server-side (or in platform secure storage) keyed by your state value. You'll need it at the token exchange step.

Redirect the user's browser to:

GET https://auth.vacationtracker.io/oauth/authorize
  ?client_id=your-client-id
  &redirect_uri=https://yourapp.com/callback
  &state=random-csrf-token
  &scope=openid email profile
  &response_type=code
  &code_challenge=<sha256-base64url>
  &code_challenge_method=S256
ParameterRequiredDescription
client_idYesYour registered OAuth client ID (from DCR or manual registration)
redirect_uriYesMust exactly match a registered redirect URI
stateYesOpaque CSRF token. Verify unchanged in the callback.
scopeNoSpace-separated. Defaults to openid.
response_typeNoMust be code. Defaults to code.
code_challengeRequired for public clients, optional for confidentialBase64url-encoded SHA-256 hash of your verifier
code_challenge_methodRequired if code_challenge is presentMust be S256

Step 2: User authenticates

Vacation Tracker redirects the user to its login page. The user authenticates with:

  • Microsoft
  • Google
  • Slack
  • Email + password

Step 3: User grants permission

After login, the user sees the consent screen with your application name and the requested scopes. They approve or deny.

Step 4: Receive authorization code

On approval, Vacation Tracker redirects the browser back to your redirect_uri with a code and the state you sent:

https://yourapp.com/callback?code=a1b2c3d4-e5f6-7890-abcd-ef1234567890&state=random-csrf-token
  • Verify that state matches the value you sent in Step 1.
  • The authorization code is single-use and expires after 10 minutes.

Step 5: Exchange code for tokens

Look up the code_verifier you stored in Step 1, then POST to /oauth/token.

Public client (PKCE, no secret):

curl -X POST https://auth.vacationtracker.io/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code" \
  -d "code=a1b2c3d4-..." \
  -d "redirect_uri=https://yourapp.com/callback" \
  -d "client_id=your-client-id" \
  -d "code_verifier=<your-stored-verifier>"

Confidential client (client_secret; PKCE optional):

curl -X POST https://auth.vacationtracker.io/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -u "your-client-id:your-client-secret" \
  -d "grant_type=authorization_code" \
  -d "code=a1b2c3d4-..." \
  -d "redirect_uri=https://yourapp.com/callback" \
  -d "code_verifier=<your-stored-verifier>"

code_verifier is required if Step 1 included a code_challenge, regardless of client type.

Step 6: Receive tokens

On success, you receive:

{
  "access_token": "eyJ...",
  "id_token": "eyJ...",
  "refresh_token": "eyJ...",
  "token_type": "Bearer",
  "expires_in": 3600
}
  • access_token: used as the Bearer for API requests. 60-minute lifetime.
  • id_token: OpenID Connect identity claims. 60-minute lifetime. For MCP, the Bearer you actually send is the id_token. See the MCP setup guide.
  • refresh_token: used to get new tokens. 365-day lifetime.

Step 7: Use the access token

curl https://auth.vacationtracker.io/oauth/me \
  -H "Authorization: Bearer eyJ..."

Refreshing tokens

When the access token expires, use the refresh token to get a new one. Confidential clients add client_secret (or Basic auth); public clients send only client_id.

curl -X POST https://auth.vacationtracker.io/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=refresh_token" \
  -d "refresh_token=eyJ..." \
  -d "client_id=your-client-id"

The same refresh token is echoed back. Vacation Tracker does not rotate refresh tokens.

Metadata discovery

If your OAuth client library supports RFC 8414, point it at:

https://auth.vacationtracker.io/.well-known/oauth-authorization-server

The response advertises every endpoint and capability (supported grant types, PKCE methods, registration_endpoint, and so on), so the library can configure itself without hard-coded URLs.

Error handling

Both endpoints return errors as JSON:

{
  "error": "invalid_grant",
  "error_description": "Code verifier did not match the challenge"
}

See Endpoints for the complete list of error codes per endpoint.

On this page