diff --git a/.gitignore b/.gitignore index d98402c23..984909b26 100644 --- a/.gitignore +++ b/.gitignore @@ -10,5 +10,9 @@ target .idea .nova +# Local config (may contain secrets) +config.yaml +config.local.yaml + # OS garbage .DS_Store diff --git a/mock-google-oidc/.gitignore b/mock-google-oidc/.gitignore deleted file mode 100644 index 504afef81..000000000 --- a/mock-google-oidc/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -node_modules/ -package-lock.json diff --git a/mock-google-oidc/README.md b/mock-google-oidc/README.md deleted file mode 100644 index ad00cc2a6..000000000 --- a/mock-google-oidc/README.md +++ /dev/null @@ -1,73 +0,0 @@ -# Mock Google OIDC Provider - -A lightweight mock OpenID Connect provider that simulates Google login for local MAS development. It lets you test the upstream OAuth2 / "Sign in with Google" flow without needing real Google credentials. - -## Prerequisites - -- Node.js 18+ -- MAS configured with an upstream OAuth2 provider pointing to this mock - -## Quick start - -```sh -cd mock-google-oidc -npm install -npm start -``` - -The server starts at `http://localhost:5556`. - -## How it works - -The mock uses [`oidc-provider`](https://github.com/panva/node-oidc-provider) to run a standards-compliant OIDC server with dev interactions enabled. When MAS redirects to the mock for login, you'll see a simple form where you can enter any email-like value (e.g. `taylor@gmail.com`). The mock will return that as the authenticated user with synthetic profile claims. - -## MAS configuration - -Add an upstream OAuth2 provider entry in your MAS `config.yaml`: - -```yaml -upstream_oauth2: - providers: - - id: "01JQ0FAKEG00G1E0D1CPR0V1D3" - human_name: "Google" - issuer: "http://localhost:5556" - client_id: "mas-dev" - client_secret: "mas-dev-secret" - token_endpoint_auth_method: "client_secret_post" - scope: "openid email profile" - claims_imports: - localpart: - action: suggest - template: "{{ user.preferred_username }}" - displayname: - action: suggest - template: "{{ user.name }}" - email: - action: suggest - template: "{{ user.email }}" - set_email_verification: always -``` - -## Environment variables - -| Variable | Default | Description | -|----------|---------|-------------| -| `MOCK_OIDC_ISSUER` | `http://localhost:5556` | The issuer URL | -| `MAS_PROVIDER_ID` | `01JQ0FAKEG00G1E0D1CPR0V1D3` | The upstream provider ID configured in MAS | -| `MOCK_OIDC_CLIENT_ID` | `mas-dev` | OAuth2 client ID | -| `MOCK_OIDC_CLIENT_SECRET` | `mas-dev-secret` | OAuth2 client secret | - -## Running with the full dev stack - -```sh -# Terminal 1: Mock Synapse (handles homeserver API calls) -python3 mock_synapse.py - -# Terminal 2: Mock Google OIDC -cd mock-google-oidc && npm start - -# Terminal 3: MAS -cargo run -- server -c config.yaml -``` - -Then visit `http://[::]:8080/login` and click "Sign in with Google". diff --git a/mock-google-oidc/package.json b/mock-google-oidc/package.json deleted file mode 100644 index a17d78baa..000000000 --- a/mock-google-oidc/package.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "mock-google-oidc", - "private": true, - "type": "module", - "scripts": { - "start": "node server.mjs" - }, - "dependencies": { - "jose": "^6.0.11", - "oidc-provider": "^9.5.1" - } -} diff --git a/mock-google-oidc/server.mjs b/mock-google-oidc/server.mjs deleted file mode 100644 index a8b8efeca..000000000 --- a/mock-google-oidc/server.mjs +++ /dev/null @@ -1,77 +0,0 @@ -import { exportJWK, generateKeyPair } from 'jose'; -import { Provider } from 'oidc-provider'; - -const ISSUER = process.env.MOCK_OIDC_ISSUER ?? 'http://localhost:5556'; -const PROVIDER_ID = - process.env.MAS_PROVIDER_ID ?? '01JQ0FAKEG00G1E0D1CPR0V1D3'; -const CLIENT_ID = process.env.MOCK_OIDC_CLIENT_ID ?? 'mas-dev'; -const CLIENT_SECRET = - process.env.MOCK_OIDC_CLIENT_SECRET ?? 'mas-dev-secret'; - -const redirectUris = [ - `http://localhost:8080/upstream/callback/${PROVIDER_ID}`, - `http://127.0.0.1:8080/upstream/callback/${PROVIDER_ID}`, - `http://[::]:8080/upstream/callback/${PROVIDER_ID}`, -]; - -const { privateKey } = await generateKeyPair('RS256', { extractable: true }); -const jwk = await exportJWK(privateKey); -jwk.use = 'sig'; -jwk.alg = 'RS256'; -jwk.kid = 'mock-google-rs256'; - -const configuration = { - clients: [ - { - client_id: CLIENT_ID, - client_secret: CLIENT_SECRET, - redirect_uris: redirectUris, - response_types: ['code'], - grant_types: ['authorization_code'], - token_endpoint_auth_method: 'client_secret_post', - }, - ], - jwks: { keys: [jwk] }, - claims: { - openid: ['sub'], - email: ['email', 'email_verified'], - profile: ['name', 'preferred_username', 'picture'], - }, - cookies: { - keys: ['mock-google-oidc-dev-key-1', 'mock-google-oidc-dev-key-2'], - }, - features: { - devInteractions: { enabled: true }, - rpInitiatedLogout: { enabled: false }, - }, - async findAccount(_ctx, sub) { - const email = sub.includes('@') ? sub : `${sub}@gmail.com`; - const preferredUsername = email.split('@')[0]; - - return { - accountId: sub, - async claims(_use, _scope) { - return { - sub, - email, - email_verified: true, - name: 'Taylor Google User', - preferred_username: preferredUsername, - picture: - 'https://www.gstatic.com/images/branding/product/1x/avatar_circle_blue_512dp.png', - }; - }, - }; - }, -}; - -const provider = new Provider(ISSUER, configuration); - -provider.listen(5556, () => { - console.log(`Mock Google OIDC running at ${ISSUER}`); - console.log('Configured redirect URIs:'); - for (const uri of redirectUris) { - console.log(` - ${uri}`); - } - console.log('Use any email-like value in the dev interaction login form.'); -}); diff --git a/mock_synapse.py b/mock_synapse.py deleted file mode 100644 index d0bc9c518..000000000 --- a/mock_synapse.py +++ /dev/null @@ -1,266 +0,0 @@ -#!/usr/bin/env python3 -""" -Mock Synapse server for testing MAS UI without a real homeserver. - -Handles both Modern (_synapse/mas/...) and Legacy (_synapse/admin/...) -API endpoints that MAS calls on Synapse. - -Usage: - python3 mock_synapse.py - -Then start MAS normally: - cargo run -- server -c config.yaml -""" - -from http.server import HTTPServer, BaseHTTPRequestHandler -from urllib.parse import urlparse, parse_qs -import json -import sys - - -# In-memory store of provisioned users: localpart -> user data -provisioned_users = {} - - -class MockSynapseHandler(BaseHTTPRequestHandler): - - def _send_json(self, data, status=200): - self.send_response(status) - self.send_header("Content-Type", "application/json") - self.end_headers() - self.wfile.write(json.dumps(data).encode()) - - def _read_body(self): - length = int(self.headers.get("Content-Length", 0)) - if length: - return json.loads(self.rfile.read(length)) - return {} - - # ── GET endpoints ────────────────────────────────────────────── - - def do_GET(self): - parsed = urlparse(self.path) - path = parsed.path.rstrip("/") - qs = parse_qs(parsed.query) - - # Modern: check if localpart is available - if path == "/_synapse/mas/is_localpart_available": - localpart = qs.get("localpart", [""])[0] - if localpart in provisioned_users: - print(f" [mock] is_localpart_available: {localpart} → in use") - self._send_json({"errcode": "M_USER_IN_USE", "error": "User ID already taken"}, 409) - else: - print(f" [mock] is_localpart_available: {localpart} → available") - self._send_json({}) # 200 = available - return - - # Modern: query user info - if path == "/_synapse/mas/query_user": - localpart = qs.get("localpart", [""])[0] - if localpart in provisioned_users: - user = provisioned_users[localpart] - print(f" [mock] query_user: {localpart} → found") - self._send_json({ - "user_id": f"@{localpart}:localhost:8008", - "display_name": user.get("display_name"), - "avatar_url": user.get("avatar_url"), - "is_suspended": False, - "is_deactivated": False, - }) - else: - print(f" [mock] query_user: {localpart} → not found (404)") - self._send_json({"errcode": "M_NOT_FOUND", "error": "User not found"}, 404) - return - - # Legacy: username available - if path == "/_synapse/admin/v1/username_available": - username = qs.get("username", [""])[0] - print(f" [mock] username_available: {username} → available=true") - self._send_json({"available": True}) - return - - # Legacy: get user info - if "/_synapse/admin/v2/users/" in path and "/devices" not in path: - print(f" [mock] get user → not found (404)") - self._send_json({"errcode": "M_NOT_FOUND", "error": "User not found"}, 404) - return - - # Legacy: list devices - if "/_synapse/admin/v2/users/" in path and path.endswith("/devices"): - print(f" [mock] list devices → empty") - self._send_json({"devices": []}) - return - - # Matrix client versions - if path == "/_matrix/client/versions": - self._send_json({"versions": ["v1.1", "v1.2", "v1.3"]}) - return - - # Catch-all - print(f" [mock] Unhandled GET: {self.path}") - self._send_json({}) - - # ── POST endpoints ───────────────────────────────────────────── - - def do_POST(self): - parsed = urlparse(self.path) - path = parsed.path.rstrip("/") - body = self._read_body() - - # Modern: provision user - if path == "/_synapse/mas/provision_user": - localpart = body.get("localpart", "?") - is_new = localpart not in provisioned_users - provisioned_users[localpart] = { - "display_name": body.get("set_displayname"), - "avatar_url": body.get("set_avatar_url"), - "emails": body.get("set_emails", []), - } - status = 201 if is_new else 200 - print(f" [mock] provision_user: {localpart} → {status} {'created' if is_new else 'updated'}") - self._send_json({}, status) - return - - # Modern: upsert device - if path == "/_synapse/mas/upsert_device": - print(f" [mock] upsert_device: {body.get('device_id', '?')} → ok") - self._send_json({}) - return - - # Modern: update device display name - if path == "/_synapse/mas/update_device_display_name": - print(f" [mock] update_device_display_name → ok") - self._send_json({}) - return - - # Modern: delete device - if path == "/_synapse/mas/delete_device": - print(f" [mock] delete_device → ok") - self._send_json({}) - return - - # Modern: sync devices - if path == "/_synapse/mas/sync_devices": - print(f" [mock] sync_devices → ok") - self._send_json({}) - return - - # Modern: delete user - if path == "/_synapse/mas/delete_user": - print(f" [mock] delete_user → ok") - self._send_json({}) - return - - # Modern: reactivate user - if path == "/_synapse/mas/reactivate_user": - print(f" [mock] reactivate_user → ok") - self._send_json({}) - return - - # Modern: set displayname - if path == "/_synapse/mas/set_displayname": - localpart = body.get("localpart", "?") - if localpart in provisioned_users: - provisioned_users[localpart]["display_name"] = body.get("displayname") - print(f" [mock] set_displayname: {localpart} → ok") - self._send_json({}) - return - - # Modern: unset displayname - if path == "/_synapse/mas/unset_displayname": - localpart = body.get("localpart", "?") - if localpart in provisioned_users: - provisioned_users[localpart]["display_name"] = None - print(f" [mock] unset_displayname: {localpart} → ok") - self._send_json({}) - return - - # Modern: allow cross-signing reset - if path == "/_synapse/mas/allow_cross_signing_reset": - print(f" [mock] allow_cross_signing_reset → ok") - self._send_json({}) - return - - # Legacy: create device - if "/_synapse/admin/v2/users/" in path and path.endswith("/devices"): - print(f" [mock] legacy create device → 201") - self._send_json({}, 201) - return - - # Legacy: bulk delete devices - if "/_synapse/admin/v2/users/" in path and "delete_devices" in path: - print(f" [mock] legacy bulk delete devices → ok") - self._send_json({}) - return - - # Legacy: deactivate user - if "/_synapse/admin/v1/deactivate/" in path: - print(f" [mock] legacy deactivate → ok") - self._send_json({}) - return - - # Legacy: allow cross-signing reset - if "_allow_cross_signing_replacement_without_uia" in path: - print(f" [mock] legacy allow cross-signing reset → ok") - self._send_json({}) - return - - # Catch-all - print(f" [mock] Unhandled POST: {path} body={body}") - self._send_json({}) - - # ── PUT endpoints ────────────────────────────────────────────── - - def do_PUT(self): - parsed = urlparse(self.path) - path = parsed.path.rstrip("/") - body = self._read_body() - - # Legacy: create/update user - if "/_synapse/admin/v2/users/" in path and "/devices/" not in path: - print(f" [mock] legacy put user → 201 created") - self._send_json({}, 201) - return - - # Legacy: update device display name - if "/_synapse/admin/v2/users/" in path and "/devices/" in path: - print(f" [mock] legacy update device display name → ok") - self._send_json({}) - return - - # Client API: set displayname - if "/_matrix/client/v3/profile/" in path and "/displayname" in path: - print(f" [mock] set profile displayname → ok") - self._send_json({}) - return - - # Catch-all - print(f" [mock] Unhandled PUT: {path} body={body}") - self._send_json({}) - - # ── DELETE endpoints ─────────────────────────────────────────── - - def do_DELETE(self): - parsed = urlparse(self.path) - path = parsed.path.rstrip("/") - - # Legacy: delete device - if "/_synapse/admin/v2/users/" in path and "/devices/" in path: - print(f" [mock] legacy delete device → ok") - self._send_json({}) - return - - # Catch-all - print(f" [mock] Unhandled DELETE: {path}") - self._send_json({}) - - -if __name__ == "__main__": - port = int(sys.argv[1]) if len(sys.argv) > 1 else 8008 - server = HTTPServer(("localhost", port), MockSynapseHandler) - print(f"Mock Synapse running on http://localhost:{port}") - print("Press Ctrl+C to stop\n") - try: - server.serve_forever() - except KeyboardInterrupt: - print("\nStopped.") diff --git a/mocks/mock-google-oidc/README.md b/mocks/mock-google-oidc/README.md index ad00cc2a6..dc5b23b1c 100644 --- a/mocks/mock-google-oidc/README.md +++ b/mocks/mock-google-oidc/README.md @@ -10,7 +10,7 @@ A lightweight mock OpenID Connect provider that simulates Google login for local ## Quick start ```sh -cd mock-google-oidc +cd mocks/mock-google-oidc npm install npm start ``` @@ -61,10 +61,10 @@ upstream_oauth2: ```sh # Terminal 1: Mock Synapse (handles homeserver API calls) -python3 mock_synapse.py +python3 mocks/mock_synapse.py # Terminal 2: Mock Google OIDC -cd mock-google-oidc && npm start +cd mocks/mock-google-oidc && npm start # Terminal 3: MAS cargo run -- server -c config.yaml diff --git a/mocks/mock_synapse.py b/mocks/mock_synapse.py index d0bc9c518..37763f4a0 100644 --- a/mocks/mock_synapse.py +++ b/mocks/mock_synapse.py @@ -6,7 +6,7 @@ Handles both Modern (_synapse/mas/...) and Legacy (_synapse/admin/...) API endpoints that MAS calls on Synapse. Usage: - python3 mock_synapse.py + python3 mocks/mock_synapse.py Then start MAS normally: cargo run -- server -c config.yaml