Skip to content

Commit 0a9c881

Browse files
committed
Feature: OpenID Connect (OIDC) authentication
Add OIDC as a login option with provider configuration, group-to-role mapping, and automatic user provisioning. Relates to #388
1 parent 6764b47 commit 0a9c881

21 files changed

Lines changed: 1993 additions & 102 deletions

File tree

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import type { Knex } from "knex";
2+
3+
export async function up(knex: Knex): Promise<void> {
4+
// 1. Add auth_provider and oidc_sub columns to users table
5+
const hasAuthProvider = await knex.schema.hasColumn("users", "auth_provider");
6+
if (!hasAuthProvider) {
7+
await knex.schema.alterTable("users", (table) => {
8+
// "local" for password-based accounts, "oidc" for OpenID Connect accounts
9+
table.string("auth_provider", 20).notNullable().defaultTo("local");
10+
});
11+
}
12+
13+
const hasOidcSub = await knex.schema.hasColumn("users", "oidc_sub");
14+
if (!hasOidcSub) {
15+
await knex.schema.alterTable("users", (table) => {
16+
// Subject identifier from the OIDC provider (unique per provider)
17+
table.string("oidc_sub", 255).nullable().unique();
18+
});
19+
}
20+
21+
// 2. Make password_hash nullable for OIDC users who have no local password.
22+
// SQLite does not support ALTER COLUMN, so we store an empty string
23+
// for OIDC users there. For PostgreSQL/MySQL we properly drop NOT NULL.
24+
const dbClient = knex.client.config.client;
25+
if (dbClient === "pg" || dbClient === "postgresql") {
26+
await knex.schema.raw('ALTER TABLE users ALTER COLUMN password_hash DROP NOT NULL');
27+
} else if (dbClient === "mysql" || dbClient === "mysql2") {
28+
await knex.schema.raw('ALTER TABLE users MODIFY password_hash VARCHAR(255) NULL');
29+
}
30+
31+
// 3. Create OIDC group-to-role mapping table
32+
if (!(await knex.schema.hasTable("oidc_group_role_mappings"))) {
33+
await knex.schema.createTable("oidc_group_role_mappings", (table) => {
34+
table.increments("id").primary();
35+
table.string("oidc_group", 255).notNullable();
36+
table
37+
.string("role_id", 100)
38+
.notNullable()
39+
.references("id")
40+
.inTable("roles")
41+
.onDelete("CASCADE");
42+
table.timestamp("created_at").defaultTo(knex.fn.now());
43+
table.timestamp("updated_at").defaultTo(knex.fn.now());
44+
table.unique(["oidc_group"]);
45+
});
46+
}
47+
}
48+
49+
export async function down(knex: Knex): Promise<void> {
50+
await knex.schema.dropTableIfExists("oidc_group_role_mappings");
51+
52+
const hasOidcSub = await knex.schema.hasColumn("users", "oidc_sub");
53+
if (hasOidcSub) {
54+
await knex.schema.alterTable("users", (table) => {
55+
table.dropColumn("oidc_sub");
56+
});
57+
}
58+
59+
const hasAuthProvider = await knex.schema.hasColumn("users", "auth_provider");
60+
if (hasAuthProvider) {
61+
await knex.schema.alterTable("users", (table) => {
62+
table.dropColumn("auth_provider");
63+
});
64+
}
65+
}

package-lock.json

Lines changed: 92 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@
161161
"node-cache": "^5.1.2",
162162
"nodemailer": "^7.0.11",
163163
"npm-run-all": "^4.1.5",
164+
"openid-client": "^6.8.4",
164165
"pg": "^8.16.3",
165166
"pg-pool": "^3.10.1",
166167
"ping": "^1.0.0",

src/lib/allPerms.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,13 @@ export const ACTION_PERMISSION_MAP: Record<string, string | null> = {
221221
updateRolePermissions: "roles.assign_permissions",
222222
addUserToRole: "roles.assign_users",
223223
removeUserFromRole: "roles.assign_users",
224+
225+
// OIDC
226+
getOidcGroupRoleMappings: "settings.read",
227+
upsertOidcGroupRoleMapping: "settings.write",
228+
deleteOidcGroupRoleMapping: "settings.write",
229+
testOidcConnection: "settings.write",
230+
clearOidcCache: "settings.write",
224231
};
225232

226233
export const ROUTE_PERMISSION_MAP: Record<string, string | null> = {
@@ -272,4 +279,7 @@ export const ROUTE_PERMISSION_MAP: Record<string, string | null> = {
272279

273280
// Roles
274281
"/(manage)/manage/app/roles": "roles.read",
282+
283+
// OIDC
284+
"/(manage)/manage/app/oidc": "settings.read",
275285
};

src/lib/server/controllers/controller.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export * from "./commonController.js";
33
export * from "./emailController.js";
44
export * from "./incidentController.js";
55
export * from "./monitorsController.js";
6+
export * from "./oidcController.js";
67
export * from "./siteDataController.js";
78
export * from "./siteDataKeys.js";
89
export * from "./triggerController.js";

0 commit comments

Comments
 (0)