@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.
Files changed (32) hide show
  1. package/CHANGELOG.md +172 -0
  2. package/LICENSE +21 -0
  3. package/README.md +132 -0
  4. package/SECURITY.md +63 -0
  5. package/bin/sheleg-dev.js +99 -0
  6. package/package.json +45 -0
  7. package/plugins/sheleg-dev/.claude-plugin/plugin.json +28 -0
  8. package/plugins/sheleg-dev/skills/ad-tracking/SKILL.md +442 -0
  9. package/plugins/sheleg-dev/skills/ad-tracking/references/consent-mode.md +257 -0
  10. package/plugins/sheleg-dev/skills/ad-tracking/references/event-tracking.md +201 -0
  11. package/plugins/sheleg-dev/skills/ad-tracking/references/gtag-api.md +389 -0
  12. package/plugins/sheleg-dev/skills/ad-tracking/references/meta-linkedin.md +103 -0
  13. package/plugins/sheleg-dev/skills/ad-tracking/references/performance-security.md +288 -0
  14. package/plugins/sheleg-dev/skills/crypto-payments/SKILL.md +384 -0
  15. package/plugins/sheleg-dev/skills/crypto-payments/references/heleket-provider.md +1357 -0
  16. package/plugins/sheleg-dev/skills/frontend-performance/SKILL.md +147 -0
  17. package/plugins/sheleg-dev/skills/frontend-performance/references/accessibility-perf.md +172 -0
  18. package/plugins/sheleg-dev/skills/frontend-performance/references/css-performance.md +180 -0
  19. package/plugins/sheleg-dev/skills/frontend-performance/references/nextjs.md +271 -0
  20. package/plugins/sheleg-dev/skills/google-auth/SKILL.md +275 -0
  21. package/plugins/sheleg-dev/skills/google-auth/references/adc-and-service-accounts.md +464 -0
  22. package/plugins/sheleg-dev/skills/google-auth/references/oauth2-web-server.md +615 -0
  23. package/plugins/sheleg-dev/skills/google-auth/references/sign-in-with-google.md +593 -0
  24. package/plugins/sheleg-dev/skills/google-auth/references/workload-identity.md +376 -0
  25. package/plugins/sheleg-dev/skills/google-signin/SKILL.md +121 -0
  26. package/plugins/sheleg-dev/skills/google-signin/references/full-guide.md +421 -0
  27. package/plugins/sheleg-dev/skills/stripe-billing/SKILL.md +458 -0
  28. package/plugins/sheleg-dev/skills/stripe-billing/references/price-integrity.md +201 -0
  29. package/plugins/sheleg-dev/skills/stripe-billing/references/stripe-agent-toolchain.md +173 -0
  30. package/plugins/sheleg-dev/skills/stripe-billing/references/subscription-lifecycle.md +294 -0
  31. package/plugins/sheleg-dev/skills/stripe-billing/references/testing-and-local-dev.md +214 -0
  32. package/plugins/sheleg-dev/skills/stripe-billing/references/webhook-events.md +211 -0
@@ -0,0 +1,376 @@
1
+ # Workload & Workforce Identity Federation (Node.js & Python)
2
+
3
+
4
+ ## Contents
5
+
6
+ - [Table of Contents](#table-of-contents)
7
+ - [Overview](#overview)
8
+ - [AWS Federation](#aws-federation)
9
+ - [Azure Federation](#azure-federation)
10
+ - [OIDC Provider Federation](#oidc-provider-federation)
11
+ - [Custom Subject Token Supplier](#custom-subject-token-supplier)
12
+ - [Workforce Identity Federation](#workforce-identity-federation)
13
+ - [Executable-Sourced Credentials](#executable-sourced-credentials)
14
+ - [Using External Identities](#using-external-identities)
15
+ - [Configurable Token Lifetime](#configurable-token-lifetime)
16
+ - [Security Considerations](#security-considerations)
17
+
18
+ ## Table of Contents
19
+ - [Overview](#overview)
20
+ - [AWS Federation](#aws-federation)
21
+ - [Azure Federation](#azure-federation)
22
+ - [OIDC Provider Federation](#oidc-provider-federation)
23
+ - [Custom Subject Token Supplier](#custom-subject-token-supplier)
24
+ - [Workforce Identity Federation](#workforce-identity-federation)
25
+ - [Executable-Sourced Credentials](#executable-sourced-credentials)
26
+ - [Using External Identities](#using-external-identities)
27
+ - [Configurable Token Lifetime](#configurable-token-lifetime)
28
+ - [Security Considerations](#security-considerations)
29
+
30
+ ## Overview
31
+
32
+ Workload Identity Federation lets non-GCP workloads (AWS, Azure, OIDC/SAML providers) access Google Cloud without service account keys. It works by exchanging an external token for a short-lived GCP access token via the Security Token Service (STS).
33
+
34
+ Benefits:
35
+ - No service account key management
36
+ - No key rotation needed
37
+ - Short-lived credentials only
38
+ - Works across cloud providers
39
+
40
+ ## AWS Federation
41
+
42
+ ### Setup
43
+
44
+ 1. Create a workload identity pool
45
+ 2. Add AWS as identity provider
46
+ 3. Grant `roles/iam.workloadIdentityUser` to external identity
47
+ 4. Generate credential configuration:
48
+
49
+ ```bash
50
+ gcloud iam workload-identity-pools create-cred-config \
51
+ projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL_ID/providers/$AWS_PROVIDER_ID \
52
+ --service-account $SERVICE_ACCOUNT_EMAIL \
53
+ --aws \
54
+ --output-file /path/to/config.json
55
+ ```
56
+
57
+ ### Custom AWS Supplier — Node.js
58
+
59
+ For non-EC2 environments (ECS, EKS, Fargate):
60
+
61
+ ```ts
62
+ import {AwsClient, AwsSecurityCredentials, AwsSecurityCredentialsSupplier, ExternalAccountSupplierContext} from 'google-auth-library';
63
+ import {fromNodeProviderChain} from '@aws-sdk/credential-providers';
64
+
65
+ class AwsSupplier implements AwsSecurityCredentialsSupplier {
66
+ private readonly region: string;
67
+ constructor(region: string) { this.region = region; }
68
+
69
+ async getAwsRegion(context: ExternalAccountSupplierContext): Promise<string> {
70
+ return this.region;
71
+ }
72
+
73
+ async getAwsSecurityCredentials(context: ExternalAccountSupplierContext): Promise<AwsSecurityCredentials> {
74
+ const creds = await fromNodeProviderChain()();
75
+ return {
76
+ accessKeyId: creds.accessKeyId,
77
+ secretAccessKey: creds.secretAccessKey,
78
+ token: creds.sessionToken
79
+ };
80
+ }
81
+ }
82
+
83
+ const authClient = new AwsClient({
84
+ audience: '//iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL_ID/providers/$PROVIDER_ID',
85
+ subject_token_type: 'urn:ietf:params:aws:token-type:aws4_request',
86
+ aws_security_credentials_supplier: new AwsSupplier('us-east-1'),
87
+ service_account_impersonation_url: 'https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/$EMAIL:generateAccessToken',
88
+ });
89
+ ```
90
+
91
+ ### Custom AWS Supplier — Python
92
+
93
+ ```python
94
+ from google.auth import aws
95
+ from google.auth import exceptions
96
+
97
+ class CustomAwsSecurityCredentialsSupplier(aws.AwsSecurityCredentialsSupplier):
98
+
99
+ def get_aws_security_credentials(self, context, request):
100
+ try:
101
+ # Return valid AWS security credentials.
102
+ # These are NOT cached by google-auth — implement caching here.
103
+ return aws.AwsSecurityCredentials(
104
+ access_key_id=ACCESS_KEY_ID,
105
+ secret_access_key=SECRET_ACCESS_KEY,
106
+ session_token=SESSION_TOKEN
107
+ )
108
+ except Exception as e:
109
+ raise exceptions.RefreshError(e, retryable=True)
110
+
111
+ def get_aws_region(self, context, request):
112
+ return 'us-east-1'
113
+
114
+ supplier = CustomAwsSecurityCredentialsSupplier()
115
+
116
+ credentials = aws.Credentials(
117
+ audience='//iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL_ID/providers/$PROVIDER_ID',
118
+ subject_token_type='urn:ietf:params:aws:token-type:aws4_request',
119
+ aws_security_credentials_supplier=supplier,
120
+ scopes=['https://www.googleapis.com/auth/cloud-platform']
121
+ )
122
+ ```
123
+
124
+ ## Azure Federation
125
+
126
+ ```bash
127
+ gcloud iam workload-identity-pools create-cred-config \
128
+ projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL_ID/providers/$AZURE_PROVIDER_ID \
129
+ --service-account $SERVICE_ACCOUNT_EMAIL \
130
+ --azure \
131
+ --output-file /path/to/config.json
132
+ ```
133
+
134
+ ## OIDC Provider Federation
135
+
136
+ ### File-Sourced
137
+
138
+ ```bash
139
+ gcloud iam workload-identity-pools create-cred-config \
140
+ projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL_ID/providers/$OIDC_PROVIDER_ID \
141
+ --service-account $SERVICE_ACCOUNT_EMAIL \
142
+ --credential-source-file $PATH_TO_OIDC_TOKEN \
143
+ --output-file /path/to/config.json
144
+ ```
145
+
146
+ ### URL-Sourced
147
+
148
+ ```bash
149
+ gcloud iam workload-identity-pools create-cred-config \
150
+ projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL_ID/providers/$OIDC_PROVIDER_ID \
151
+ --service-account $SERVICE_ACCOUNT_EMAIL \
152
+ --credential-source-url $URL_TO_GET_TOKEN \
153
+ --credential-source-headers Metadata-Flavor=Google \
154
+ --output-file /path/to/config.json
155
+ ```
156
+
157
+ ## Custom Subject Token Supplier
158
+
159
+ For OIDC/SAML tokens from custom sources.
160
+
161
+ ### Node.js
162
+
163
+ ```ts
164
+ import {IdentityPoolClient, SubjectTokenSupplier, ExternalAccountSupplierContext} from 'google-auth-library';
165
+
166
+ class CustomSupplier implements SubjectTokenSupplier {
167
+ async getSubjectToken(context: ExternalAccountSupplierContext): Promise<string> {
168
+ // Implement caching — library does not cache
169
+ const audience = context.audience;
170
+ const subjectTokenType = context.subjectTokenType;
171
+ return fetchTokenFromProvider();
172
+ }
173
+ }
174
+
175
+ const client = new IdentityPoolClient({
176
+ audience: '//iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL_ID/providers/$PROVIDER_ID',
177
+ subject_token_type: 'urn:ietf:params:oauth:token-type:id_token',
178
+ subject_token_supplier: new CustomSupplier()
179
+ });
180
+ ```
181
+
182
+ ### Python
183
+
184
+ ```python
185
+ from google.auth import identity_pool
186
+ from google.auth import exceptions
187
+
188
+ class CustomSubjectTokenSupplier(identity_pool.SubjectTokenSupplier):
189
+
190
+ def get_subject_token(self, context, request):
191
+ try:
192
+ # Implement caching — library does not cache external tokens.
193
+ audience = context.audience
194
+ subject_token_type = context.subject_token_type
195
+ return fetch_token_from_provider()
196
+ except Exception as e:
197
+ raise exceptions.RefreshError(e, retryable=True)
198
+
199
+ supplier = CustomSubjectTokenSupplier()
200
+
201
+ credentials = identity_pool.Credentials(
202
+ audience='//iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL_ID/providers/$PROVIDER_ID',
203
+ subject_token_type='urn:ietf:params:oauth:token-type:id_token',
204
+ subject_token_supplier=supplier,
205
+ scopes=['https://www.googleapis.com/auth/cloud-platform']
206
+ )
207
+ ```
208
+
209
+ ## Workforce Identity Federation
210
+
211
+ For human users (employees, partners) authenticating via external IdP (Azure AD, Okta, AD FS).
212
+
213
+ ### Generate Config
214
+
215
+ ```bash
216
+ # OIDC
217
+ gcloud iam workforce-pools create-cred-config \
218
+ locations/global/workforcePools/$WORKFORCE_POOL_ID/providers/$PROVIDER_ID \
219
+ --subject-token-type=urn:ietf:params:oauth:token-type:id_token \
220
+ --credential-source-file=$PATH_TO_TOKEN \
221
+ --workforce-pool-user-project=$PROJECT_NUMBER \
222
+ --output-file=/path/to/config.json
223
+
224
+ # SAML
225
+ gcloud iam workforce-pools create-cred-config \
226
+ locations/global/workforcePools/$WORKFORCE_POOL_ID/providers/$PROVIDER_ID \
227
+ --credential-source-file=$PATH_TO_SAML_ASSERTION \
228
+ --subject-token-type=urn:ietf:params:oauth:token-type:saml2 \
229
+ --workforce-pool-user-project=$PROJECT_NUMBER \
230
+ --output-file=/path/to/config.json
231
+ ```
232
+
233
+ ### Browser-Based Login
234
+
235
+ ```bash
236
+ gcloud auth application-default login --login-config=$LOGIN_CONFIG
237
+ ```
238
+
239
+ Default refresh token lifetime: 1 hour (configurable up to 12 hours via pool session duration).
240
+
241
+ ## Executable-Sourced Credentials
242
+
243
+ For custom token retrieval via local executable:
244
+
245
+ ```bash
246
+ gcloud iam workload-identity-pools create-cred-config \
247
+ projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL_ID/providers/$PROVIDER_ID \
248
+ --service-account=$SERVICE_ACCOUNT_EMAIL \
249
+ --subject-token-type=$SUBJECT_TOKEN_TYPE \
250
+ --executable-command="/path/to/command --arg=value" \
251
+ --executable-timeout-millis=30000 \
252
+ --executable-output-file=/path/to/cached-token.json \
253
+ --output-file /path/to/config.json
254
+ ```
255
+
256
+ Requires: `export GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES=1`
257
+
258
+ ### Executable Response Format
259
+
260
+ Success (OIDC):
261
+ ```json
262
+ {"version": 1, "success": true, "token_type": "urn:ietf:params:oauth:token-type:id_token", "id_token": "HEADER.PAYLOAD.SIGNATURE", "expiration_time": 1620499962}
263
+ ```
264
+
265
+ Error:
266
+ ```json
267
+ {"version": 1, "success": false, "code": "401", "message": "Caller not authorized."}
268
+ ```
269
+
270
+ ### Environment Variables Set for Executable
271
+
272
+ | Variable | Description |
273
+ |----------|-------------|
274
+ | `GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE` | Audience from config |
275
+ | `GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL` | SA email (if impersonating) |
276
+ | `GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE` | Output file path (if set) |
277
+ | `GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE` | Expected subject token type |
278
+
279
+ ## Using External Identities
280
+
281
+ After generating a config JSON, use with ADC.
282
+
283
+ ### Environment Setup
284
+
285
+ ```bash
286
+ export GOOGLE_APPLICATION_CREDENTIALS=/path/to/config.json
287
+ ```
288
+
289
+ ### Node.js
290
+
291
+ ```js
292
+ const {GoogleAuth} = require('google-auth-library');
293
+
294
+ const auth = new GoogleAuth({
295
+ scopes: 'https://www.googleapis.com/auth/cloud-platform',
296
+ projectId: 'CLOUD_RESOURCE_PROJECT_ID' // avoids needing roles/browser
297
+ });
298
+ const client = await auth.getClient();
299
+ const res = await client.fetch(url);
300
+ ```
301
+
302
+ Or explicit initialization:
303
+
304
+ ```js
305
+ const {ExternalAccountClient} = require('google-auth-library');
306
+ const jsonConfig = require('/path/to/config.json');
307
+
308
+ const client = ExternalAccountClient.fromJSON(jsonConfig);
309
+ client.scopes = ['https://www.googleapis.com/auth/cloud-platform'];
310
+ ```
311
+
312
+ ### Python
313
+
314
+ ```python
315
+ import google.auth
316
+
317
+ # Via ADC (auto-detects external account config from GOOGLE_APPLICATION_CREDENTIALS)
318
+ credentials, project = google.auth.default(
319
+ scopes=['https://www.googleapis.com/auth/cloud-platform']
320
+ )
321
+ ```
322
+
323
+ Or load from file explicitly:
324
+
325
+ ```python
326
+ import google.auth
327
+
328
+ credentials, project = google.auth.load_credentials_from_file(
329
+ '/path/to/config.json',
330
+ scopes=['https://www.googleapis.com/auth/cloud-platform']
331
+ )
332
+ ```
333
+
334
+ Or direct instantiation by credential type:
335
+
336
+ ```python
337
+ # Identity Pool (OIDC/SAML)
338
+ from google.auth import identity_pool
339
+
340
+ credentials = identity_pool.Credentials.from_file(
341
+ '/path/to/config.json',
342
+ scopes=['https://www.googleapis.com/auth/cloud-platform']
343
+ )
344
+
345
+ # AWS
346
+ from google.auth import aws
347
+
348
+ credentials = aws.Credentials.from_file(
349
+ '/path/to/config.json',
350
+ scopes=['https://www.googleapis.com/auth/cloud-platform']
351
+ )
352
+ ```
353
+
354
+ Requirements for ADC with external identities:
355
+ - Grant `roles/browser` to the service account (or specify `projectId` explicitly)
356
+ - Enable Cloud Resource Manager API on the project
357
+
358
+ ## Configurable Token Lifetime
359
+
360
+ ```bash
361
+ gcloud iam workload-identity-pools create-cred-config \
362
+ ... \
363
+ --service-account-token-lifetime-seconds=3600
364
+ ```
365
+
366
+ - Default: 3600 (1 hour)
367
+ - Min: 600 (10 minutes), Max: 43200 (12 hours)
368
+ - Lifetimes > 1 hour require org policy: `constraints/iam.allowServiceAccountCredentialLifetimeExtension`
369
+
370
+ ## Security Considerations
371
+
372
+ - **Validate external credential configs** — verify `token_url`, `token_info_url`, `service_account_impersonation_url` point to `googleapis.com`
373
+ - **Restrict executable access** — prevent unauthorized processes from reading credentials from stdout
374
+ - **Make config files read-only** — prevent modification of executable command paths
375
+ - **Prefer file/URL-sourced** over executable-sourced credentials when possible
376
+ - **Implement caching** in custom suppliers — neither the Node.js nor Python library caches external tokens
@@ -0,0 +1,121 @@
1
+ ---
2
+ name: google-signin
3
+ description: >-
4
+ Use when implementing, reviewing or debugging Google login / sign-in / sign-up on a website —
5
+ wiring the GIS button or One Tap, verifying Google ID tokens on the server, linking Google to
6
+ existing password accounts, or fixing "origin is not allowed for the given client ID". Covers
7
+ GCP OAuth client setup, backend ID-token verification, three-way account linking with the pre-
8
+ hijacking guard, login-CSRF defense, nonce/replay protection and a mandatory security
9
+ checklist. Triggers - "sign in with google", "google login", "google sign-in button", "GIS",
10
+ "google.accounts.id", "gsi/client", "verify google token", "one tap", "account linking",
11
+ "login csrf", "g_csrf_token", "вход через Google", "кнопка входа Google", "связать аккаунты",
12
+ "проверить токен Google". For the broader library surface use google-auth instead.
13
+ ---
14
+
15
+ # Google Sign-In (GIS) — production web login
16
+
17
+ Condensed operating instructions. Full narrative guide with diagrams, token
18
+ anatomy, troubleshooting table, and a copy-paste FastAPI + JS skeleton:
19
+ [references/full-guide.md](references/full-guide.md) — read it when
20
+ implementing from scratch. A live reference implementation exists in the
21
+ Prowl repo (`0xDEV`): `web/auth.py`, `web/server.py:1657`
22
+ (`/api/auth/google`), `web/static/js/app.js:16-110`.
23
+
24
+ ## Pick the right flow first
25
+
26
+ - Only need "who is this user?" → **GIS ID-token flow** (this skill): load
27
+ `https://accounts.google.com/gsi/client`, receive one Google-signed JWT,
28
+ verify server-side. No redirect URI, no client secret, no Google-token
29
+ storage.
30
+ - Need to call Google APIs (Gmail/Drive/Calendar) on the user's behalf →
31
+ OAuth 2.0 **authorization-code** flow instead (redirect URI + client
32
+ secret + refresh tokens). Never use the code flow just for login.
33
+
34
+ ## Core flow (ID-token)
35
+
36
+ 1. Frontend: `google.accounts.id.initialize({ client_id, callback, nonce })`
37
+ + `renderButton()`. Generate a **fresh random nonce per render**
38
+ (`crypto.randomUUID()`). The GSI script loads async — retry rendering
39
+ (e.g. 20 × 150 ms) instead of silently dropping the button.
40
+ 2. Callback receives `{ credential }` — a Google-signed ID token (JWT).
41
+ POST it with the nonce to your backend. Never treat it as a session.
42
+ 3. Backend: verify with the official lib — Python
43
+ `google.oauth2.id_token.verify_oauth2_token(credential, transport, CLIENT_ID)`,
44
+ Node `google-auth-library` `verifyIdToken({ idToken, audience })`. That
45
+ checks signature (Google JWKS), `aud`, `iss`, `exp`. Never hand-decode
46
+ and trust the payload.
47
+ 4. Additionally require `email_verified == true` and token `nonce` claim ==
48
+ submitted nonce.
49
+ 5. Find-or-create the user, then issue YOUR OWN session (app JWT) as an
50
+ **HttpOnly + Secure + SameSite=Strict cookie**. Google's token is
51
+ verified once, never stored, never logged.
52
+
53
+ ## Identity & account linking (three-way branch)
54
+
55
+ Key the user on `sub` (stable Google user ID; store as `google_id`) — never
56
+ on email (emails change, `sub` doesn't). On each Google login:
57
+
58
+ 1. Record with this `google_id` exists → login (refresh name/picture).
59
+ 2. Email matches an existing account without `google_id` → link, BUT apply
60
+ the **pre-hijacking guard**: if that account is password-based and its
61
+ email was never verified, REFUSE the auto-link ("sign in with your
62
+ password instead"). Otherwise an attacker who pre-registered the
63
+ victim's email with a known password captures the victim's first Google
64
+ login into the attacker's record.
65
+ 3. No match → create the user with `email_verified=True`, empty password
66
+ hash.
67
+
68
+ ## Login-CSRF (cover BOTH delivery flows)
69
+
70
+ - Form-POST flow (`login_uri` auto-POST): GIS double-submits `g_csrf_token`
71
+ in body AND cookie — compare constant-time (`hmac.compare_digest`);
72
+ reject if either side is missing.
73
+ - JS-fetch flow: no `g_csrf_token` — enforce same-origin via Fetch-Metadata
74
+ (`Sec-Fetch-Site: same-origin`) with an Origin-allowlist fallback.
75
+
76
+ Without this, an attacker's page can force-POST the *attacker's* credential
77
+ and silently log the victim into the attacker's account.
78
+
79
+ ## Setup (GCP)
80
+
81
+ 1. Cloud Console → APIs & Services → OAuth consent screen (External;
82
+ publish it — in "Testing" only allowlisted users can sign in).
83
+ 2. Credentials → Create OAuth client ID → **Web application** →
84
+ **Authorized JavaScript origins** = every origin the button renders on,
85
+ incl. `http://localhost:<port>` for dev (scheme+host+port, no path, no
86
+ trailing slash). **Authorized redirect URIs: leave empty** for GIS.
87
+ 3. Ship only `GOOGLE_CLIENT_ID` (public; env var; one source of truth —
88
+ inject into HTML server-side, e.g. via a `<meta>` tag). The client
89
+ secret is UNUSED in the GIS flow — never put it in the app.
90
+ 4. CSP if present: allow `https://accounts.google.com` in `script-src`,
91
+ `connect-src`, `frame-src`.
92
+ 5. If `GOOGLE_CLIENT_ID` is empty → hide the button and return
93
+ "not configured" server-side (honest degradation, no crash).
94
+
95
+ ## Security checklist (verify ALL before calling it done)
96
+
97
+ - [ ] Signature verified via official lib against Google JWKS (no
98
+ `verify=False`, no manual base64 decode-and-trust)
99
+ - [ ] `aud` == your client ID; `exp` enforced
100
+ - [ ] `email_verified` required
101
+ - [ ] Nonce: fresh per render, checked server-side against the token claim
102
+ - [ ] Pre-hijacking guard on email-based linking
103
+ - [ ] Login-CSRF: `g_csrf_token` double-submit AND same-origin check for
104
+ the JS flow
105
+ - [ ] Own session cookie: HttpOnly + Secure + SameSite=Strict
106
+ - [ ] `/api/auth/*` rate-limited; auth events logged WITHOUT the credential
107
+ - [ ] Session revocation path exists (e.g. an `auth_version`/`ver` claim
108
+ check)
109
+ - [ ] Logout also calls `google.accounts.id.disableAutoSelect()`
110
+
111
+ ## Debugging quick table
112
+
113
+ | Symptom | Fix |
114
+ |---|---|
115
+ | "The given origin is not allowed for the given client ID" | Add the exact origin (port, `www.`) to Authorized JavaScript origins; propagation takes minutes |
116
+ | Button never renders | GSI async race (add retry loop), empty client ID, or CSP blocks `accounts.google.com` |
117
+ | "Token used too early/expired" | Server clock skew → sync NTP |
118
+ | Audience mismatch | Frontend/backend client IDs differ → single env source of truth |
119
+ | `access_denied` on consent | Consent screen still in Testing → publish the app |
120
+ | Works locally, breaks in prod | Prod origin missing in GCP, or `Secure` cookie served over plain HTTP |
121
+ | One Tap missing / FedCM console warnings | FedCM is mandatory since Aug 2025 and the opt-out is gone (checked 2026-08-06). Load `gsi/client` from Google and keep it current — a vendored or pinned copy is the real break. The rendered button flow is unaffected; code branching on the old `isNotDisplayed()` moment callbacks is not |