From 158aaf7e67514358b112a95665243b72e52aaa32 Mon Sep 17 00:00:00 2001 From: Till-JS <101404291+Till-JS@users.noreply.github.com> Date: Wed, 28 Jan 2026 16:40:33 +0100 Subject: [PATCH] feat(auth): add OIDC Provider for Matrix SSO integration - Add OIDC Provider plugin to Better Auth configuration - Add OIDC database tables (oauth_applications, oauth_access_tokens, oauth_authorization_codes, oauth_consents) - Configure Synapse as OIDC client in homeserver.yaml - Update Element Web config for SSO support - Add seed script for OIDC clients (db:seed:oidc) - Update Cloudflare tunnel config with Matrix URLs This enables Single Sign-On between Mana Core Auth and Matrix/Synapse, allowing users to authenticate via their existing Mana account. Co-Authored-By: Claude Opus 4.5 --- cloudflared-config.yml | 6 + docker/matrix/element-config.json | 3 +- docker/matrix/homeserver.yaml | 33 ++++++ services/mana-core-auth/package.json | 3 +- .../src/auth/better-auth.config.ts | 43 ++++++- .../src/db/schema/auth.schema.ts | 61 ++++++++++ .../src/db/seeds/seed-oidc-clients.ts | 112 ++++++++++++++++++ 7 files changed, 258 insertions(+), 3 deletions(-) create mode 100644 services/mana-core-auth/src/db/seeds/seed-oidc-clients.ts diff --git a/cloudflared-config.yml b/cloudflared-config.yml index 50e3bc241..ad4857ed7 100644 --- a/cloudflared-config.yml +++ b/cloudflared-config.yml @@ -46,5 +46,11 @@ ingress: - hostname: n8n.mana.how service: http://localhost:5678 + # Matrix (DSGVO-konformes Messaging) + - hostname: matrix.mana.how + service: http://localhost:8008 + - hostname: element.mana.how + service: http://localhost:8087 + # Catch-all - service: http_status:404 diff --git a/docker/matrix/element-config.json b/docker/matrix/element-config.json index ab7ec413c..4a6718886 100644 --- a/docker/matrix/element-config.json +++ b/docker/matrix/element-config.json @@ -41,7 +41,8 @@ "permalink_prefix": "https://element.mana.how", "terms_and_conditions_links": [], "sso_redirect_options": { - "immediate": false + "immediate": false, + "on_welcome_page": true }, "posthog": { "disabled": true diff --git a/docker/matrix/homeserver.yaml b/docker/matrix/homeserver.yaml index bc4c2b6e7..3612a02dd 100644 --- a/docker/matrix/homeserver.yaml +++ b/docker/matrix/homeserver.yaml @@ -188,3 +188,36 @@ run_background_tasks_on: synapse # smtp_pass: "${SMTP_PASSWORD}" # require_transport_security: true # notif_from: "ManaCore Matrix " + +# ============================================ +# OIDC / SSO Configuration (Mana Core Auth) +# ============================================ + +# Enable SSO via Mana Core Auth OIDC Provider +oidc_providers: + - idp_id: manacore + idp_name: "Mana Core" + idp_brand: "org.matrix.custom" + discover: true + issuer: "https://auth.mana.how" + client_id: "synapse" + client_secret: "${SYNAPSE_OIDC_CLIENT_SECRET}" + scopes: ["openid", "profile", "email"] + # Map OIDC claims to Matrix user attributes + user_mapping_provider: + config: + subject_claim: "sub" + localpart_template: "{{ user.email.split('@')[0] }}" + display_name_template: "{{ user.name }}" + email_template: "{{ user.email }}" + # Allow account linking with existing Matrix accounts + allow_existing_users: true + # Auto-provision new users from OIDC + enable_registration: true + +# SSO UI Settings +sso: + # Where to redirect after SSO login + client_whitelist: + - "https://element.mana.how" + - "https://matrix.mana.how" diff --git a/services/mana-core-auth/package.json b/services/mana-core-auth/package.json index fa36e8994..a678444b1 100644 --- a/services/mana-core-auth/package.json +++ b/services/mana-core-auth/package.json @@ -20,7 +20,8 @@ "db:generate": "drizzle-kit generate", "db:migrate": "tsx src/db/migrate.ts", "db:studio": "drizzle-kit studio", - "db:seed:dev": "tsx src/db/seed-dev-user.ts" + "db:seed:dev": "tsx src/db/seed-dev-user.ts", + "db:seed:oidc": "tsx src/db/seeds/seed-oidc-clients.ts" }, "dependencies": { "@google/generative-ai": "^0.24.1", diff --git a/services/mana-core-auth/src/auth/better-auth.config.ts b/services/mana-core-auth/src/auth/better-auth.config.ts index d79b44bb8..c10f5e074 100644 --- a/services/mana-core-auth/src/auth/better-auth.config.ts +++ b/services/mana-core-auth/src/auth/better-auth.config.ts @@ -18,9 +18,20 @@ import { betterAuth } from 'better-auth'; import { drizzleAdapter } from 'better-auth/adapters/drizzle'; import { jwt } from 'better-auth/plugins/jwt'; import { organization } from 'better-auth/plugins/organization'; +import { oidcProvider } from 'better-auth/plugins/oidc-provider'; import { getDb } from '../db/connection'; import { organizations, members, invitations } from '../db/schema/organizations.schema'; -import { users, sessions, accounts, verificationTokens, jwks } from '../db/schema/auth.schema'; +import { + users, + sessions, + accounts, + verificationTokens, + jwks, + oauthApplications, + oauthAccessTokens, + oauthAuthorizationCodes, + oauthConsents, +} from '../db/schema/auth.schema'; import type { JWTPayloadContext } from './types/better-auth.types'; import { sendPasswordResetEmail, @@ -84,6 +95,12 @@ export function createBetterAuth(databaseUrl: string) { // JWT plugin table jwks: jwks, + + // OIDC Provider tables + oauthApplication: oauthApplications, + oauthAccessToken: oauthAccessTokens, + oauthAuthorizationCode: oauthAuthorizationCodes, + oauthConsent: oauthConsents, }, }), @@ -268,6 +285,30 @@ export function createBetterAuth(databaseUrl: string) { }, }, }), + + /** + * OIDC Provider Plugin + * + * Enables Mana Core Auth to act as an OpenID Connect Provider. + * This allows Matrix/Synapse and other services to use SSO. + * + * Endpoints provided: + * - GET /.well-known/openid-configuration + * - GET /api/oidc/authorize + * - POST /api/oidc/token + * - GET /api/oidc/userinfo + * - GET /api/oidc/jwks + */ + oidcProvider({ + // Login page for OIDC authorization + loginPage: '/login', + // Consent page (skipped for trusted clients) + consentPage: '/consent', + // Use JWT plugin for token signing + metadata: { + issuer: process.env.BASE_URL || 'http://localhost:3001', + }, + }), ], }); } diff --git a/services/mana-core-auth/src/db/schema/auth.schema.ts b/services/mana-core-auth/src/db/schema/auth.schema.ts index 0f392c657..cb28a5b21 100644 --- a/services/mana-core-auth/src/db/schema/auth.schema.ts +++ b/services/mana-core-auth/src/db/schema/auth.schema.ts @@ -126,6 +126,67 @@ export const jwks = authSchema.table('jwks', { createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), }); +// OIDC Provider tables (Better Auth OIDC Provider plugin) +// OAuth Applications (OIDC Clients like Matrix/Synapse) +export const oauthApplications = authSchema.table('oauth_applications', { + id: text('id').primaryKey(), + name: text('name').notNull(), + icon: text('icon'), + metadata: text('metadata'), + clientId: text('client_id').unique().notNull(), + clientSecret: text('client_secret').notNull(), + redirectURLs: text('redirect_urls').notNull(), // JSON array as text + type: text('type').notNull().default('web'), // web, native, spa + disabled: boolean('disabled').default(false).notNull(), + userId: text('user_id').references(() => users.id, { onDelete: 'cascade' }), + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), +}); + +// OAuth Access Tokens +export const oauthAccessTokens = authSchema.table('oauth_access_tokens', { + id: text('id').primaryKey(), + accessToken: text('access_token').unique().notNull(), + refreshToken: text('refresh_token').unique(), + accessTokenExpiresAt: timestamp('access_token_expires_at', { withTimezone: true }).notNull(), + refreshTokenExpiresAt: timestamp('refresh_token_expires_at', { withTimezone: true }), + clientId: text('client_id').notNull(), + userId: text('user_id') + .references(() => users.id, { onDelete: 'cascade' }) + .notNull(), + scopes: text('scopes').notNull(), // JSON array as text + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), +}); + +// OAuth Authorization Codes +export const oauthAuthorizationCodes = authSchema.table('oauth_authorization_codes', { + id: text('id').primaryKey(), + code: text('code').unique().notNull(), + clientId: text('client_id').notNull(), + userId: text('user_id') + .references(() => users.id, { onDelete: 'cascade' }) + .notNull(), + scopes: text('scopes').notNull(), // JSON array as text + redirectUri: text('redirect_uri').notNull(), + codeChallenge: text('code_challenge'), + codeChallengeMethod: text('code_challenge_method'), + expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(), + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), +}); + +// OAuth Consents (user consent records for OIDC scopes) +export const oauthConsents = authSchema.table('oauth_consents', { + id: text('id').primaryKey(), + userId: text('user_id') + .references(() => users.id, { onDelete: 'cascade' }) + .notNull(), + clientId: text('client_id').notNull(), + scopes: text('scopes').notNull(), // JSON array as text + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), +}); + // User settings table (synced across all apps) export const userSettings = authSchema.table('user_settings', { userId: text('user_id') diff --git a/services/mana-core-auth/src/db/seeds/seed-oidc-clients.ts b/services/mana-core-auth/src/db/seeds/seed-oidc-clients.ts new file mode 100644 index 000000000..84f7f7470 --- /dev/null +++ b/services/mana-core-auth/src/db/seeds/seed-oidc-clients.ts @@ -0,0 +1,112 @@ +/** + * Seed Script: OIDC Clients + * + * This script creates the OIDC client entries for services that use + * Mana Core Auth as their OIDC Provider (e.g., Matrix/Synapse). + * + * Usage: + * pnpm db:seed:oidc + * + * Environment: + * DATABASE_URL - PostgreSQL connection string + * SYNAPSE_OIDC_CLIENT_SECRET - Client secret for Synapse (generate with: openssl rand -hex 32) + */ + +import { drizzle } from 'drizzle-orm/postgres-js'; +import postgres from 'postgres'; +import { oauthApplications } from '../schema/auth.schema'; +import { eq } from 'drizzle-orm'; +import { randomBytes } from 'crypto'; + +// Load environment variables +import 'dotenv/config'; + +// Generate a secure random ID +function generateId(): string { + return randomBytes(16).toString('hex'); +} + +async function seed() { + const databaseUrl = process.env.DATABASE_URL; + if (!databaseUrl) { + console.error('āŒ DATABASE_URL environment variable is required'); + process.exit(1); + } + + const client = postgres(databaseUrl); + const db = drizzle(client); + + console.log('🌱 Seeding OIDC clients...\n'); + + // Get or generate Synapse client secret + const synapseClientSecret = + process.env.SYNAPSE_OIDC_CLIENT_SECRET || randomBytes(32).toString('hex'); + + if (!process.env.SYNAPSE_OIDC_CLIENT_SECRET) { + console.log('āš ļø No SYNAPSE_OIDC_CLIENT_SECRET provided, generated new secret:'); + console.log(` ${synapseClientSecret}`); + console.log(' Add this to your .env and Synapse configuration!\n'); + } + + // Check if Synapse client already exists + const existingClient = await db + .select() + .from(oauthApplications) + .where(eq(oauthApplications.clientId, 'synapse')) + .limit(1); + + if (existingClient.length > 0) { + console.log('ā„¹ļø Synapse OIDC client already exists, updating...'); + + await db + .update(oauthApplications) + .set({ + clientSecret: synapseClientSecret, + redirectURLs: JSON.stringify(['https://matrix.mana.how/_synapse/client/oidc/callback']), + updatedAt: new Date(), + }) + .where(eq(oauthApplications.clientId, 'synapse')); + + console.log('āœ… Synapse OIDC client updated\n'); + } else { + console.log('šŸ“ Creating Synapse OIDC client...'); + + await db.insert(oauthApplications).values({ + id: generateId(), + name: 'Matrix Synapse', + icon: 'https://matrix.org/images/matrix-logo.svg', + clientId: 'synapse', + clientSecret: synapseClientSecret, + redirectURLs: JSON.stringify(['https://matrix.mana.how/_synapse/client/oidc/callback']), + type: 'web', + disabled: false, + metadata: JSON.stringify({ + description: 'Matrix Synapse homeserver for DSGVO-compliant messaging', + trusted: true, + skipConsent: true, + }), + createdAt: new Date(), + updatedAt: new Date(), + }); + + console.log('āœ… Synapse OIDC client created\n'); + } + + // Summary + console.log('šŸ“‹ OIDC Client Summary:'); + console.log(' Client ID: synapse'); + console.log(` Client Secret: ${synapseClientSecret.substring(0, 8)}...`); + console.log(' Redirect URL: https://matrix.mana.how/_synapse/client/oidc/callback'); + console.log('\nšŸ” Next steps:'); + console.log(' 1. Add SYNAPSE_OIDC_CLIENT_SECRET to Synapse environment'); + console.log(' 2. Restart Synapse to pick up OIDC configuration'); + console.log(' 3. Test SSO flow via Element Web\n'); + + await client.end(); + console.log('✨ Seeding complete!'); +} + +seed().catch((error) => { + console.error('āŒ Seeding failed:', error); + process.exit(1); +});