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 <noreply@anthropic.com>
This commit is contained in:
Till-JS 2026-01-28 16:40:33 +01:00
parent dbd14f7134
commit 158aaf7e67
7 changed files with 258 additions and 3 deletions

View file

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

View file

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

View file

@ -188,3 +188,36 @@ run_background_tasks_on: synapse
# smtp_pass: "${SMTP_PASSWORD}"
# require_transport_security: true
# notif_from: "ManaCore Matrix <noreply@mana.how>"
# ============================================
# 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"

View file

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

View file

@ -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',
},
}),
],
});
}

View file

@ -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')

View file

@ -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);
});