@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,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 |
|