Skip to content

Commit 45bef2e

Browse files
dcramerclaudecoolguyzone
authored
docs(api): Add OAuth2 documentation with PKCE support (#15904)
- Adds comprehensive OAuth2 documentation to `docs/api/auth.mdx` covering authorization flow, token exchange, refresh tokens, and PKCE implementation - Consolidates OAuth documentation by streamlining the partnership platform page to reference the central auth docs, eliminating duplication - Includes complete Python/Flask code examples demonstrating PKCE-enabled OAuth flow Refs getsentry/sentry#104418 --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: Alex Krawiec <alex.krawiec@sentry.io>
1 parent bc89f7a commit 45bef2e

2 files changed

Lines changed: 202 additions & 284 deletions

File tree

docs/api/auth.mdx

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,194 @@ Some API endpoints require an authentication token that's associated with your u
2727

2828
The endpoints that require a user authentication token are specific to your user, such as [Retrieve an Organization](/api/organizations/retrieve-an-organization/).
2929

30+
## OAuth2
31+
32+
For third-party applications that need to access Sentry on behalf of users, Sentry supports OAuth2 with the authorization code grant type. This allows users to authorize your application without sharing their credentials.
33+
34+
### Authorization Request
35+
36+
Direct users to the authorization endpoint:
37+
38+
```
39+
https://sentry.io/oauth/authorize/?client_id={CLIENT_ID}&response_type=code&scope={SCOPES}
40+
```
41+
42+
**Parameters:**
43+
| Parameter | Required | Description |
44+
|-----------|----------|-------------|
45+
| `client_id` | Yes | Your registered client ID |
46+
| `response_type` | Yes | Must be `code` |
47+
| `scope` | Yes | Space-separated list of [permissions](/api/permissions/) |
48+
| `redirect_uri` | No | Your callback URI (must match registered URI) |
49+
| `state` | No | Random string to prevent CSRF attacks |
50+
| `code_challenge` | Recommended | PKCE challenge (see below) |
51+
| `code_challenge_method` | Recommended | Must be `S256` |
52+
53+
After the user approves, Sentry redirects to your callback URI with an authorization code:
54+
55+
```
56+
https://your-app.com/callback?code={AUTHORIZATION_CODE}
57+
```
58+
59+
### Token Exchange
60+
61+
Exchange the authorization code for an access token:
62+
63+
```bash
64+
curl -X POST https://sentry.io/oauth/token/ \
65+
-d client_id={CLIENT_ID} \
66+
-d client_secret={CLIENT_SECRET} \
67+
-d grant_type=authorization_code \
68+
-d code={AUTHORIZATION_CODE} \
69+
-d code_verifier={CODE_VERIFIER}
70+
```
71+
72+
The `code_verifier` parameter is required if you used PKCE in the authorization request.
73+
74+
**Response:**
75+
```json
76+
{
77+
"access_token": "{ACCESS_TOKEN}",
78+
"refresh_token": "{REFRESH_TOKEN}",
79+
"expires_in": 2591999,
80+
"expires_at": "2024-11-27T23:20:21.054320Z",
81+
"token_type": "bearer",
82+
"scope": "org:read project:read",
83+
"user": {
84+
"id": "123",
85+
"name": "Jane Doe",
86+
"email": "jane@example.com"
87+
}
88+
}
89+
```
90+
91+
### Refreshing Tokens
92+
93+
Access tokens expire after 30 days. Use the refresh token to obtain new tokens:
94+
95+
```bash
96+
curl -X POST https://sentry.io/oauth/token/ \
97+
-d client_id={CLIENT_ID} \
98+
-d client_secret={CLIENT_SECRET} \
99+
-d grant_type=refresh_token \
100+
-d refresh_token={REFRESH_TOKEN}
101+
```
102+
103+
### Using Access Tokens
104+
105+
Include the access token in API requests using the Authorization header:
106+
107+
```bash
108+
curl -H 'Authorization: Bearer {ACCESS_TOKEN}' \
109+
https://sentry.io/api/0/organizations/
110+
```
111+
112+
### Organization Scoping
113+
114+
A Sentry user can belong to multiple organizations. The access token only provides access to the specific organization the user selected during the OAuth flow. The `/api/0/organizations/` endpoint will only return the connected organization.
115+
116+
### PKCE (Proof Key for Code Exchange)
117+
118+
PKCE protects against authorization code interception attacks and is strongly recommended for all OAuth clients.
119+
120+
**How it works:** For each authorization request, generate a unique random secret called the `code_verifier`. Create a `code_challenge` by hashing this verifier. The challenge is sent with the authorization request, while the original verifier is sent when exchanging the code for a token. Sentry verifies they match, ensuring the same client that started the flow is completing it.
121+
122+
**Generating PKCE values (generate fresh for each authorization request):**
123+
124+
```python
125+
import base64
126+
import hashlib
127+
import secrets
128+
129+
# Generate a random code_verifier (43-128 URL-safe characters)
130+
code_verifier = secrets.token_urlsafe(64)
131+
132+
# Create code_challenge by hashing the verifier with SHA256
133+
code_challenge = base64.urlsafe_b64encode(
134+
hashlib.sha256(code_verifier.encode()).digest()
135+
).rstrip(b'=').decode()
136+
137+
# Store code_verifier securely - you'll need it for the token exchange
138+
```
139+
140+
### Error Handling
141+
142+
| Status Code | Meaning | Action |
143+
|-------------|---------|--------|
144+
| 401 | Token expired or revoked | Refresh the token, or prompt user to reconnect |
145+
| 403 | Insufficient permissions | Request additional scopes or handle gracefully |
146+
147+
### Example Implementation
148+
149+
```python
150+
import base64
151+
import hashlib
152+
import secrets
153+
154+
import requests
155+
from flask import Flask, redirect, request, session
156+
157+
app = Flask(__name__)
158+
app.secret_key = 'your-secret-key'
159+
160+
CLIENT_ID = 'your-client-id'
161+
CLIENT_SECRET = 'your-client-secret'
162+
REDIRECT_URI = 'https://your-app.com/callback'
163+
TOKEN_URL = 'https://sentry.io/oauth/token/'
164+
165+
def generate_pkce_pair():
166+
"""Generate a code verifier and challenge for PKCE."""
167+
code_verifier = secrets.token_urlsafe(64)
168+
code_challenge = base64.urlsafe_b64encode(
169+
hashlib.sha256(code_verifier.encode()).digest()
170+
).rstrip(b'=').decode()
171+
return code_verifier, code_challenge
172+
173+
@app.route('/connect')
174+
def connect():
175+
code_verifier, code_challenge = generate_pkce_pair()
176+
session['code_verifier'] = code_verifier
177+
178+
return redirect(
179+
f"https://sentry.io/oauth/authorize/"
180+
f"?client_id={CLIENT_ID}"
181+
f"&response_type=code"
182+
f"&scope=org:read%20project:read"
183+
f"&redirect_uri={REDIRECT_URI}"
184+
f"&code_challenge={code_challenge}"
185+
f"&code_challenge_method=S256"
186+
)
187+
188+
@app.route('/callback')
189+
def callback():
190+
code = request.args.get('code')
191+
code_verifier = session.pop('code_verifier', None)
192+
193+
response = requests.post(TOKEN_URL, data={
194+
"client_id": CLIENT_ID,
195+
"client_secret": CLIENT_SECRET,
196+
"grant_type": "authorization_code",
197+
"code": code,
198+
"code_verifier": code_verifier
199+
})
200+
tokens = response.json()
201+
202+
session['access_token'] = tokens['access_token']
203+
session['refresh_token'] = tokens['refresh_token']
204+
return "Connected!"
205+
206+
def refresh_token():
207+
response = requests.post(TOKEN_URL, data={
208+
"client_id": CLIENT_ID,
209+
"client_secret": CLIENT_SECRET,
210+
"grant_type": "refresh_token",
211+
"refresh_token": session['refresh_token']
212+
})
213+
tokens = response.json()
214+
session['access_token'] = tokens['access_token']
215+
session['refresh_token'] = tokens['refresh_token']
216+
```
217+
30218
## DSN Authentication
31219

32220
Some API endpoints may allow DSN-based authentication. This is generally very limited and an endpoint will describe if its supported. This works similar to Bearer token authentication, but uses your DSN (Client Key).

0 commit comments

Comments
 (0)