mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 22:21:10 +02:00
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:
parent
dbd14f7134
commit
158aaf7e67
7 changed files with 258 additions and 3 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
112
services/mana-core-auth/src/db/seeds/seed-oidc-clients.ts
Normal file
112
services/mana-core-auth/src/db/seeds/seed-oidc-clients.ts
Normal 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);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue