remdb 0.3.242__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.
- rem/__init__.py +129 -0
- rem/agentic/README.md +760 -0
- rem/agentic/__init__.py +54 -0
- rem/agentic/agents/README.md +155 -0
- rem/agentic/agents/__init__.py +38 -0
- rem/agentic/agents/agent_manager.py +311 -0
- rem/agentic/agents/sse_simulator.py +502 -0
- rem/agentic/context.py +425 -0
- rem/agentic/context_builder.py +360 -0
- rem/agentic/llm_provider_models.py +301 -0
- rem/agentic/mcp/__init__.py +0 -0
- rem/agentic/mcp/tool_wrapper.py +273 -0
- rem/agentic/otel/__init__.py +5 -0
- rem/agentic/otel/setup.py +240 -0
- rem/agentic/providers/phoenix.py +926 -0
- rem/agentic/providers/pydantic_ai.py +854 -0
- rem/agentic/query.py +117 -0
- rem/agentic/query_helper.py +89 -0
- rem/agentic/schema.py +737 -0
- rem/agentic/serialization.py +245 -0
- rem/agentic/tools/__init__.py +5 -0
- rem/agentic/tools/rem_tools.py +242 -0
- rem/api/README.md +657 -0
- rem/api/deps.py +253 -0
- rem/api/main.py +460 -0
- rem/api/mcp_router/prompts.py +182 -0
- rem/api/mcp_router/resources.py +820 -0
- rem/api/mcp_router/server.py +243 -0
- rem/api/mcp_router/tools.py +1605 -0
- rem/api/middleware/tracking.py +172 -0
- rem/api/routers/admin.py +520 -0
- rem/api/routers/auth.py +898 -0
- rem/api/routers/chat/__init__.py +5 -0
- rem/api/routers/chat/child_streaming.py +394 -0
- rem/api/routers/chat/completions.py +702 -0
- rem/api/routers/chat/json_utils.py +76 -0
- rem/api/routers/chat/models.py +202 -0
- rem/api/routers/chat/otel_utils.py +33 -0
- rem/api/routers/chat/sse_events.py +546 -0
- rem/api/routers/chat/streaming.py +950 -0
- rem/api/routers/chat/streaming_utils.py +327 -0
- rem/api/routers/common.py +18 -0
- rem/api/routers/dev.py +87 -0
- rem/api/routers/feedback.py +276 -0
- rem/api/routers/messages.py +620 -0
- rem/api/routers/models.py +86 -0
- rem/api/routers/query.py +362 -0
- rem/api/routers/shared_sessions.py +422 -0
- rem/auth/README.md +258 -0
- rem/auth/__init__.py +36 -0
- rem/auth/jwt.py +367 -0
- rem/auth/middleware.py +318 -0
- rem/auth/providers/__init__.py +16 -0
- rem/auth/providers/base.py +376 -0
- rem/auth/providers/email.py +215 -0
- rem/auth/providers/google.py +163 -0
- rem/auth/providers/microsoft.py +237 -0
- rem/cli/README.md +517 -0
- rem/cli/__init__.py +8 -0
- rem/cli/commands/README.md +299 -0
- rem/cli/commands/__init__.py +3 -0
- rem/cli/commands/ask.py +549 -0
- rem/cli/commands/cluster.py +1808 -0
- rem/cli/commands/configure.py +495 -0
- rem/cli/commands/db.py +828 -0
- rem/cli/commands/dreaming.py +324 -0
- rem/cli/commands/experiments.py +1698 -0
- rem/cli/commands/mcp.py +66 -0
- rem/cli/commands/process.py +388 -0
- rem/cli/commands/query.py +109 -0
- rem/cli/commands/scaffold.py +47 -0
- rem/cli/commands/schema.py +230 -0
- rem/cli/commands/serve.py +106 -0
- rem/cli/commands/session.py +453 -0
- rem/cli/dreaming.py +363 -0
- rem/cli/main.py +123 -0
- rem/config.py +244 -0
- rem/mcp_server.py +41 -0
- rem/models/core/__init__.py +49 -0
- rem/models/core/core_model.py +70 -0
- rem/models/core/engram.py +333 -0
- rem/models/core/experiment.py +672 -0
- rem/models/core/inline_edge.py +132 -0
- rem/models/core/rem_query.py +246 -0
- rem/models/entities/__init__.py +68 -0
- rem/models/entities/domain_resource.py +38 -0
- rem/models/entities/feedback.py +123 -0
- rem/models/entities/file.py +57 -0
- rem/models/entities/image_resource.py +88 -0
- rem/models/entities/message.py +64 -0
- rem/models/entities/moment.py +123 -0
- rem/models/entities/ontology.py +181 -0
- rem/models/entities/ontology_config.py +131 -0
- rem/models/entities/resource.py +95 -0
- rem/models/entities/schema.py +87 -0
- rem/models/entities/session.py +84 -0
- rem/models/entities/shared_session.py +180 -0
- rem/models/entities/subscriber.py +175 -0
- rem/models/entities/user.py +93 -0
- rem/py.typed +0 -0
- rem/registry.py +373 -0
- rem/schemas/README.md +507 -0
- rem/schemas/__init__.py +6 -0
- rem/schemas/agents/README.md +92 -0
- rem/schemas/agents/core/agent-builder.yaml +235 -0
- rem/schemas/agents/core/moment-builder.yaml +178 -0
- rem/schemas/agents/core/rem-query-agent.yaml +226 -0
- rem/schemas/agents/core/resource-affinity-assessor.yaml +99 -0
- rem/schemas/agents/core/simple-assistant.yaml +19 -0
- rem/schemas/agents/core/user-profile-builder.yaml +163 -0
- rem/schemas/agents/examples/contract-analyzer.yaml +317 -0
- rem/schemas/agents/examples/contract-extractor.yaml +134 -0
- rem/schemas/agents/examples/cv-parser.yaml +263 -0
- rem/schemas/agents/examples/hello-world.yaml +37 -0
- rem/schemas/agents/examples/query.yaml +54 -0
- rem/schemas/agents/examples/simple.yaml +21 -0
- rem/schemas/agents/examples/test.yaml +29 -0
- rem/schemas/agents/rem.yaml +132 -0
- rem/schemas/evaluators/hello-world/default.yaml +77 -0
- rem/schemas/evaluators/rem/faithfulness.yaml +219 -0
- rem/schemas/evaluators/rem/lookup-correctness.yaml +182 -0
- rem/schemas/evaluators/rem/retrieval-precision.yaml +199 -0
- rem/schemas/evaluators/rem/retrieval-recall.yaml +211 -0
- rem/schemas/evaluators/rem/search-correctness.yaml +192 -0
- rem/services/__init__.py +18 -0
- rem/services/audio/INTEGRATION.md +308 -0
- rem/services/audio/README.md +376 -0
- rem/services/audio/__init__.py +15 -0
- rem/services/audio/chunker.py +354 -0
- rem/services/audio/transcriber.py +259 -0
- rem/services/content/README.md +1269 -0
- rem/services/content/__init__.py +5 -0
- rem/services/content/providers.py +760 -0
- rem/services/content/service.py +762 -0
- rem/services/dreaming/README.md +230 -0
- rem/services/dreaming/__init__.py +53 -0
- rem/services/dreaming/affinity_service.py +322 -0
- rem/services/dreaming/moment_service.py +251 -0
- rem/services/dreaming/ontology_service.py +54 -0
- rem/services/dreaming/user_model_service.py +297 -0
- rem/services/dreaming/utils.py +39 -0
- rem/services/email/__init__.py +10 -0
- rem/services/email/service.py +522 -0
- rem/services/email/templates.py +360 -0
- rem/services/embeddings/__init__.py +11 -0
- rem/services/embeddings/api.py +127 -0
- rem/services/embeddings/worker.py +435 -0
- rem/services/fs/README.md +662 -0
- rem/services/fs/__init__.py +62 -0
- rem/services/fs/examples.py +206 -0
- rem/services/fs/examples_paths.py +204 -0
- rem/services/fs/git_provider.py +935 -0
- rem/services/fs/local_provider.py +760 -0
- rem/services/fs/parsing-hooks-examples.md +172 -0
- rem/services/fs/paths.py +276 -0
- rem/services/fs/provider.py +460 -0
- rem/services/fs/s3_provider.py +1042 -0
- rem/services/fs/service.py +186 -0
- rem/services/git/README.md +1075 -0
- rem/services/git/__init__.py +17 -0
- rem/services/git/service.py +469 -0
- rem/services/phoenix/EXPERIMENT_DESIGN.md +1146 -0
- rem/services/phoenix/README.md +453 -0
- rem/services/phoenix/__init__.py +46 -0
- rem/services/phoenix/client.py +960 -0
- rem/services/phoenix/config.py +88 -0
- rem/services/phoenix/prompt_labels.py +477 -0
- rem/services/postgres/README.md +757 -0
- rem/services/postgres/__init__.py +49 -0
- rem/services/postgres/diff_service.py +599 -0
- rem/services/postgres/migration_service.py +427 -0
- rem/services/postgres/programmable_diff_service.py +635 -0
- rem/services/postgres/pydantic_to_sqlalchemy.py +562 -0
- rem/services/postgres/register_type.py +353 -0
- rem/services/postgres/repository.py +481 -0
- rem/services/postgres/schema_generator.py +661 -0
- rem/services/postgres/service.py +802 -0
- rem/services/postgres/sql_builder.py +355 -0
- rem/services/rate_limit.py +113 -0
- rem/services/rem/README.md +318 -0
- rem/services/rem/__init__.py +23 -0
- rem/services/rem/exceptions.py +71 -0
- rem/services/rem/executor.py +293 -0
- rem/services/rem/parser.py +180 -0
- rem/services/rem/queries.py +196 -0
- rem/services/rem/query.py +371 -0
- rem/services/rem/service.py +608 -0
- rem/services/session/README.md +374 -0
- rem/services/session/__init__.py +13 -0
- rem/services/session/compression.py +488 -0
- rem/services/session/pydantic_messages.py +310 -0
- rem/services/session/reload.py +85 -0
- rem/services/user_service.py +130 -0
- rem/settings.py +1877 -0
- rem/sql/background_indexes.sql +52 -0
- rem/sql/migrations/001_install.sql +983 -0
- rem/sql/migrations/002_install_models.sql +3157 -0
- rem/sql/migrations/003_optional_extensions.sql +326 -0
- rem/sql/migrations/004_cache_system.sql +282 -0
- rem/sql/migrations/005_schema_update.sql +145 -0
- rem/sql/migrations/migrate_session_id_to_uuid.sql +45 -0
- rem/utils/AGENTIC_CHUNKING.md +597 -0
- rem/utils/README.md +628 -0
- rem/utils/__init__.py +61 -0
- rem/utils/agentic_chunking.py +622 -0
- rem/utils/batch_ops.py +343 -0
- rem/utils/chunking.py +108 -0
- rem/utils/clip_embeddings.py +276 -0
- rem/utils/constants.py +97 -0
- rem/utils/date_utils.py +228 -0
- rem/utils/dict_utils.py +98 -0
- rem/utils/embeddings.py +436 -0
- rem/utils/examples/embeddings_example.py +305 -0
- rem/utils/examples/sql_types_example.py +202 -0
- rem/utils/files.py +323 -0
- rem/utils/markdown.py +16 -0
- rem/utils/mime_types.py +158 -0
- rem/utils/model_helpers.py +492 -0
- rem/utils/schema_loader.py +649 -0
- rem/utils/sql_paths.py +146 -0
- rem/utils/sql_types.py +350 -0
- rem/utils/user_id.py +81 -0
- rem/utils/vision.py +325 -0
- rem/workers/README.md +506 -0
- rem/workers/__init__.py +7 -0
- rem/workers/db_listener.py +579 -0
- rem/workers/db_maintainer.py +74 -0
- rem/workers/dreaming.py +502 -0
- rem/workers/engram_processor.py +312 -0
- rem/workers/sqs_file_processor.py +193 -0
- rem/workers/unlogged_maintainer.py +463 -0
- remdb-0.3.242.dist-info/METADATA +1632 -0
- remdb-0.3.242.dist-info/RECORD +235 -0
- remdb-0.3.242.dist-info/WHEEL +4 -0
- remdb-0.3.242.dist-info/entry_points.txt +2 -0
|
@@ -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
|
+
)
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Microsoft Entra ID (Azure AD) OAuth Provider.
|
|
3
|
+
|
|
4
|
+
Implements OAuth 2.1 / OIDC for Microsoft authentication.
|
|
5
|
+
|
|
6
|
+
Configuration:
|
|
7
|
+
1. Register application at https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps
|
|
8
|
+
2. Create client secret under "Certificates & secrets"
|
|
9
|
+
3. Add redirect URI: http://localhost:8000/api/auth/callback (dev)
|
|
10
|
+
4. Set API permissions:
|
|
11
|
+
- Microsoft Graph: User.Read (delegated)
|
|
12
|
+
- Optional: email, profile, openid (automatically included)
|
|
13
|
+
5. Set environment variables:
|
|
14
|
+
- AUTH__MICROSOFT__CLIENT_ID (Application ID)
|
|
15
|
+
- AUTH__MICROSOFT__CLIENT_SECRET
|
|
16
|
+
- AUTH__MICROSOFT__TENANT_ID (or "common" for multi-tenant)
|
|
17
|
+
- AUTH__MICROSOFT__REDIRECT_URI
|
|
18
|
+
|
|
19
|
+
Microsoft-specific features:
|
|
20
|
+
- Multi-tenant support (common, organizations, consumers)
|
|
21
|
+
- Azure AD B2C support
|
|
22
|
+
- Conditional access policies
|
|
23
|
+
- Token caching with MSAL
|
|
24
|
+
|
|
25
|
+
Tenant options:
|
|
26
|
+
- common: Multi-tenant + personal Microsoft accounts
|
|
27
|
+
- organizations: Multi-tenant (work/school only)
|
|
28
|
+
- consumers: Personal Microsoft accounts only
|
|
29
|
+
- {tenant-id}: Single tenant (specific organization)
|
|
30
|
+
|
|
31
|
+
References:
|
|
32
|
+
- Microsoft identity platform: https://learn.microsoft.com/en-us/entra/identity-platform/
|
|
33
|
+
- OAuth 2.0 flow: https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow
|
|
34
|
+
- OIDC: https://learn.microsoft.com/en-us/entra/identity-platform/v2-protocols-oidc
|
|
35
|
+
- Scopes: https://learn.microsoft.com/en-us/graph/permissions-reference
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
from typing import Any
|
|
39
|
+
|
|
40
|
+
from .base import OAuthProvider, OAuthUserInfo
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class MicrosoftOAuthProvider(OAuthProvider):
|
|
44
|
+
"""
|
|
45
|
+
Microsoft Entra ID (Azure AD) OAuth 2.1 / OIDC provider.
|
|
46
|
+
|
|
47
|
+
Supports multi-tenant authentication and Microsoft Graph API access.
|
|
48
|
+
Uses Microsoft identity platform v2.0 endpoints.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
# Microsoft identity platform v2.0 endpoints
|
|
52
|
+
# Replace {tenant} with:
|
|
53
|
+
# - "common" for multi-tenant + personal accounts
|
|
54
|
+
# - "organizations" for work/school accounts only
|
|
55
|
+
# - "consumers" for personal Microsoft accounts only
|
|
56
|
+
# - Tenant ID/domain for single-tenant
|
|
57
|
+
AUTHORIZATION_ENDPOINT_TEMPLATE = (
|
|
58
|
+
"https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize"
|
|
59
|
+
)
|
|
60
|
+
TOKEN_ENDPOINT_TEMPLATE = "https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token"
|
|
61
|
+
USERINFO_ENDPOINT = "https://graph.microsoft.com/v1.0/me"
|
|
62
|
+
JWKS_URI_TEMPLATE = "https://login.microsoftonline.com/{tenant}/discovery/v2.0/keys"
|
|
63
|
+
|
|
64
|
+
# Microsoft Graph scopes
|
|
65
|
+
# openid: Required for OIDC
|
|
66
|
+
# email: User email address
|
|
67
|
+
# profile: User profile information
|
|
68
|
+
# User.Read: Read user profile via Microsoft Graph
|
|
69
|
+
# offline_access: Request refresh token
|
|
70
|
+
DEFAULT_SCOPES = [
|
|
71
|
+
"openid",
|
|
72
|
+
"email",
|
|
73
|
+
"profile",
|
|
74
|
+
"User.Read", # Microsoft Graph: read user profile
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
def __init__(
|
|
78
|
+
self,
|
|
79
|
+
client_id: str,
|
|
80
|
+
client_secret: str,
|
|
81
|
+
redirect_uri: str,
|
|
82
|
+
tenant: str = "common",
|
|
83
|
+
):
|
|
84
|
+
"""
|
|
85
|
+
Initialize Microsoft OAuth provider.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
client_id: Application (client) ID from Azure portal
|
|
89
|
+
client_secret: Client secret from Azure portal
|
|
90
|
+
redirect_uri: Redirect URI registered in Azure portal
|
|
91
|
+
tenant: Tenant ID or "common"/"organizations"/"consumers"
|
|
92
|
+
"""
|
|
93
|
+
super().__init__(client_id, client_secret, redirect_uri)
|
|
94
|
+
self.tenant = tenant
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def authorization_endpoint(self) -> str:
|
|
98
|
+
"""Microsoft authorization endpoint."""
|
|
99
|
+
return self.AUTHORIZATION_ENDPOINT_TEMPLATE.format(tenant=self.tenant)
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def token_endpoint(self) -> str:
|
|
103
|
+
"""Microsoft token endpoint."""
|
|
104
|
+
return self.TOKEN_ENDPOINT_TEMPLATE.format(tenant=self.tenant)
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def userinfo_endpoint(self) -> str:
|
|
108
|
+
"""Microsoft Graph /me endpoint for user info."""
|
|
109
|
+
return self.USERINFO_ENDPOINT
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def jwks_uri(self) -> str:
|
|
113
|
+
"""Microsoft JWKS URI for token validation."""
|
|
114
|
+
return self.JWKS_URI_TEMPLATE.format(tenant=self.tenant)
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def default_scopes(self) -> list[str]:
|
|
118
|
+
"""Default scopes for Microsoft OAuth."""
|
|
119
|
+
return self.DEFAULT_SCOPES.copy()
|
|
120
|
+
|
|
121
|
+
def normalize_user_info(self, claims: dict[str, Any]) -> OAuthUserInfo:
|
|
122
|
+
"""
|
|
123
|
+
Normalize Microsoft claims to OAuthUserInfo.
|
|
124
|
+
|
|
125
|
+
Microsoft Graph /me response:
|
|
126
|
+
- id: Unique user ID (stable identifier)
|
|
127
|
+
- userPrincipalName: User principal name (UPN)
|
|
128
|
+
- mail: Primary email (may be null)
|
|
129
|
+
- displayName: Display name
|
|
130
|
+
- givenName: First name
|
|
131
|
+
- surname: Last name
|
|
132
|
+
- preferredLanguage: User locale
|
|
133
|
+
|
|
134
|
+
Microsoft ID token claims:
|
|
135
|
+
- sub: Subject (unique user ID, different from Graph ID)
|
|
136
|
+
- email: User email
|
|
137
|
+
- name: Full name
|
|
138
|
+
- given_name: First name
|
|
139
|
+
- family_name: Last name
|
|
140
|
+
- preferred_username: UPN or email
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
claims: Raw claims from ID token or Microsoft Graph /me
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
Normalized user information
|
|
147
|
+
"""
|
|
148
|
+
# Handle both ID token claims and Graph API response
|
|
149
|
+
# Graph API uses different field names than OIDC claims
|
|
150
|
+
if "id" in claims:
|
|
151
|
+
# Microsoft Graph /me response
|
|
152
|
+
sub = claims["id"]
|
|
153
|
+
email = claims.get("mail") or claims.get("userPrincipalName")
|
|
154
|
+
name = claims.get("displayName")
|
|
155
|
+
given_name = claims.get("givenName")
|
|
156
|
+
family_name = claims.get("surname")
|
|
157
|
+
locale = claims.get("preferredLanguage")
|
|
158
|
+
else:
|
|
159
|
+
# OIDC ID token claims
|
|
160
|
+
sub = claims["sub"]
|
|
161
|
+
email = claims.get("email") or claims.get("preferred_username")
|
|
162
|
+
name = claims.get("name")
|
|
163
|
+
given_name = claims.get("given_name")
|
|
164
|
+
family_name = claims.get("family_name")
|
|
165
|
+
locale = claims.get("locale")
|
|
166
|
+
|
|
167
|
+
return OAuthUserInfo(
|
|
168
|
+
sub=sub,
|
|
169
|
+
email=email,
|
|
170
|
+
email_verified=True, # Microsoft verifies emails during account creation
|
|
171
|
+
name=name,
|
|
172
|
+
given_name=given_name,
|
|
173
|
+
family_name=family_name,
|
|
174
|
+
picture=None, # Microsoft Graph requires separate photo endpoint
|
|
175
|
+
locale=locale,
|
|
176
|
+
provider="microsoft",
|
|
177
|
+
raw_claims=claims,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
def generate_auth_url_with_prompt(
|
|
181
|
+
self,
|
|
182
|
+
state: str,
|
|
183
|
+
code_challenge: str,
|
|
184
|
+
prompt: str | None = None,
|
|
185
|
+
domain_hint: str | None = None,
|
|
186
|
+
login_hint: str | None = None,
|
|
187
|
+
scopes: list[str] | None = None,
|
|
188
|
+
nonce: str | None = None,
|
|
189
|
+
) -> str:
|
|
190
|
+
"""
|
|
191
|
+
Generate authorization URL with Microsoft-specific parameters.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
state: CSRF protection state
|
|
195
|
+
code_challenge: PKCE code challenge
|
|
196
|
+
prompt: Authentication behavior (none, login, consent, select_account)
|
|
197
|
+
domain_hint: Domain hint for faster login (e.g., "contoso.com")
|
|
198
|
+
login_hint: Login hint (email) to pre-fill sign-in form
|
|
199
|
+
scopes: OAuth scopes (uses default_scopes if None)
|
|
200
|
+
nonce: OIDC nonce for ID token replay protection
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
Authorization URL
|
|
204
|
+
|
|
205
|
+
Microsoft-specific parameters:
|
|
206
|
+
- prompt: Authentication behavior
|
|
207
|
+
- none: Silent authentication (fails if interaction required)
|
|
208
|
+
- login: Force user to re-authenticate
|
|
209
|
+
- consent: Force consent screen
|
|
210
|
+
- select_account: Show account picker
|
|
211
|
+
- domain_hint: Domain hint for faster login (skip domain discovery)
|
|
212
|
+
- login_hint: Email to pre-fill sign-in form
|
|
213
|
+
- response_mode: query (default), form_post, fragment
|
|
214
|
+
"""
|
|
215
|
+
extra_params: dict[str, str] = {}
|
|
216
|
+
|
|
217
|
+
if prompt:
|
|
218
|
+
extra_params["prompt"] = prompt
|
|
219
|
+
|
|
220
|
+
if domain_hint:
|
|
221
|
+
extra_params["domain_hint"] = domain_hint
|
|
222
|
+
|
|
223
|
+
if login_hint:
|
|
224
|
+
extra_params["login_hint"] = login_hint
|
|
225
|
+
|
|
226
|
+
# Add offline_access scope for refresh token
|
|
227
|
+
scopes_with_offline = scopes or self.default_scopes.copy()
|
|
228
|
+
if "offline_access" not in scopes_with_offline:
|
|
229
|
+
scopes_with_offline.append("offline_access")
|
|
230
|
+
|
|
231
|
+
return self.generate_auth_url(
|
|
232
|
+
state=state,
|
|
233
|
+
code_challenge=code_challenge,
|
|
234
|
+
scopes=scopes_with_offline,
|
|
235
|
+
nonce=nonce,
|
|
236
|
+
extra_params=extra_params,
|
|
237
|
+
)
|