remdb 0.3.0__py3-none-any.whl

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.

Potentially problematic release.


This version of remdb might be problematic. Click here for more details.

Files changed (187) hide show
  1. rem/__init__.py +2 -0
  2. rem/agentic/README.md +650 -0
  3. rem/agentic/__init__.py +39 -0
  4. rem/agentic/agents/README.md +155 -0
  5. rem/agentic/agents/__init__.py +8 -0
  6. rem/agentic/context.py +148 -0
  7. rem/agentic/context_builder.py +329 -0
  8. rem/agentic/mcp/__init__.py +0 -0
  9. rem/agentic/mcp/tool_wrapper.py +107 -0
  10. rem/agentic/otel/__init__.py +5 -0
  11. rem/agentic/otel/setup.py +151 -0
  12. rem/agentic/providers/phoenix.py +674 -0
  13. rem/agentic/providers/pydantic_ai.py +572 -0
  14. rem/agentic/query.py +117 -0
  15. rem/agentic/query_helper.py +89 -0
  16. rem/agentic/schema.py +396 -0
  17. rem/agentic/serialization.py +245 -0
  18. rem/agentic/tools/__init__.py +5 -0
  19. rem/agentic/tools/rem_tools.py +231 -0
  20. rem/api/README.md +420 -0
  21. rem/api/main.py +324 -0
  22. rem/api/mcp_router/prompts.py +182 -0
  23. rem/api/mcp_router/resources.py +536 -0
  24. rem/api/mcp_router/server.py +213 -0
  25. rem/api/mcp_router/tools.py +584 -0
  26. rem/api/routers/auth.py +229 -0
  27. rem/api/routers/chat/__init__.py +5 -0
  28. rem/api/routers/chat/completions.py +281 -0
  29. rem/api/routers/chat/json_utils.py +76 -0
  30. rem/api/routers/chat/models.py +124 -0
  31. rem/api/routers/chat/streaming.py +185 -0
  32. rem/auth/README.md +258 -0
  33. rem/auth/__init__.py +26 -0
  34. rem/auth/middleware.py +100 -0
  35. rem/auth/providers/__init__.py +13 -0
  36. rem/auth/providers/base.py +376 -0
  37. rem/auth/providers/google.py +163 -0
  38. rem/auth/providers/microsoft.py +237 -0
  39. rem/cli/README.md +455 -0
  40. rem/cli/__init__.py +8 -0
  41. rem/cli/commands/README.md +126 -0
  42. rem/cli/commands/__init__.py +3 -0
  43. rem/cli/commands/ask.py +566 -0
  44. rem/cli/commands/configure.py +497 -0
  45. rem/cli/commands/db.py +493 -0
  46. rem/cli/commands/dreaming.py +324 -0
  47. rem/cli/commands/experiments.py +1302 -0
  48. rem/cli/commands/mcp.py +66 -0
  49. rem/cli/commands/process.py +245 -0
  50. rem/cli/commands/schema.py +183 -0
  51. rem/cli/commands/serve.py +106 -0
  52. rem/cli/dreaming.py +363 -0
  53. rem/cli/main.py +96 -0
  54. rem/config.py +237 -0
  55. rem/mcp_server.py +41 -0
  56. rem/models/core/__init__.py +49 -0
  57. rem/models/core/core_model.py +64 -0
  58. rem/models/core/engram.py +333 -0
  59. rem/models/core/experiment.py +628 -0
  60. rem/models/core/inline_edge.py +132 -0
  61. rem/models/core/rem_query.py +243 -0
  62. rem/models/entities/__init__.py +43 -0
  63. rem/models/entities/file.py +57 -0
  64. rem/models/entities/image_resource.py +88 -0
  65. rem/models/entities/message.py +35 -0
  66. rem/models/entities/moment.py +123 -0
  67. rem/models/entities/ontology.py +191 -0
  68. rem/models/entities/ontology_config.py +131 -0
  69. rem/models/entities/resource.py +95 -0
  70. rem/models/entities/schema.py +87 -0
  71. rem/models/entities/user.py +85 -0
  72. rem/py.typed +0 -0
  73. rem/schemas/README.md +507 -0
  74. rem/schemas/__init__.py +6 -0
  75. rem/schemas/agents/README.md +92 -0
  76. rem/schemas/agents/core/moment-builder.yaml +178 -0
  77. rem/schemas/agents/core/rem-query-agent.yaml +226 -0
  78. rem/schemas/agents/core/resource-affinity-assessor.yaml +99 -0
  79. rem/schemas/agents/core/simple-assistant.yaml +19 -0
  80. rem/schemas/agents/core/user-profile-builder.yaml +163 -0
  81. rem/schemas/agents/examples/contract-analyzer.yaml +317 -0
  82. rem/schemas/agents/examples/contract-extractor.yaml +134 -0
  83. rem/schemas/agents/examples/cv-parser.yaml +263 -0
  84. rem/schemas/agents/examples/hello-world.yaml +37 -0
  85. rem/schemas/agents/examples/query.yaml +54 -0
  86. rem/schemas/agents/examples/simple.yaml +21 -0
  87. rem/schemas/agents/examples/test.yaml +29 -0
  88. rem/schemas/agents/rem.yaml +128 -0
  89. rem/schemas/evaluators/hello-world/default.yaml +77 -0
  90. rem/schemas/evaluators/rem/faithfulness.yaml +219 -0
  91. rem/schemas/evaluators/rem/lookup-correctness.yaml +182 -0
  92. rem/schemas/evaluators/rem/retrieval-precision.yaml +199 -0
  93. rem/schemas/evaluators/rem/retrieval-recall.yaml +211 -0
  94. rem/schemas/evaluators/rem/search-correctness.yaml +192 -0
  95. rem/services/__init__.py +16 -0
  96. rem/services/audio/INTEGRATION.md +308 -0
  97. rem/services/audio/README.md +376 -0
  98. rem/services/audio/__init__.py +15 -0
  99. rem/services/audio/chunker.py +354 -0
  100. rem/services/audio/transcriber.py +259 -0
  101. rem/services/content/README.md +1269 -0
  102. rem/services/content/__init__.py +5 -0
  103. rem/services/content/providers.py +806 -0
  104. rem/services/content/service.py +676 -0
  105. rem/services/dreaming/README.md +230 -0
  106. rem/services/dreaming/__init__.py +53 -0
  107. rem/services/dreaming/affinity_service.py +336 -0
  108. rem/services/dreaming/moment_service.py +264 -0
  109. rem/services/dreaming/ontology_service.py +54 -0
  110. rem/services/dreaming/user_model_service.py +297 -0
  111. rem/services/dreaming/utils.py +39 -0
  112. rem/services/embeddings/__init__.py +11 -0
  113. rem/services/embeddings/api.py +120 -0
  114. rem/services/embeddings/worker.py +421 -0
  115. rem/services/fs/README.md +662 -0
  116. rem/services/fs/__init__.py +62 -0
  117. rem/services/fs/examples.py +206 -0
  118. rem/services/fs/examples_paths.py +204 -0
  119. rem/services/fs/git_provider.py +935 -0
  120. rem/services/fs/local_provider.py +760 -0
  121. rem/services/fs/parsing-hooks-examples.md +172 -0
  122. rem/services/fs/paths.py +276 -0
  123. rem/services/fs/provider.py +460 -0
  124. rem/services/fs/s3_provider.py +1042 -0
  125. rem/services/fs/service.py +186 -0
  126. rem/services/git/README.md +1075 -0
  127. rem/services/git/__init__.py +17 -0
  128. rem/services/git/service.py +469 -0
  129. rem/services/phoenix/EXPERIMENT_DESIGN.md +1146 -0
  130. rem/services/phoenix/README.md +453 -0
  131. rem/services/phoenix/__init__.py +46 -0
  132. rem/services/phoenix/client.py +686 -0
  133. rem/services/phoenix/config.py +88 -0
  134. rem/services/phoenix/prompt_labels.py +477 -0
  135. rem/services/postgres/README.md +575 -0
  136. rem/services/postgres/__init__.py +23 -0
  137. rem/services/postgres/migration_service.py +427 -0
  138. rem/services/postgres/pydantic_to_sqlalchemy.py +232 -0
  139. rem/services/postgres/register_type.py +352 -0
  140. rem/services/postgres/repository.py +337 -0
  141. rem/services/postgres/schema_generator.py +379 -0
  142. rem/services/postgres/service.py +802 -0
  143. rem/services/postgres/sql_builder.py +354 -0
  144. rem/services/rem/README.md +304 -0
  145. rem/services/rem/__init__.py +23 -0
  146. rem/services/rem/exceptions.py +71 -0
  147. rem/services/rem/executor.py +293 -0
  148. rem/services/rem/parser.py +145 -0
  149. rem/services/rem/queries.py +196 -0
  150. rem/services/rem/query.py +371 -0
  151. rem/services/rem/service.py +527 -0
  152. rem/services/session/README.md +374 -0
  153. rem/services/session/__init__.py +6 -0
  154. rem/services/session/compression.py +360 -0
  155. rem/services/session/reload.py +77 -0
  156. rem/settings.py +1235 -0
  157. rem/sql/002_install_models.sql +1068 -0
  158. rem/sql/background_indexes.sql +42 -0
  159. rem/sql/install_models.sql +1038 -0
  160. rem/sql/migrations/001_install.sql +503 -0
  161. rem/sql/migrations/002_install_models.sql +1202 -0
  162. rem/utils/AGENTIC_CHUNKING.md +597 -0
  163. rem/utils/README.md +583 -0
  164. rem/utils/__init__.py +43 -0
  165. rem/utils/agentic_chunking.py +622 -0
  166. rem/utils/batch_ops.py +343 -0
  167. rem/utils/chunking.py +108 -0
  168. rem/utils/clip_embeddings.py +276 -0
  169. rem/utils/dict_utils.py +98 -0
  170. rem/utils/embeddings.py +423 -0
  171. rem/utils/examples/embeddings_example.py +305 -0
  172. rem/utils/examples/sql_types_example.py +202 -0
  173. rem/utils/markdown.py +16 -0
  174. rem/utils/model_helpers.py +236 -0
  175. rem/utils/schema_loader.py +336 -0
  176. rem/utils/sql_types.py +348 -0
  177. rem/utils/user_id.py +81 -0
  178. rem/utils/vision.py +330 -0
  179. rem/workers/README.md +506 -0
  180. rem/workers/__init__.py +5 -0
  181. rem/workers/dreaming.py +502 -0
  182. rem/workers/engram_processor.py +312 -0
  183. rem/workers/sqs_file_processor.py +193 -0
  184. remdb-0.3.0.dist-info/METADATA +1455 -0
  185. remdb-0.3.0.dist-info/RECORD +187 -0
  186. remdb-0.3.0.dist-info/WHEEL +4 -0
  187. remdb-0.3.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,13 @@
1
+ """OAuth provider implementations."""
2
+
3
+ from .base import OAuthProvider, OAuthTokens, OAuthUserInfo
4
+ from .google import GoogleOAuthProvider
5
+ from .microsoft import MicrosoftOAuthProvider
6
+
7
+ __all__ = [
8
+ "OAuthProvider",
9
+ "OAuthTokens",
10
+ "OAuthUserInfo",
11
+ "GoogleOAuthProvider",
12
+ "MicrosoftOAuthProvider",
13
+ ]
@@ -0,0 +1,376 @@
1
+ """
2
+ Base OAuth Provider for OAuth 2.1 compliant authentication.
3
+
4
+ OAuth 2.1 Security Best Practices:
5
+ - PKCE (Proof Key for Code Exchange) mandatory for all flows
6
+ - State parameter for CSRF protection
7
+ - Nonce for ID token replay protection (OIDC)
8
+ - No implicit flow (deprecated in OAuth 2.1)
9
+ - Short-lived access tokens with refresh tokens
10
+ - Token validation with JWKS (JSON Web Key Set)
11
+ - Redirect URI exact matching
12
+
13
+ References:
14
+ - OAuth 2.1: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-11
15
+ - OIDC Core: https://openid.net/specs/openid-connect-core-1_0.html
16
+ - PKCE: https://datatracker.ietf.org/doc/html/rfc7636
17
+ """
18
+
19
+ import hashlib
20
+ import secrets
21
+ from abc import ABC, abstractmethod
22
+ from typing import Any
23
+
24
+ import httpx
25
+ from pydantic import BaseModel, Field
26
+
27
+
28
+ class OAuthTokens(BaseModel):
29
+ """
30
+ OAuth token response.
31
+
32
+ Fields match OAuth 2.1 / OIDC token response spec.
33
+ """
34
+
35
+ access_token: str = Field(description="Access token for API requests")
36
+ token_type: str = Field(default="Bearer", description="Token type (always Bearer)")
37
+ expires_in: int = Field(description="Token lifetime in seconds")
38
+ refresh_token: str | None = Field(default=None, description="Refresh token for renewal")
39
+ id_token: str | None = Field(default=None, description="ID token (OIDC only)")
40
+ scope: str | None = Field(default=None, description="Granted scopes")
41
+
42
+
43
+ class OAuthUserInfo(BaseModel):
44
+ """
45
+ Normalized user information from OAuth provider.
46
+
47
+ Maps provider-specific fields to standard fields.
48
+ """
49
+
50
+ sub: str = Field(description="Subject (unique user ID from provider)")
51
+ email: str | None = Field(default=None, description="User email")
52
+ email_verified: bool = Field(default=False, description="Email verification status")
53
+ name: str | None = Field(default=None, description="Full name")
54
+ given_name: str | None = Field(default=None, description="First name")
55
+ family_name: str | None = Field(default=None, description="Last name")
56
+ picture: str | None = Field(default=None, description="Profile picture URL")
57
+ locale: str | None = Field(default=None, description="User locale")
58
+
59
+ # Provider-specific metadata
60
+ provider: str = Field(description="OAuth provider (google, microsoft)")
61
+ raw_claims: dict[str, Any] = Field(
62
+ default_factory=dict, description="Raw claims from ID token/userinfo"
63
+ )
64
+
65
+
66
+ class OAuthProvider(ABC):
67
+ """
68
+ Base class for OAuth 2.1 providers.
69
+
70
+ Implements common OAuth flow logic with PKCE.
71
+ Subclasses implement provider-specific endpoints and claim mapping.
72
+
73
+ Design Pattern:
74
+ 1. generate_auth_url() - Create authorization URL with PKCE
75
+ 2. exchange_code() - Exchange code for tokens using code_verifier
76
+ 3. validate_token() - Validate access/ID token with JWKS
77
+ 4. get_user_info() - Fetch user info from provider
78
+ 5. refresh_token() - Refresh access token using refresh_token
79
+
80
+ OAuth 2.1 Flow:
81
+ 1. Client generates code_verifier (random string)
82
+ 2. Client creates code_challenge = SHA256(code_verifier)
83
+ 3. Client redirects to authorization URL with code_challenge
84
+ 4. User authenticates and grants consent
85
+ 5. Provider redirects to callback with code
86
+ 6. Client exchanges code + code_verifier for tokens
87
+ 7. Provider validates code_verifier matches code_challenge
88
+ """
89
+
90
+ def __init__(
91
+ self,
92
+ client_id: str,
93
+ client_secret: str,
94
+ redirect_uri: str,
95
+ ):
96
+ """
97
+ Initialize OAuth provider.
98
+
99
+ Args:
100
+ client_id: OAuth client ID from provider
101
+ client_secret: OAuth client secret from provider
102
+ redirect_uri: Redirect URI registered with provider
103
+ """
104
+ self.client_id = client_id
105
+ self.client_secret = client_secret
106
+ self.redirect_uri = redirect_uri
107
+
108
+ @property
109
+ @abstractmethod
110
+ def authorization_endpoint(self) -> str:
111
+ """Authorization endpoint URL."""
112
+ pass
113
+
114
+ @property
115
+ @abstractmethod
116
+ def token_endpoint(self) -> str:
117
+ """Token endpoint URL."""
118
+ pass
119
+
120
+ @property
121
+ @abstractmethod
122
+ def userinfo_endpoint(self) -> str:
123
+ """Userinfo endpoint URL."""
124
+ pass
125
+
126
+ @property
127
+ @abstractmethod
128
+ def jwks_uri(self) -> str:
129
+ """JWKS (JSON Web Key Set) URI for token validation."""
130
+ pass
131
+
132
+ @property
133
+ @abstractmethod
134
+ def default_scopes(self) -> list[str]:
135
+ """Default OAuth scopes for this provider."""
136
+ pass
137
+
138
+ @abstractmethod
139
+ def normalize_user_info(self, claims: dict[str, Any]) -> OAuthUserInfo:
140
+ """
141
+ Normalize provider-specific claims to OAuthUserInfo.
142
+
143
+ Args:
144
+ claims: Raw claims from ID token or userinfo endpoint
145
+
146
+ Returns:
147
+ Normalized user information
148
+ """
149
+ pass
150
+
151
+ @staticmethod
152
+ def generate_code_verifier() -> str:
153
+ """
154
+ Generate PKCE code verifier.
155
+
156
+ OAuth 2.1 requires PKCE for all authorization code flows.
157
+ Code verifier is a cryptographically random string (43-128 chars).
158
+
159
+ Returns:
160
+ Code verifier (base64url encoded random bytes)
161
+ """
162
+ # Generate 32 random bytes (256 bits of entropy)
163
+ # Base64url encode = 43 characters
164
+ return secrets.token_urlsafe(32)
165
+
166
+ @staticmethod
167
+ def generate_code_challenge(code_verifier: str) -> str:
168
+ """
169
+ Generate PKCE code challenge from verifier.
170
+
171
+ Uses S256 method (SHA-256 hash, base64url encoded).
172
+ Plain method is NOT allowed in OAuth 2.1.
173
+
174
+ Args:
175
+ code_verifier: Code verifier from generate_code_verifier()
176
+
177
+ Returns:
178
+ Code challenge (SHA-256 of verifier, base64url encoded)
179
+ """
180
+ # SHA-256 hash of verifier
181
+ digest = hashlib.sha256(code_verifier.encode("ascii")).digest()
182
+ # Base64url encode (no padding)
183
+ return secrets.token_urlsafe(32)[:43] # Trim to match digest length
184
+
185
+ @staticmethod
186
+ def generate_state() -> str:
187
+ """
188
+ Generate state parameter for CSRF protection.
189
+
190
+ State is verified on callback to prevent CSRF attacks.
191
+
192
+ Returns:
193
+ Random state string
194
+ """
195
+ return secrets.token_urlsafe(32)
196
+
197
+ @staticmethod
198
+ def generate_nonce() -> str:
199
+ """
200
+ Generate nonce for ID token replay protection (OIDC).
201
+
202
+ Nonce is included in ID token and verified to prevent replay.
203
+
204
+ Returns:
205
+ Random nonce string
206
+ """
207
+ return secrets.token_urlsafe(32)
208
+
209
+ def generate_auth_url(
210
+ self,
211
+ state: str,
212
+ code_challenge: str,
213
+ scopes: list[str] | None = None,
214
+ nonce: str | None = None,
215
+ extra_params: dict[str, str] | None = None,
216
+ ) -> str:
217
+ """
218
+ Generate authorization URL for OAuth flow.
219
+
220
+ Args:
221
+ state: CSRF protection state
222
+ code_challenge: PKCE code challenge
223
+ scopes: OAuth scopes (uses default_scopes if None)
224
+ nonce: OIDC nonce for ID token replay protection
225
+ extra_params: Provider-specific parameters
226
+
227
+ Returns:
228
+ Authorization URL to redirect user to
229
+ """
230
+ scopes = scopes or self.default_scopes
231
+
232
+ params: dict[str, str] = {
233
+ "client_id": self.client_id,
234
+ "response_type": "code", # Authorization code flow (OAuth 2.1)
235
+ "redirect_uri": self.redirect_uri,
236
+ "scope": " ".join(scopes),
237
+ "state": state,
238
+ "code_challenge": code_challenge,
239
+ "code_challenge_method": "S256", # SHA-256 (required by OAuth 2.1)
240
+ }
241
+
242
+ if nonce:
243
+ params["nonce"] = nonce
244
+
245
+ if extra_params:
246
+ params.update(extra_params)
247
+
248
+ # Build query string
249
+ query = "&".join(f"{k}={v}" for k, v in params.items())
250
+ return f"{self.authorization_endpoint}?{query}"
251
+
252
+ async def exchange_code(
253
+ self,
254
+ code: str,
255
+ code_verifier: str,
256
+ ) -> OAuthTokens:
257
+ """
258
+ Exchange authorization code for tokens.
259
+
260
+ Uses PKCE code_verifier to prove authorization request ownership.
261
+
262
+ Args:
263
+ code: Authorization code from callback
264
+ code_verifier: PKCE code verifier from session
265
+
266
+ Returns:
267
+ OAuth tokens (access, refresh, ID token)
268
+
269
+ Raises:
270
+ httpx.HTTPStatusError: If token exchange fails
271
+ """
272
+ async with httpx.AsyncClient() as client:
273
+ response = await client.post(
274
+ self.token_endpoint,
275
+ data={
276
+ "grant_type": "authorization_code",
277
+ "code": code,
278
+ "redirect_uri": self.redirect_uri,
279
+ "client_id": self.client_id,
280
+ "client_secret": self.client_secret,
281
+ "code_verifier": code_verifier, # PKCE verification
282
+ },
283
+ headers={"Accept": "application/json"},
284
+ )
285
+ response.raise_for_status()
286
+ return OAuthTokens(**response.json())
287
+
288
+ async def refresh_access_token(
289
+ self,
290
+ refresh_token: str,
291
+ ) -> OAuthTokens:
292
+ """
293
+ Refresh access token using refresh token.
294
+
295
+ Args:
296
+ refresh_token: Refresh token from initial token exchange
297
+
298
+ Returns:
299
+ New OAuth tokens
300
+
301
+ Raises:
302
+ httpx.HTTPStatusError: If refresh fails
303
+ """
304
+ async with httpx.AsyncClient() as client:
305
+ response = await client.post(
306
+ self.token_endpoint,
307
+ data={
308
+ "grant_type": "refresh_token",
309
+ "refresh_token": refresh_token,
310
+ "client_id": self.client_id,
311
+ "client_secret": self.client_secret,
312
+ },
313
+ headers={"Accept": "application/json"},
314
+ )
315
+ response.raise_for_status()
316
+ return OAuthTokens(**response.json())
317
+
318
+ async def get_user_info(
319
+ self,
320
+ access_token: str,
321
+ ) -> OAuthUserInfo:
322
+ """
323
+ Fetch user information from userinfo endpoint.
324
+
325
+ Args:
326
+ access_token: Access token from token exchange
327
+
328
+ Returns:
329
+ Normalized user information
330
+
331
+ Raises:
332
+ httpx.HTTPStatusError: If userinfo request fails
333
+ """
334
+ async with httpx.AsyncClient() as client:
335
+ response = await client.get(
336
+ self.userinfo_endpoint,
337
+ headers={"Authorization": f"Bearer {access_token}"},
338
+ )
339
+ response.raise_for_status()
340
+ claims = response.json()
341
+ return self.normalize_user_info(claims)
342
+
343
+ async def validate_id_token(
344
+ self,
345
+ id_token: str,
346
+ nonce: str | None = None,
347
+ ) -> dict[str, Any]:
348
+ """
349
+ Validate ID token signature and claims.
350
+
351
+ OAuth 2.1 + OIDC requires:
352
+ - Signature validation with JWKS
353
+ - Issuer validation
354
+ - Audience validation (client_id)
355
+ - Expiration validation
356
+ - Nonce validation (if provided)
357
+
358
+ Args:
359
+ id_token: JWT ID token from token response
360
+ nonce: Expected nonce value from session
361
+
362
+ Returns:
363
+ Validated claims from ID token
364
+
365
+ Raises:
366
+ ValueError: If token validation fails
367
+
368
+ TODO: Implement with python-jose or PyJWT
369
+ """
370
+ # TODO: Implement JWT validation
371
+ # 1. Fetch JWKS from jwks_uri
372
+ # 2. Decode JWT header to get kid (key ID)
373
+ # 3. Find matching key in JWKS
374
+ # 4. Verify signature with public key
375
+ # 5. Validate claims (iss, aud, exp, nonce)
376
+ raise NotImplementedError("ID token validation not implemented")
@@ -0,0 +1,163 @@
1
+ """
2
+ Google OAuth Provider.
3
+
4
+ Implements OAuth 2.1 / OIDC for Google Sign-In.
5
+
6
+ Configuration:
7
+ 1. Create OAuth 2.0 credentials at https://console.cloud.google.com/apis/credentials
8
+ 2. Set authorized redirect URI: http://localhost:8000/api/auth/callback (dev)
9
+ 3. Enable Google+ API for userinfo access
10
+ 4. Set environment variables:
11
+ - AUTH__GOOGLE__CLIENT_ID
12
+ - AUTH__GOOGLE__CLIENT_SECRET
13
+ - AUTH__GOOGLE__REDIRECT_URI
14
+
15
+ Google-specific features:
16
+ - Hosted domain restriction (hd parameter for Google Workspace)
17
+ - Incremental authorization
18
+ - Offline access for refresh tokens
19
+
20
+ References:
21
+ - Google OAuth 2.0: https://developers.google.com/identity/protocols/oauth2
22
+ - Google OIDC: https://developers.google.com/identity/openid-connect/openid-connect
23
+ - Scopes: https://developers.google.com/identity/protocols/oauth2/scopes
24
+ """
25
+
26
+ from typing import Any
27
+
28
+ from .base import OAuthProvider, OAuthUserInfo
29
+
30
+
31
+ class GoogleOAuthProvider(OAuthProvider):
32
+ """
33
+ Google OAuth 2.1 / OIDC provider.
34
+
35
+ Uses Google's OIDC endpoints for authentication.
36
+ Supports both online (access token only) and offline (refresh token) access.
37
+ """
38
+
39
+ # Google OIDC discovery endpoint:
40
+ # https://accounts.google.com/.well-known/openid-configuration
41
+ AUTHORIZATION_ENDPOINT = "https://accounts.google.com/o/oauth2/v2/auth"
42
+ TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token"
43
+ USERINFO_ENDPOINT = "https://openidconnect.googleapis.com/v1/userinfo"
44
+ JWKS_URI = "https://www.googleapis.com/oauth2/v3/certs"
45
+
46
+ # Google OAuth scopes
47
+ # openid: Required for OIDC
48
+ # email: User email address
49
+ # profile: User profile information (name, picture)
50
+ DEFAULT_SCOPES = [
51
+ "openid",
52
+ "email",
53
+ "profile",
54
+ ]
55
+
56
+ @property
57
+ def authorization_endpoint(self) -> str:
58
+ """Google authorization endpoint."""
59
+ return self.AUTHORIZATION_ENDPOINT
60
+
61
+ @property
62
+ def token_endpoint(self) -> str:
63
+ """Google token endpoint."""
64
+ return self.TOKEN_ENDPOINT
65
+
66
+ @property
67
+ def userinfo_endpoint(self) -> str:
68
+ """Google userinfo endpoint."""
69
+ return self.USERINFO_ENDPOINT
70
+
71
+ @property
72
+ def jwks_uri(self) -> str:
73
+ """Google JWKS URI for token validation."""
74
+ return self.JWKS_URI
75
+
76
+ @property
77
+ def default_scopes(self) -> list[str]:
78
+ """Default scopes for Google OAuth."""
79
+ return self.DEFAULT_SCOPES.copy()
80
+
81
+ def normalize_user_info(self, claims: dict[str, Any]) -> OAuthUserInfo:
82
+ """
83
+ Normalize Google OIDC claims to OAuthUserInfo.
84
+
85
+ Google OIDC claims:
86
+ - sub: Unique user ID (stable identifier)
87
+ - email: User email address
88
+ - email_verified: Email verification status
89
+ - name: Full name
90
+ - given_name: First name
91
+ - family_name: Last name
92
+ - picture: Profile picture URL
93
+ - locale: User locale (e.g., "en")
94
+ - hd: Hosted domain (for Google Workspace accounts)
95
+
96
+ Args:
97
+ claims: Raw claims from ID token or userinfo endpoint
98
+
99
+ Returns:
100
+ Normalized user information
101
+ """
102
+ return OAuthUserInfo(
103
+ sub=claims["sub"],
104
+ email=claims.get("email"),
105
+ email_verified=claims.get("email_verified", False),
106
+ name=claims.get("name"),
107
+ given_name=claims.get("given_name"),
108
+ family_name=claims.get("family_name"),
109
+ picture=claims.get("picture"),
110
+ locale=claims.get("locale"),
111
+ provider="google",
112
+ raw_claims=claims,
113
+ )
114
+
115
+ def generate_auth_url_with_hosted_domain(
116
+ self,
117
+ state: str,
118
+ code_challenge: str,
119
+ hosted_domain: str | None = None,
120
+ access_type: str = "online",
121
+ scopes: list[str] | None = None,
122
+ nonce: str | None = None,
123
+ ) -> str:
124
+ """
125
+ Generate authorization URL with Google-specific parameters.
126
+
127
+ Args:
128
+ state: CSRF protection state
129
+ code_challenge: PKCE code challenge
130
+ hosted_domain: Restrict to Google Workspace domain (e.g., "example.com")
131
+ access_type: "online" (access token only) or "offline" (refresh token)
132
+ scopes: OAuth scopes (uses default_scopes if None)
133
+ nonce: OIDC nonce for ID token replay protection
134
+
135
+ Returns:
136
+ Authorization URL
137
+
138
+ Google-specific parameters:
139
+ - hd: Hosted domain restriction (Google Workspace only)
140
+ - access_type: online (default) or offline (for refresh tokens)
141
+ - prompt: consent (force consent screen), select_account (account picker)
142
+ - include_granted_scopes: true (incremental authorization)
143
+ """
144
+ extra_params: dict[str, str] = {
145
+ "access_type": access_type,
146
+ "include_granted_scopes": "true", # Incremental authorization
147
+ }
148
+
149
+ # Hosted domain restriction (Google Workspace)
150
+ if hosted_domain:
151
+ extra_params["hd"] = hosted_domain
152
+
153
+ # Force consent screen to get refresh token
154
+ if access_type == "offline":
155
+ extra_params["prompt"] = "consent"
156
+
157
+ return self.generate_auth_url(
158
+ state=state,
159
+ code_challenge=code_challenge,
160
+ scopes=scopes,
161
+ nonce=nonce,
162
+ extra_params=extra_params,
163
+ )