@ssheleg/sheleg-dev 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +172 -0
- package/LICENSE +21 -0
- package/README.md +132 -0
- package/SECURITY.md +63 -0
- package/bin/sheleg-dev.js +99 -0
- package/package.json +45 -0
- package/plugins/sheleg-dev/.claude-plugin/plugin.json +28 -0
- package/plugins/sheleg-dev/skills/ad-tracking/SKILL.md +442 -0
- package/plugins/sheleg-dev/skills/ad-tracking/references/consent-mode.md +257 -0
- package/plugins/sheleg-dev/skills/ad-tracking/references/event-tracking.md +201 -0
- package/plugins/sheleg-dev/skills/ad-tracking/references/gtag-api.md +389 -0
- package/plugins/sheleg-dev/skills/ad-tracking/references/meta-linkedin.md +103 -0
- package/plugins/sheleg-dev/skills/ad-tracking/references/performance-security.md +288 -0
- package/plugins/sheleg-dev/skills/crypto-payments/SKILL.md +384 -0
- package/plugins/sheleg-dev/skills/crypto-payments/references/heleket-provider.md +1357 -0
- package/plugins/sheleg-dev/skills/frontend-performance/SKILL.md +147 -0
- package/plugins/sheleg-dev/skills/frontend-performance/references/accessibility-perf.md +172 -0
- package/plugins/sheleg-dev/skills/frontend-performance/references/css-performance.md +180 -0
- package/plugins/sheleg-dev/skills/frontend-performance/references/nextjs.md +271 -0
- package/plugins/sheleg-dev/skills/google-auth/SKILL.md +275 -0
- package/plugins/sheleg-dev/skills/google-auth/references/adc-and-service-accounts.md +464 -0
- package/plugins/sheleg-dev/skills/google-auth/references/oauth2-web-server.md +615 -0
- package/plugins/sheleg-dev/skills/google-auth/references/sign-in-with-google.md +593 -0
- package/plugins/sheleg-dev/skills/google-auth/references/workload-identity.md +376 -0
- package/plugins/sheleg-dev/skills/google-signin/SKILL.md +121 -0
- package/plugins/sheleg-dev/skills/google-signin/references/full-guide.md +421 -0
- package/plugins/sheleg-dev/skills/stripe-billing/SKILL.md +458 -0
- package/plugins/sheleg-dev/skills/stripe-billing/references/price-integrity.md +201 -0
- package/plugins/sheleg-dev/skills/stripe-billing/references/stripe-agent-toolchain.md +173 -0
- package/plugins/sheleg-dev/skills/stripe-billing/references/subscription-lifecycle.md +294 -0
- package/plugins/sheleg-dev/skills/stripe-billing/references/testing-and-local-dev.md +214 -0
- package/plugins/sheleg-dev/skills/stripe-billing/references/webhook-events.md +211 -0
|
@@ -0,0 +1,615 @@
|
|
|
1
|
+
# OAuth 2.0 Web Server Flow (Node.js & Python)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
## Contents
|
|
5
|
+
|
|
6
|
+
- [Table of Contents](#table-of-contents)
|
|
7
|
+
- [Prerequisites](#prerequisites)
|
|
8
|
+
- [Step 1: Set Authorization Parameters](#step-1-set-authorization-parameters)
|
|
9
|
+
- [Step 2: Redirect to Google](#step-2-redirect-to-google)
|
|
10
|
+
- [Step 3: Handle Callback](#step-3-handle-callback)
|
|
11
|
+
- [Step 4: Use Access Token](#step-4-use-access-token)
|
|
12
|
+
- [Refreshing Tokens](#refreshing-tokens)
|
|
13
|
+
- [Revoking Tokens](#revoking-tokens)
|
|
14
|
+
- [Incremental Authorization](#incremental-authorization)
|
|
15
|
+
- [Error Reference](#error-reference)
|
|
16
|
+
- [Complete Express Example](#complete-express-example)
|
|
17
|
+
- [Complete Flask Example](#complete-flask-example)
|
|
18
|
+
- [Complete FastAPI Example](#complete-fastapi-example)
|
|
19
|
+
- [Scopes Reference](#scopes-reference)
|
|
20
|
+
|
|
21
|
+
## Table of Contents
|
|
22
|
+
- [Prerequisites](#prerequisites)
|
|
23
|
+
- [Step 1: Set Authorization Parameters](#step-1-set-authorization-parameters)
|
|
24
|
+
- [Step 2: Redirect to Google](#step-2-redirect-to-google)
|
|
25
|
+
- [Step 3: Handle Callback](#step-3-handle-callback)
|
|
26
|
+
- [Step 4: Use Access Token](#step-4-use-access-token)
|
|
27
|
+
- [Refreshing Tokens](#refreshing-tokens)
|
|
28
|
+
- [Revoking Tokens](#revoking-tokens)
|
|
29
|
+
- [Incremental Authorization](#incremental-authorization)
|
|
30
|
+
- [Error Reference](#error-reference)
|
|
31
|
+
- [Complete Express Example](#complete-express-example)
|
|
32
|
+
- [Complete Flask Example](#complete-flask-example)
|
|
33
|
+
- [Complete FastAPI Example](#complete-fastapi-example)
|
|
34
|
+
|
|
35
|
+
## Prerequisites
|
|
36
|
+
|
|
37
|
+
1. Enable APIs in [Google Cloud Console](https://console.developers.google.com/apis/library)
|
|
38
|
+
2. Create OAuth 2.0 Client ID at [Clients page](https://console.developers.google.com/auth/clients):
|
|
39
|
+
- Application type: **Web application**
|
|
40
|
+
- Set authorized redirect URIs
|
|
41
|
+
3. Download `client_secret.json`
|
|
42
|
+
4. Install dependencies:
|
|
43
|
+
|
|
44
|
+
**Node.js**
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npm install googleapis crypto express express-session
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**Python**
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install google-auth google-auth-oauthlib google-api-python-client flask
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Step 1: Set Authorization Parameters
|
|
57
|
+
|
|
58
|
+
### Node.js
|
|
59
|
+
|
|
60
|
+
```js
|
|
61
|
+
const {google} = require('googleapis');
|
|
62
|
+
const crypto = require('crypto');
|
|
63
|
+
|
|
64
|
+
const oauth2Client = new google.auth.OAuth2(
|
|
65
|
+
YOUR_CLIENT_ID,
|
|
66
|
+
YOUR_CLIENT_SECRET,
|
|
67
|
+
YOUR_REDIRECT_URL
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const state = crypto.randomBytes(32).toString('hex');
|
|
71
|
+
req.session.state = state;
|
|
72
|
+
|
|
73
|
+
const authorizationUrl = oauth2Client.generateAuthUrl({
|
|
74
|
+
access_type: 'offline', // 'online' (default) or 'offline' (gets refresh_token)
|
|
75
|
+
scope: [
|
|
76
|
+
'https://www.googleapis.com/auth/drive.metadata.readonly',
|
|
77
|
+
'https://www.googleapis.com/auth/calendar.readonly'
|
|
78
|
+
],
|
|
79
|
+
include_granted_scopes: true, // incremental authorization
|
|
80
|
+
state: state, // CSRF protection
|
|
81
|
+
// prompt: 'consent', // force re-consent (returns new refresh_token)
|
|
82
|
+
// login_hint: 'user@example.com'
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Python
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from google_auth_oauthlib.flow import Flow
|
|
90
|
+
import secrets
|
|
91
|
+
|
|
92
|
+
flow = Flow.from_client_secrets_file(
|
|
93
|
+
'client_secret.json',
|
|
94
|
+
scopes=[
|
|
95
|
+
'https://www.googleapis.com/auth/drive.metadata.readonly',
|
|
96
|
+
'https://www.googleapis.com/auth/calendar.readonly'
|
|
97
|
+
],
|
|
98
|
+
redirect_uri=YOUR_REDIRECT_URL
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
state = secrets.token_hex(32)
|
|
102
|
+
session['state'] = state
|
|
103
|
+
|
|
104
|
+
authorization_url, state = flow.authorization_url(
|
|
105
|
+
access_type='offline', # gets refresh_token
|
|
106
|
+
include_granted_scopes='true', # incremental authorization
|
|
107
|
+
state=state, # CSRF protection
|
|
108
|
+
# prompt='consent', # force re-consent
|
|
109
|
+
# login_hint='user@example.com'
|
|
110
|
+
)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Authorization Parameters Reference
|
|
114
|
+
|
|
115
|
+
| Parameter | Required | Description |
|
|
116
|
+
|-----------|----------|-------------|
|
|
117
|
+
| `client_id` | Yes | From Cloud Console |
|
|
118
|
+
| `redirect_uri` | Yes | Must match authorized redirect URI exactly |
|
|
119
|
+
| `response_type` | Yes | Set to `code` (handled by library) |
|
|
120
|
+
| `scope` | Yes | Space-delimited or array of scopes |
|
|
121
|
+
| `access_type` | Recommended | `offline` to get refresh_token |
|
|
122
|
+
| `state` | Recommended | Random string for CSRF protection |
|
|
123
|
+
| `include_granted_scopes` | Optional | `true` for incremental authorization |
|
|
124
|
+
| `login_hint` | Optional | Email or `sub` identifier |
|
|
125
|
+
| `prompt` | Optional | `none`, `consent`, or `select_account` |
|
|
126
|
+
|
|
127
|
+
## Step 2: Redirect to Google
|
|
128
|
+
|
|
129
|
+
### Node.js
|
|
130
|
+
|
|
131
|
+
```js
|
|
132
|
+
res.redirect(authorizationUrl);
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Python (Flask)
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
return redirect(authorization_url)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Step 3: Handle Callback
|
|
142
|
+
|
|
143
|
+
### Node.js
|
|
144
|
+
|
|
145
|
+
```js
|
|
146
|
+
const url = require('url');
|
|
147
|
+
|
|
148
|
+
app.get('/oauth2callback', async (req, res) => {
|
|
149
|
+
const q = url.parse(req.url, true).query;
|
|
150
|
+
|
|
151
|
+
if (q.error) {
|
|
152
|
+
console.log('Error: ' + q.error);
|
|
153
|
+
return res.status(400).send('Authorization failed');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (q.state !== req.session.state) {
|
|
157
|
+
return res.status(403).send('State mismatch. Possible CSRF attack');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const {tokens} = await oauth2Client.getToken(q.code);
|
|
161
|
+
oauth2Client.setCredentials(tokens);
|
|
162
|
+
|
|
163
|
+
// tokens.access_token — short-lived access token
|
|
164
|
+
// tokens.refresh_token — long-lived (only on first auth!)
|
|
165
|
+
// tokens.expiry_date — expiration timestamp
|
|
166
|
+
// tokens.scope — granted scopes (check for partial consent)
|
|
167
|
+
|
|
168
|
+
const grantedScopes = tokens.scope.split(' ');
|
|
169
|
+
});
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Python (Flask)
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
@app.route('/oauth2callback')
|
|
176
|
+
def oauth2callback():
|
|
177
|
+
if request.args.get('error'):
|
|
178
|
+
return 'Authorization failed', 400
|
|
179
|
+
|
|
180
|
+
if request.args.get('state') != session.get('state'):
|
|
181
|
+
abort(403, 'State mismatch. Possible CSRF attack')
|
|
182
|
+
|
|
183
|
+
flow = Flow.from_client_secrets_file(
|
|
184
|
+
'client_secret.json',
|
|
185
|
+
scopes=SCOPES,
|
|
186
|
+
redirect_uri=YOUR_REDIRECT_URL,
|
|
187
|
+
state=session['state']
|
|
188
|
+
)
|
|
189
|
+
flow.fetch_token(authorization_response=request.url)
|
|
190
|
+
|
|
191
|
+
credentials = flow.credentials
|
|
192
|
+
# credentials.token — access token
|
|
193
|
+
# credentials.refresh_token — only on first auth!
|
|
194
|
+
# credentials.expiry — expiration datetime
|
|
195
|
+
# credentials.scopes — granted scopes
|
|
196
|
+
|
|
197
|
+
session['credentials'] = {
|
|
198
|
+
'token': credentials.token,
|
|
199
|
+
'refresh_token': credentials.refresh_token,
|
|
200
|
+
'token_uri': credentials.token_uri,
|
|
201
|
+
'client_id': credentials.client_id,
|
|
202
|
+
'client_secret': credentials.client_secret,
|
|
203
|
+
'scopes': list(credentials.scopes)
|
|
204
|
+
}
|
|
205
|
+
return redirect('/profile')
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Python (FastAPI)
|
|
209
|
+
|
|
210
|
+
```python
|
|
211
|
+
from fastapi import APIRouter, Request
|
|
212
|
+
from fastapi.responses import RedirectResponse
|
|
213
|
+
from google_auth_oauthlib.flow import Flow
|
|
214
|
+
|
|
215
|
+
@router.get('/oauth2callback')
|
|
216
|
+
async def oauth2callback(request: Request):
|
|
217
|
+
if request.query_params.get('error'):
|
|
218
|
+
raise HTTPException(400, 'Authorization failed')
|
|
219
|
+
|
|
220
|
+
state = request.session.get('state')
|
|
221
|
+
if request.query_params.get('state') != state:
|
|
222
|
+
raise HTTPException(403, 'State mismatch')
|
|
223
|
+
|
|
224
|
+
flow = Flow.from_client_secrets_file(
|
|
225
|
+
'client_secret.json',
|
|
226
|
+
scopes=SCOPES,
|
|
227
|
+
redirect_uri=YOUR_REDIRECT_URL,
|
|
228
|
+
state=state
|
|
229
|
+
)
|
|
230
|
+
flow.fetch_token(code=request.query_params.get('code'))
|
|
231
|
+
credentials = flow.credentials
|
|
232
|
+
|
|
233
|
+
# Store credentials in session or database
|
|
234
|
+
request.session['credentials'] = credentials_to_dict(credentials)
|
|
235
|
+
return RedirectResponse('/profile')
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## Step 4: Use Access Token
|
|
239
|
+
|
|
240
|
+
### Node.js
|
|
241
|
+
|
|
242
|
+
```js
|
|
243
|
+
// Preferred: Authorization header
|
|
244
|
+
const res = await oauth2Client.fetch('https://www.googleapis.com/drive/v3/files');
|
|
245
|
+
|
|
246
|
+
// Or with googleapis client:
|
|
247
|
+
const drive = google.drive({version: 'v3', auth: oauth2Client});
|
|
248
|
+
const fileList = await drive.files.list();
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Python
|
|
252
|
+
|
|
253
|
+
```python
|
|
254
|
+
from google.oauth2.credentials import Credentials
|
|
255
|
+
from googleapiclient.discovery import build
|
|
256
|
+
|
|
257
|
+
credentials = Credentials(**session['credentials'])
|
|
258
|
+
drive = build('drive', 'v3', credentials=credentials)
|
|
259
|
+
file_list = drive.files().list().execute()
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
## Refreshing Tokens
|
|
263
|
+
|
|
264
|
+
### Node.js
|
|
265
|
+
|
|
266
|
+
```js
|
|
267
|
+
// Auto-refresh: the library handles this if refresh_token is set
|
|
268
|
+
oauth2Client.setCredentials({
|
|
269
|
+
refresh_token: STORED_REFRESH_TOKEN
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// Manual refresh:
|
|
273
|
+
const {credentials} = await oauth2Client.refreshAccessToken();
|
|
274
|
+
|
|
275
|
+
// Listen for new tokens:
|
|
276
|
+
oauth2Client.on('tokens', (tokens) => {
|
|
277
|
+
if (tokens.refresh_token) {
|
|
278
|
+
// Store in database — only sent once!
|
|
279
|
+
}
|
|
280
|
+
console.log('New access_token:', tokens.access_token);
|
|
281
|
+
});
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### Python
|
|
285
|
+
|
|
286
|
+
```python
|
|
287
|
+
from google.oauth2.credentials import Credentials
|
|
288
|
+
from google.auth.transport.requests import Request
|
|
289
|
+
|
|
290
|
+
credentials = Credentials(
|
|
291
|
+
token=stored_access_token,
|
|
292
|
+
refresh_token=stored_refresh_token,
|
|
293
|
+
token_uri='https://oauth2.googleapis.com/token',
|
|
294
|
+
client_id=CLIENT_ID,
|
|
295
|
+
client_secret=CLIENT_SECRET
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
# Manual refresh:
|
|
299
|
+
if credentials.expired and credentials.refresh_token:
|
|
300
|
+
credentials.refresh(Request())
|
|
301
|
+
# credentials.token is the new access token
|
|
302
|
+
# credentials.expiry is the new expiration datetime
|
|
303
|
+
# Persist new token to storage
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
**Note**: `google-api-python-client` (`build(...)`) auto-refreshes credentials before each API call if a refresh_token is available.
|
|
307
|
+
|
|
308
|
+
## Revoking Tokens
|
|
309
|
+
|
|
310
|
+
### Node.js
|
|
311
|
+
|
|
312
|
+
```js
|
|
313
|
+
// Revoke access token:
|
|
314
|
+
await oauth2Client.revokeToken(access_token);
|
|
315
|
+
|
|
316
|
+
// Revoke refresh token:
|
|
317
|
+
await oauth2Client.revokeToken(refresh_token);
|
|
318
|
+
|
|
319
|
+
// Or revoke credentials on the client:
|
|
320
|
+
await oauth2Client.revokeCredentials();
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### Python
|
|
324
|
+
|
|
325
|
+
```python
|
|
326
|
+
import google.oauth2.credentials
|
|
327
|
+
from google.auth.transport.requests import Request
|
|
328
|
+
|
|
329
|
+
# Revoke via requests library:
|
|
330
|
+
import requests as http_requests
|
|
331
|
+
|
|
332
|
+
http_requests.post(
|
|
333
|
+
'https://oauth2.googleapis.com/revoke',
|
|
334
|
+
params={'token': credentials.token},
|
|
335
|
+
headers={'content-type': 'application/x-www-form-urlencoded'}
|
|
336
|
+
)
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
After revocation, redirect user to re-consent if needed.
|
|
340
|
+
|
|
341
|
+
## Incremental Authorization
|
|
342
|
+
|
|
343
|
+
Request additional scopes later without re-requesting all scopes:
|
|
344
|
+
|
|
345
|
+
### Node.js
|
|
346
|
+
|
|
347
|
+
```js
|
|
348
|
+
const newAuthUrl = oauth2Client.generateAuthUrl({
|
|
349
|
+
access_type: 'offline',
|
|
350
|
+
scope: ['https://www.googleapis.com/auth/gmail.readonly'],
|
|
351
|
+
include_granted_scopes: true,
|
|
352
|
+
state: newState
|
|
353
|
+
});
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
### Python
|
|
357
|
+
|
|
358
|
+
```python
|
|
359
|
+
flow = Flow.from_client_secrets_file(
|
|
360
|
+
'client_secret.json',
|
|
361
|
+
scopes=['https://www.googleapis.com/auth/gmail.readonly'],
|
|
362
|
+
redirect_uri=YOUR_REDIRECT_URL
|
|
363
|
+
)
|
|
364
|
+
authorization_url, state = flow.authorization_url(
|
|
365
|
+
access_type='offline',
|
|
366
|
+
include_granted_scopes='true',
|
|
367
|
+
state=new_state
|
|
368
|
+
)
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
## Error Reference
|
|
372
|
+
|
|
373
|
+
| Error | Cause |
|
|
374
|
+
|-------|-------|
|
|
375
|
+
| `access_denied` | User denied consent |
|
|
376
|
+
| `admin_policy_enforced` | Workspace admin blocked scopes |
|
|
377
|
+
| `redirect_uri_mismatch` | redirect_uri doesn't match Cloud Console config |
|
|
378
|
+
| `invalid_client` | Wrong client_secret |
|
|
379
|
+
| `deleted_client` | OAuth client was deleted |
|
|
380
|
+
| `invalid_grant` | Token expired or invalidated; re-authenticate user |
|
|
381
|
+
| `invalid_request` | Missing/malformed parameters |
|
|
382
|
+
| `org_internal` | Project restricts to specific GCP org accounts |
|
|
383
|
+
|
|
384
|
+
## Complete Express Example
|
|
385
|
+
|
|
386
|
+
```js
|
|
387
|
+
const express = require('express');
|
|
388
|
+
const session = require('express-session');
|
|
389
|
+
const crypto = require('crypto');
|
|
390
|
+
const {google} = require('googleapis');
|
|
391
|
+
|
|
392
|
+
const app = express();
|
|
393
|
+
app.use(session({secret: 'your-secret', resave: false, saveUninitialized: false}));
|
|
394
|
+
|
|
395
|
+
const oauth2Client = new google.auth.OAuth2(
|
|
396
|
+
process.env.GOOGLE_CLIENT_ID,
|
|
397
|
+
process.env.GOOGLE_CLIENT_SECRET,
|
|
398
|
+
'http://localhost:3000/oauth2callback'
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
const SCOPES = ['https://www.googleapis.com/auth/userinfo.profile'];
|
|
402
|
+
|
|
403
|
+
app.get('/auth', (req, res) => {
|
|
404
|
+
const state = crypto.randomBytes(32).toString('hex');
|
|
405
|
+
req.session.state = state;
|
|
406
|
+
|
|
407
|
+
const url = oauth2Client.generateAuthUrl({
|
|
408
|
+
access_type: 'offline',
|
|
409
|
+
scope: SCOPES,
|
|
410
|
+
state,
|
|
411
|
+
include_granted_scopes: true
|
|
412
|
+
});
|
|
413
|
+
res.redirect(url);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
app.get('/oauth2callback', async (req, res) => {
|
|
417
|
+
if (req.query.error) return res.redirect('/error');
|
|
418
|
+
if (req.query.state !== req.session.state) return res.status(403).send('CSRF');
|
|
419
|
+
|
|
420
|
+
const {tokens} = await oauth2Client.getToken(req.query.code);
|
|
421
|
+
req.session.tokens = tokens;
|
|
422
|
+
res.redirect('/profile');
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
app.get('/profile', async (req, res) => {
|
|
426
|
+
if (!req.session.tokens) return res.redirect('/auth');
|
|
427
|
+
oauth2Client.setCredentials(req.session.tokens);
|
|
428
|
+
|
|
429
|
+
const oauth2 = google.oauth2({version: 'v2', auth: oauth2Client});
|
|
430
|
+
const {data} = await oauth2.userinfo.get();
|
|
431
|
+
res.json(data);
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
app.listen(3000);
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
## Complete Flask Example
|
|
438
|
+
|
|
439
|
+
```python
|
|
440
|
+
import os
|
|
441
|
+
import secrets
|
|
442
|
+
from flask import Flask, redirect, request, session, jsonify, abort
|
|
443
|
+
from google_auth_oauthlib.flow import Flow
|
|
444
|
+
from google.oauth2.credentials import Credentials
|
|
445
|
+
from googleapiclient.discovery import build
|
|
446
|
+
|
|
447
|
+
app = Flask(__name__)
|
|
448
|
+
app.secret_key = os.environ.get('FLASK_SECRET_KEY', 'dev-secret')
|
|
449
|
+
|
|
450
|
+
CLIENT_ID = os.environ['GOOGLE_CLIENT_ID']
|
|
451
|
+
CLIENT_SECRET = os.environ['GOOGLE_CLIENT_SECRET']
|
|
452
|
+
REDIRECT_URI = 'http://localhost:5000/oauth2callback'
|
|
453
|
+
SCOPES = ['https://www.googleapis.com/auth/userinfo.profile']
|
|
454
|
+
|
|
455
|
+
@app.route('/auth')
|
|
456
|
+
def auth():
|
|
457
|
+
flow = Flow.from_client_config(
|
|
458
|
+
{
|
|
459
|
+
'web': {
|
|
460
|
+
'client_id': CLIENT_ID,
|
|
461
|
+
'client_secret': CLIENT_SECRET,
|
|
462
|
+
'auth_uri': 'https://accounts.google.com/o/oauth2/auth',
|
|
463
|
+
'token_uri': 'https://oauth2.googleapis.com/token',
|
|
464
|
+
}
|
|
465
|
+
},
|
|
466
|
+
scopes=SCOPES,
|
|
467
|
+
redirect_uri=REDIRECT_URI
|
|
468
|
+
)
|
|
469
|
+
state = secrets.token_hex(32)
|
|
470
|
+
session['state'] = state
|
|
471
|
+
authorization_url, _ = flow.authorization_url(
|
|
472
|
+
access_type='offline',
|
|
473
|
+
include_granted_scopes='true',
|
|
474
|
+
state=state
|
|
475
|
+
)
|
|
476
|
+
return redirect(authorization_url)
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
@app.route('/oauth2callback')
|
|
480
|
+
def oauth2callback():
|
|
481
|
+
if request.args.get('error'):
|
|
482
|
+
return 'Authorization failed', 400
|
|
483
|
+
if request.args.get('state') != session.get('state'):
|
|
484
|
+
abort(403, 'State mismatch')
|
|
485
|
+
|
|
486
|
+
flow = Flow.from_client_config(
|
|
487
|
+
{
|
|
488
|
+
'web': {
|
|
489
|
+
'client_id': CLIENT_ID,
|
|
490
|
+
'client_secret': CLIENT_SECRET,
|
|
491
|
+
'auth_uri': 'https://accounts.google.com/o/oauth2/auth',
|
|
492
|
+
'token_uri': 'https://oauth2.googleapis.com/token',
|
|
493
|
+
}
|
|
494
|
+
},
|
|
495
|
+
scopes=SCOPES,
|
|
496
|
+
redirect_uri=REDIRECT_URI,
|
|
497
|
+
state=session['state']
|
|
498
|
+
)
|
|
499
|
+
flow.fetch_token(authorization_response=request.url)
|
|
500
|
+
creds = flow.credentials
|
|
501
|
+
session['credentials'] = {
|
|
502
|
+
'token': creds.token,
|
|
503
|
+
'refresh_token': creds.refresh_token,
|
|
504
|
+
'token_uri': creds.token_uri,
|
|
505
|
+
'client_id': creds.client_id,
|
|
506
|
+
'client_secret': creds.client_secret,
|
|
507
|
+
'scopes': list(creds.scopes or [])
|
|
508
|
+
}
|
|
509
|
+
return redirect('/profile')
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
@app.route('/profile')
|
|
513
|
+
def profile():
|
|
514
|
+
if 'credentials' not in session:
|
|
515
|
+
return redirect('/auth')
|
|
516
|
+
creds = Credentials(**session['credentials'])
|
|
517
|
+
service = build('oauth2', 'v2', credentials=creds)
|
|
518
|
+
user_info = service.userinfo().get().execute()
|
|
519
|
+
return jsonify(user_info)
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
if __name__ == '__main__':
|
|
523
|
+
os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1' # dev only
|
|
524
|
+
app.run(port=5000, debug=True)
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
## Complete FastAPI Example
|
|
528
|
+
|
|
529
|
+
```python
|
|
530
|
+
import os
|
|
531
|
+
import secrets
|
|
532
|
+
from fastapi import FastAPI, Request, HTTPException
|
|
533
|
+
from fastapi.responses import RedirectResponse
|
|
534
|
+
from starlette.middleware.sessions import SessionMiddleware
|
|
535
|
+
from google_auth_oauthlib.flow import Flow
|
|
536
|
+
from google.oauth2.credentials import Credentials
|
|
537
|
+
from googleapiclient.discovery import build
|
|
538
|
+
|
|
539
|
+
os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1' # dev only
|
|
540
|
+
|
|
541
|
+
app = FastAPI()
|
|
542
|
+
app.add_middleware(SessionMiddleware, secret_key='dev-secret')
|
|
543
|
+
|
|
544
|
+
CLIENT_ID = os.environ['GOOGLE_CLIENT_ID']
|
|
545
|
+
CLIENT_SECRET = os.environ['GOOGLE_CLIENT_SECRET']
|
|
546
|
+
REDIRECT_URI = 'http://localhost:8000/oauth2callback'
|
|
547
|
+
SCOPES = ['https://www.googleapis.com/auth/userinfo.profile']
|
|
548
|
+
|
|
549
|
+
CLIENT_CONFIG = {
|
|
550
|
+
'web': {
|
|
551
|
+
'client_id': CLIENT_ID,
|
|
552
|
+
'client_secret': CLIENT_SECRET,
|
|
553
|
+
'auth_uri': 'https://accounts.google.com/o/oauth2/auth',
|
|
554
|
+
'token_uri': 'https://oauth2.googleapis.com/token',
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
@app.get('/auth')
|
|
559
|
+
async def auth(request: Request):
|
|
560
|
+
flow = Flow.from_client_config(CLIENT_CONFIG, scopes=SCOPES, redirect_uri=REDIRECT_URI)
|
|
561
|
+
state = secrets.token_hex(32)
|
|
562
|
+
request.session['state'] = state
|
|
563
|
+
authorization_url, _ = flow.authorization_url(
|
|
564
|
+
access_type='offline',
|
|
565
|
+
include_granted_scopes='true',
|
|
566
|
+
state=state
|
|
567
|
+
)
|
|
568
|
+
return RedirectResponse(authorization_url)
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
@app.get('/oauth2callback')
|
|
572
|
+
async def oauth2callback(request: Request):
|
|
573
|
+
if request.query_params.get('error'):
|
|
574
|
+
raise HTTPException(400, 'Authorization failed')
|
|
575
|
+
if request.query_params.get('state') != request.session.get('state'):
|
|
576
|
+
raise HTTPException(403, 'State mismatch')
|
|
577
|
+
|
|
578
|
+
flow = Flow.from_client_config(
|
|
579
|
+
CLIENT_CONFIG, scopes=SCOPES, redirect_uri=REDIRECT_URI,
|
|
580
|
+
state=request.session['state']
|
|
581
|
+
)
|
|
582
|
+
flow.fetch_token(code=request.query_params.get('code'))
|
|
583
|
+
creds = flow.credentials
|
|
584
|
+
request.session['credentials'] = {
|
|
585
|
+
'token': creds.token,
|
|
586
|
+
'refresh_token': creds.refresh_token,
|
|
587
|
+
'token_uri': creds.token_uri,
|
|
588
|
+
'client_id': creds.client_id,
|
|
589
|
+
'client_secret': creds.client_secret,
|
|
590
|
+
'scopes': list(creds.scopes or [])
|
|
591
|
+
}
|
|
592
|
+
return RedirectResponse('/profile')
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
@app.get('/profile')
|
|
596
|
+
async def profile(request: Request):
|
|
597
|
+
if 'credentials' not in request.session:
|
|
598
|
+
return RedirectResponse('/auth')
|
|
599
|
+
creds = Credentials(**request.session['credentials'])
|
|
600
|
+
service = build('oauth2', 'v2', credentials=creds)
|
|
601
|
+
user_info = service.userinfo().get().execute()
|
|
602
|
+
return user_info
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
## Scopes Reference
|
|
606
|
+
|
|
607
|
+
Full list: https://developers.google.com/identity/protocols/oauth2/scopes
|
|
608
|
+
|
|
609
|
+
Common scopes:
|
|
610
|
+
- `openid` — OpenID Connect
|
|
611
|
+
- `https://www.googleapis.com/auth/userinfo.email` — User email
|
|
612
|
+
- `https://www.googleapis.com/auth/userinfo.profile` — User profile info
|
|
613
|
+
- `https://www.googleapis.com/auth/drive.readonly` — Read-only Drive
|
|
614
|
+
- `https://www.googleapis.com/auth/calendar.readonly` — Read-only Calendar
|
|
615
|
+
- `https://www.googleapis.com/auth/cloud-platform` — Full GCP access
|