bd25cba89c
* refactor: combine exchange and refresh into token endpoint * refactor: controller error handling * refactor: use snake_case * refactor: use snake_case * refactor: use snake case * refactor: token endpoint accepts application/x-www-form-urlencoded * refactor: token endpoint accepts application/x-www-form-urlencoded * refactor: flat token response data * refactor: error structure * refactor: client_id in the body * fix: address Cubic AI review feedback on OAuth2 endpoints - Fix getClient endpoint to use proper REST API error format instead of OAuth token error format (confidence 9/10) - Add missing space after comma in error format string in token.input.pipe.ts (confidence 9/10) - Support both camelCase and snake_case inputs in authorize endpoint for backward compatibility (confidence 10/10) - Restore legacy /exchange and /refresh endpoints alongside new /token endpoint for backward compatibility (confidence 10/10) - Add OAuth2TokensResponseDto for legacy endpoint wrapped responses - Add OAuth2LegacyExchangeInput and OAuth2LegacyRefreshInput for legacy endpoints Co-Authored-By: unknown <> * fix: address additional Cubic AI feedback on OAuth2 endpoints - Log errors when status code >= 500 in handleClientError (confidence 9/10) - Add Cache-Control: no-store and Pragma: no-cache headers to legacy /exchange and /refresh endpoints (confidence 9/10) Co-Authored-By: unknown <> * docs * Revert "fix: address additional Cubic AI feedback on OAuth2 endpoints" This reverts commit 39cc4aa3ebb9e59a171541d7010398425995ed89. * Revert "fix: address Cubic AI review feedback on OAuth2 endpoints" This reverts commit 97bf593186db04c0859f9ca30950c9e3e524019d. * docs * fix: address Cubic AI review feedback on OAuth2 endpoints - Fix getClient to use handleClientError instead of handleTokenError (confidence 10) - Restore legacy /exchange and /refresh endpoints for backward compatibility (confidence 9) - Fix RFC 6749 error format: use human-readable messages in error_description (confidence 9) - Fix errorDescription in OAuthService to use OAUTH_ERROR_REASONS mapping (confidence 9) Co-Authored-By: unknown <> * fix: address additional Cubic AI feedback on OAuth2 endpoints - Fix security issue: Replace 'CALENDSO_ENCRYPTION_KEY is not set' with generic 'Internal server configuration error' message (confidence 10/10) - Fix backward compatibility: Create OAuth2LegacyTokensDto with camelCase properties for legacy /exchange and /refresh endpoints (confidence 9/10) - Skipped: RFC 6749 error field issue (confidence 8/10, below threshold) Co-Authored-By: unknown <> * e2e * Revert "fix: address additional Cubic AI feedback on OAuth2 endpoints" This reverts commit a080e93f07aaf5a7dcf81fe605012cb7ebcdc192. * Revert "fix: address Cubic AI review feedback on OAuth2 endpoints" This reverts commit 04986a16c981521ca97069152457bf521a9ee45f. * fix: re-apply Cubic AI review feedback on OAuth2 endpoints - Restore OAuth2LegacyExchangeInput and OAuth2LegacyRefreshInput classes - Restore legacy /exchange and /refresh endpoints in OAuth2Controller - Restore OAuth2LegacyTokensDto and OAuth2TokensResponseDto classes - Restore OAUTH_ERROR_DESCRIPTIONS mapping in oauth2-error.service.ts - Restore OAUTH_ERROR_REASONS lookup in OAuthService.ts mapErrorToOAuthError - Fix encryption_key_missing error to not expose internal env var name Addresses Cubic AI feedback with confidence >= 9/10: - Comment 32 (9/10): Legacy endpoints and input classes - Comment 34 (9/10): Error description mapping in OAuthService - Comment 35 (10/10): OAUTH_ERROR_DESCRIPTIONS in error service Skipped (confidence < 9/10): - Comment 33 (8/10): getClient handleTokenError vs handleClientError Co-Authored-By: unknown <> * Revert "fix: re-apply Cubic AI review feedback on OAuth2 endpoints" This reverts commit 416bef9c931d9a7ed78c65a70a3425550d61b151. * delete unused file * fix: e2e tests * address cubic review * fix: address Cubic AI review feedback on OAuth2 exception filter - Fix header case sensitivity: use lowercase 'x-request-id' instead of 'X-Request-Id' since Express lowercases all request headers - Redact request body in error logs to prevent exposing sensitive OAuth2 credentials like client_secret, password, and refresh_token Co-Authored-By: unknown <> * docs: api v2 oauth controller docs * chore: remove authorize endpoint * refactor: remove scope from docs --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
452 lines
13 KiB
Plaintext
452 lines
13 KiB
Plaintext
---
|
|
title: "OAuth"
|
|
description: "Authorize apps with Cal.com accounts using OAuth"
|
|
---
|
|
|
|
As an example, you can view our OAuth flow in action on Zapier. Try to connect your Cal.com account [here](https://zapier.com/apps/calcom/integrations). To enable OAuth in one of your apps, you will need a Client ID, Client Secret, Authorization URL, Access Token Request URL, and Refresh Token Request URL.
|
|
|
|
<Frame>
|
|

|
|
</Frame>
|
|
|
|
#### Get your OAuth "Continue with [Cal.com](http://Cal.com)" Badge
|
|
|
|
- https://app.cal.com/continue-with-calcom-coss-ui.svg
|
|
- https://app.cal.com/continue-with-calcom-dark-rounded.svg
|
|
- https://app.cal.com/continue-with-calcom-dark-squared.svg
|
|
- https://app.cal.com/continue-with-calcom-light-rounded.svg
|
|
- https://app.cal.com/continue-with-calcom-light-squared.svg
|
|
- https://app.cal.com/continue-with-calcom-neutral-rounded.svg
|
|
- https://app.cal.com/continue-with-calcom-light-squared.svg
|
|
|
|
|
|
## 1. OAuth Client Credentials
|
|
|
|
You can create an OAuth client via the following page https://app.cal.com/settings/developer/oauth. The OAuth client will be in a "pending" state
|
|
and not yet ready to use.
|
|
|
|
An admin from Cal.com will then review your OAuth client and you will receive an email if it was accepted or rejected. If it was accepted then your OAuth client
|
|
is ready to be used.
|
|
|
|
## 2. Authorize
|
|
|
|
To initiate the OAuth flow, direct users to the following authorization URL:
|
|
|
|
`https://app.cal.com/auth/oauth2/authorize?client_id=YOUR_CLIENT_ID&redirect_uri=YOUR_REDIRECT_URI&state=YOUR_STATE`
|
|
|
|
**URL Parameters:**
|
|
|
|
| Parameter | Required | Description |
|
|
|-----------|----------|-------------|
|
|
| `client_id` | Yes | Your OAuth client ID |
|
|
| `redirect_uri` | Yes | Where users will be redirected after authorization. Must exactly match the registered redirect URI. |
|
|
| `state` | Recommended | A securely generated random string to mitigate CSRF attacks |
|
|
| `code_challenge` | For public clients | PKCE code challenge (S256 method) |
|
|
|
|
After users click **Allow**, they will be redirected to the `redirect_uri` with `code` (authorization code) and `state` as URL parameters:
|
|
|
|
```
|
|
https://your-app.com/callback?code=AUTHORIZATION_CODE&state=YOUR_STATE
|
|
```
|
|
|
|
#### Error Handling
|
|
|
|
Errors during the authorization step are displayed directly to the user on the Cal.com authorization page. Your application will not receive a JSON error response for these cases:
|
|
|
|
- **Client not found**: No OAuth client exists with the provided `client_id`.
|
|
- **Client not approved**: The OAuth client has not been approved by a Cal.com admin yet.
|
|
- **Mismatched redirect URI**: The `redirect_uri` does not match the one registered for the OAuth client.
|
|
|
|
If an error occurs after the client is validated (e.g., the user denies access or has insufficient permissions), the user is redirected to the `redirect_uri` with an error:
|
|
|
|
```
|
|
https://your-app.com/callback?error=access_denied&error_description=team_not_found_or_no_access&state=YOUR_STATE
|
|
```
|
|
|
|
## 3. Exchange Token
|
|
|
|
Exchange an authorization code for access and refresh tokens. The token endpoint also accepts `application/x-www-form-urlencoded` content type.
|
|
|
|
**Endpoint:** `POST https://api.cal.com/v2/auth/oauth2/token`
|
|
|
|
### 3.1 Confidential Clients
|
|
|
|
Confidential clients authenticate with a `client_secret`. All parameters are required:
|
|
|
|
| Parameter | Description |
|
|
|-----------|-------------|
|
|
| `client_id` | Your OAuth client ID |
|
|
| `client_secret` | Your OAuth client secret |
|
|
| `grant_type` | Must be `authorization_code` |
|
|
| `code` | The authorization code received in the redirect URI |
|
|
| `redirect_uri` | Must match the redirect URI used in the authorization request |
|
|
|
|
<Tabs>
|
|
<Tab title="cURL">
|
|
```bash
|
|
curl -X POST https://api.cal.com/v2/auth/oauth2/token \
|
|
-H "Content-Type: application/json" \
|
|
-d '{
|
|
"client_id": "YOUR_CLIENT_ID",
|
|
"client_secret": "YOUR_CLIENT_SECRET",
|
|
"grant_type": "authorization_code",
|
|
"code": "AUTHORIZATION_CODE",
|
|
"redirect_uri": "https://your-app.com/callback"
|
|
}'
|
|
```
|
|
</Tab>
|
|
<Tab title="fetch">
|
|
```typescript
|
|
const response = await fetch("https://api.cal.com/v2/auth/oauth2/token", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
client_id: "YOUR_CLIENT_ID",
|
|
client_secret: "YOUR_CLIENT_SECRET",
|
|
grant_type: "authorization_code",
|
|
code: "AUTHORIZATION_CODE",
|
|
redirect_uri: "https://your-app.com/callback",
|
|
}),
|
|
});
|
|
|
|
const tokens = await response.json();
|
|
```
|
|
</Tab>
|
|
<Tab title="axios">
|
|
```typescript
|
|
import axios from "axios";
|
|
|
|
const { data } = await axios.post(
|
|
"https://api.cal.com/v2/auth/oauth2/token",
|
|
{
|
|
client_id: "YOUR_CLIENT_ID",
|
|
client_secret: "YOUR_CLIENT_SECRET",
|
|
grant_type: "authorization_code",
|
|
code: "AUTHORIZATION_CODE",
|
|
redirect_uri: "https://your-app.com/callback",
|
|
}
|
|
);
|
|
```
|
|
</Tab>
|
|
</Tabs>
|
|
|
|
### 3.2 Public Clients (PKCE)
|
|
|
|
Public clients (e.g. single-page apps, mobile apps) use PKCE instead of a `client_secret`. You must have sent a `code_challenge` during the authorization step. All parameters are required:
|
|
|
|
| Parameter | Description |
|
|
|-----------|-------------|
|
|
| `client_id` | Your OAuth client ID |
|
|
| `grant_type` | Must be `authorization_code` |
|
|
| `code` | The authorization code received in the redirect URI |
|
|
| `redirect_uri` | Must match the redirect URI used in the authorization request |
|
|
| `code_verifier` | The original PKCE code verifier used to generate the `code_challenge` |
|
|
|
|
<Tabs>
|
|
<Tab title="cURL">
|
|
```bash
|
|
curl -X POST https://api.cal.com/v2/auth/oauth2/token \
|
|
-H "Content-Type: application/json" \
|
|
-d '{
|
|
"client_id": "YOUR_CLIENT_ID",
|
|
"grant_type": "authorization_code",
|
|
"code": "AUTHORIZATION_CODE",
|
|
"redirect_uri": "https://your-app.com/callback",
|
|
"code_verifier": "YOUR_CODE_VERIFIER"
|
|
}'
|
|
```
|
|
</Tab>
|
|
<Tab title="fetch">
|
|
```typescript
|
|
const response = await fetch("https://api.cal.com/v2/auth/oauth2/token", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
client_id: "YOUR_CLIENT_ID",
|
|
grant_type: "authorization_code",
|
|
code: "AUTHORIZATION_CODE",
|
|
redirect_uri: "https://your-app.com/callback",
|
|
code_verifier: "YOUR_CODE_VERIFIER",
|
|
}),
|
|
});
|
|
|
|
const tokens = await response.json();
|
|
```
|
|
</Tab>
|
|
<Tab title="axios">
|
|
```typescript
|
|
import axios from "axios";
|
|
|
|
const { data } = await axios.post(
|
|
"https://api.cal.com/v2/auth/oauth2/token",
|
|
{
|
|
client_id: "YOUR_CLIENT_ID",
|
|
grant_type: "authorization_code",
|
|
code: "AUTHORIZATION_CODE",
|
|
redirect_uri: "https://your-app.com/callback",
|
|
code_verifier: "YOUR_CODE_VERIFIER",
|
|
}
|
|
);
|
|
```
|
|
</Tab>
|
|
</Tabs>
|
|
|
|
#### Success Response (200)
|
|
|
|
```json
|
|
{
|
|
"access_token": "eyJhbGciOiJIUzI1NiIs...",
|
|
"refresh_token": "eyJhbGciOiJIUzI1NiIs...",
|
|
"token_type": "bearer",
|
|
"expires_in": 1800
|
|
}
|
|
```
|
|
|
|
<Note>
|
|
Access tokens expire after 30 minutes (`expires_in: 1800`). Use the refresh token to obtain a new access token.
|
|
</Note>
|
|
|
|
#### Error Responses
|
|
|
|
Error responses include `error` and `error_description` fields.
|
|
|
|
<AccordionGroup>
|
|
<Accordion title="Invalid or expired authorization code (400)">
|
|
```json
|
|
{
|
|
"error": "invalid_grant",
|
|
"error_description": "code_invalid_or_expired"
|
|
}
|
|
```
|
|
The authorization code has already been used, has expired, or is invalid. Request a new authorization code.
|
|
</Accordion>
|
|
<Accordion title="Invalid client credentials (401)">
|
|
```json
|
|
{
|
|
"error": "invalid_client",
|
|
"error_description": "invalid_client_credentials"
|
|
}
|
|
```
|
|
The `client_secret` does not match the `client_id`. Verify your credentials.
|
|
</Accordion>
|
|
<Accordion title="Client not found (401)">
|
|
```json
|
|
{
|
|
"error": "invalid_client",
|
|
"error_description": "client_not_found"
|
|
}
|
|
```
|
|
No OAuth client exists with the provided `client_id`.
|
|
</Accordion>
|
|
<Accordion title="Missing client_id (400)">
|
|
```json
|
|
{
|
|
"error": "invalid_request",
|
|
"error_description": "client_id is required"
|
|
}
|
|
```
|
|
The `client_id` field is missing from the request body.
|
|
</Accordion>
|
|
<Accordion title="Invalid grant_type (400)">
|
|
```json
|
|
{
|
|
"error": "invalid_request",
|
|
"error_description": "grant_type must be 'authorization_code' or 'refresh_token'"
|
|
}
|
|
```
|
|
The `grant_type` field must be either `authorization_code` or `refresh_token`.
|
|
</Accordion>
|
|
</AccordionGroup>
|
|
|
|
## 4. Refresh Token
|
|
|
|
Refresh an expired access token using a refresh token.
|
|
|
|
**Endpoint:** `POST https://api.cal.com/v2/auth/oauth2/token`
|
|
|
|
### 4.1 Confidential Clients
|
|
|
|
Confidential clients authenticate with a `client_secret`. All parameters are required:
|
|
|
|
| Parameter | Description |
|
|
|-----------|-------------|
|
|
| `client_id` | Your OAuth client ID |
|
|
| `client_secret` | Your OAuth client secret |
|
|
| `grant_type` | Must be `refresh_token` |
|
|
| `refresh_token` | The refresh token received from a previous token response |
|
|
|
|
<Tabs>
|
|
<Tab title="cURL">
|
|
```bash
|
|
curl -X POST https://api.cal.com/v2/auth/oauth2/token \
|
|
-H "Content-Type: application/json" \
|
|
-d '{
|
|
"client_id": "YOUR_CLIENT_ID",
|
|
"client_secret": "YOUR_CLIENT_SECRET",
|
|
"grant_type": "refresh_token",
|
|
"refresh_token": "YOUR_REFRESH_TOKEN"
|
|
}'
|
|
```
|
|
</Tab>
|
|
<Tab title="fetch">
|
|
```typescript
|
|
const response = await fetch("https://api.cal.com/v2/auth/oauth2/token", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
client_id: "YOUR_CLIENT_ID",
|
|
client_secret: "YOUR_CLIENT_SECRET",
|
|
grant_type: "refresh_token",
|
|
refresh_token: "YOUR_REFRESH_TOKEN",
|
|
}),
|
|
});
|
|
|
|
const tokens = await response.json();
|
|
```
|
|
</Tab>
|
|
<Tab title="axios">
|
|
```typescript
|
|
import axios from "axios";
|
|
|
|
const { data } = await axios.post(
|
|
"https://api.cal.com/v2/auth/oauth2/token",
|
|
{
|
|
client_id: "YOUR_CLIENT_ID",
|
|
client_secret: "YOUR_CLIENT_SECRET",
|
|
grant_type: "refresh_token",
|
|
refresh_token: "YOUR_REFRESH_TOKEN",
|
|
}
|
|
);
|
|
```
|
|
</Tab>
|
|
</Tabs>
|
|
|
|
### 4.2 Public Clients
|
|
|
|
Public clients do not use a `client_secret`. All parameters are required:
|
|
|
|
| Parameter | Description |
|
|
|-----------|-------------|
|
|
| `client_id` | Your OAuth client ID |
|
|
| `grant_type` | Must be `refresh_token` |
|
|
| `refresh_token` | The refresh token received from a previous token response |
|
|
|
|
<Tabs>
|
|
<Tab title="cURL">
|
|
```bash
|
|
curl -X POST https://api.cal.com/v2/auth/oauth2/token \
|
|
-H "Content-Type: application/json" \
|
|
-d '{
|
|
"client_id": "YOUR_CLIENT_ID",
|
|
"grant_type": "refresh_token",
|
|
"refresh_token": "YOUR_REFRESH_TOKEN"
|
|
}'
|
|
```
|
|
</Tab>
|
|
<Tab title="fetch">
|
|
```typescript
|
|
const response = await fetch("https://api.cal.com/v2/auth/oauth2/token", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
client_id: "YOUR_CLIENT_ID",
|
|
grant_type: "refresh_token",
|
|
refresh_token: "YOUR_REFRESH_TOKEN",
|
|
}),
|
|
});
|
|
|
|
const tokens = await response.json();
|
|
```
|
|
</Tab>
|
|
<Tab title="axios">
|
|
```typescript
|
|
import axios from "axios";
|
|
|
|
const { data } = await axios.post(
|
|
"https://api.cal.com/v2/auth/oauth2/token",
|
|
{
|
|
client_id: "YOUR_CLIENT_ID",
|
|
grant_type: "refresh_token",
|
|
refresh_token: "YOUR_REFRESH_TOKEN",
|
|
}
|
|
);
|
|
```
|
|
</Tab>
|
|
</Tabs>
|
|
|
|
#### Success Response (200)
|
|
|
|
```json
|
|
{
|
|
"access_token": "eyJhbGciOiJIUzI1NiIs...",
|
|
"refresh_token": "eyJhbGciOiJIUzI1NiIs...",
|
|
"token_type": "bearer",
|
|
"expires_in": 1800
|
|
}
|
|
```
|
|
|
|
#### Error Responses
|
|
|
|
<AccordionGroup>
|
|
<Accordion title="Invalid refresh token (400)">
|
|
```json
|
|
{
|
|
"error": "invalid_grant",
|
|
"error_description": "invalid_refresh_token"
|
|
}
|
|
```
|
|
The refresh token is invalid, expired, or malformed. The user must re-authorize.
|
|
</Accordion>
|
|
<Accordion title="Invalid client credentials (401)">
|
|
```json
|
|
{
|
|
"error": "invalid_client",
|
|
"error_description": "invalid_client_credentials"
|
|
}
|
|
```
|
|
The `client_secret` does not match the `client_id`.
|
|
</Accordion>
|
|
<Accordion title="Client not found (401)">
|
|
```json
|
|
{
|
|
"error": "invalid_client",
|
|
"error_description": "client_not_found"
|
|
}
|
|
```
|
|
No OAuth client exists with the provided `client_id`.
|
|
</Accordion>
|
|
</AccordionGroup>
|
|
|
|
## 5. Verify Access Token
|
|
|
|
To verify the correct setup and functionality of OAuth credentials, use the following endpoint:
|
|
|
|
**Endpoint:** `GET https://api.cal.com/v2/me`
|
|
|
|
<Tabs>
|
|
<Tab title="cURL">
|
|
```bash
|
|
curl -X GET https://api.cal.com/v2/me \
|
|
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
|
|
```
|
|
</Tab>
|
|
<Tab title="fetch">
|
|
```typescript
|
|
const response = await fetch("https://api.cal.com/v2/me", {
|
|
headers: { Authorization: "Bearer YOUR_ACCESS_TOKEN" },
|
|
});
|
|
|
|
const user = await response.json();
|
|
```
|
|
</Tab>
|
|
<Tab title="axios">
|
|
```typescript
|
|
import axios from "axios";
|
|
|
|
const { data } = await axios.get("https://api.cal.com/v2/me", {
|
|
headers: { Authorization: "Bearer YOUR_ACCESS_TOKEN" },
|
|
});
|
|
```
|
|
</Tab>
|
|
</Tabs>
|