Skip to content

Feature: OpenID Connect (OIDC) authentication - #733

Open
Plattenspatz wants to merge 2 commits into
rajnandan1:mainfrom
Plattenspatz:feature/oidc-support
Open

Feature: OpenID Connect (OIDC) authentication#733
Plattenspatz wants to merge 2 commits into
rajnandan1:mainfrom
Plattenspatz:feature/oidc-support

Conversation

@Plattenspatz

@Plattenspatz Plattenspatz commented May 29, 2026

Copy link
Copy Markdown

Relates to #388

This adds OpenID Connect as a login option for Kener. Users can sign in via an external identity provider (Keycloak, Authentik, Azure AD, Okta, etc.) instead of — or in addition to — local username/password.

What it does

  • OIDC login flow: Authorization Code Flow with PKCE. Users click "Sign in with {provider}" on the login page, authenticate at the provider, and get redirected back with a session.
  • Admin settings page: New page under Manage → OpenID Connect where admins configure the provider (issuer URL, client ID/secret, scopes, groups claim) and control whether local login stays available.
  • Group-to-role mapping: A table in the settings page maps OIDC group names to Kener roles. For example, the OIDC group "Windows-Admins" can be mapped to a Kener role "Windows Administrators".
  • Role sync on every login: Each time a user logs in via OIDC, their group memberships are read from the ID token and their Kener roles are updated accordingly. If a user is removed from a group in the identity provider, they lose the corresponding role on next login. Manually assigned roles (not part of any mapping) are preserved.
  • Auto-provisioning: New users are created automatically on first OIDC login (configurable). OIDC users and local users are kept separate — no account merging.
  • Default role fallback: If none of a user's OIDC groups match any mapping, a configurable default role is assigned.
  • Test connection button: Admins can verify the OIDC discovery endpoint works before saving.

What it doesn't do (yet)

  • No RP-Initiated Logout (signing out of Kener doesn't end the session at the provider)
  • No LDAP or SAML — just OIDC for now
  • No Forward Auth / Header Auth

Screenshots

grafik grafik grafik grafik grafik

Changes overview

New files:

  • migrations/20260610120000_add_oidc_support.ts — adds auth_provider and oidc_sub columns to users, creates oidc_group_role_mappings table
  • src/lib/server/controllers/oidcController.ts — OIDC discovery, token exchange, user provisioning, role sync
  • src/routes/(account)/account/oidc/login/+server.ts — redirects to the OIDC provider
  • src/routes/(account)/account/oidc/callback/+server.ts — handles the callback, creates/updates the user, sets the session cookie
  • src/routes/(manage)/manage/app/oidc/+page.svelte — admin settings page with provider config and group-role mapping

Modified files:

  • package.json — added openid-client dependency
  • src/lib/types/site.tsOidcSettings interface
  • src/lib/server/types/db.ts — extended user types with auth_provider and oidc_sub, added mapping types
  • src/lib/server/db/repositories/users.ts — OIDC user lookup and group-role mapping CRUD
  • src/lib/server/db/dbimpl.ts — exposed new repository methods
  • src/lib/server/controllers/siteDataKeys.ts — registered oidcSettings
  • src/lib/server/db/seedSiteData.ts — default OIDC settings
  • src/lib/server/controllers/controller.ts — barrel export
  • src/lib/allPerms.ts — registered OIDC actions in permission maps and route map
  • src/routes/(account)/account/signin/+page.server.ts — loads OIDC settings, passes them to the page, blocks local login for OIDC users
  • src/routes/(account)/account/signin/+page.svelte — OIDC login button, separator, error display
  • src/routes/(manage)/manage/api/+server.ts — OIDC management API actions
  • src/routes/(manage)/+layout.svelte — sidebar navigation link
  • src/routes/(manage)/manage/app/users/+page.svelte — auth provider badge in users table

How to test

  1. Set up an OIDC provider (e.g. Keycloak) with a client for Kener
  2. Set the redirect URI to https://your-kener/account/oidc/callback
  3. In Kener: Manage → OpenID Connect → configure and save
  4. Click "Test Connection" to verify discovery works
  5. Add group-role mappings
  6. Open the login page — "Sign in with {provider}" button should appear
  7. Sign in, verify user is created with correct roles
  8. Change groups in the provider, sign in again, verify roles update

Summary by CodeRabbit

Release Notes

  • New Features

    • Added OpenID Connect (OIDC) single sign-on with Authorization Code Flow + PKCE, including discovery caching and callback handling.
    • Added an OpenID Connect settings panel for enabling SSO, configuring issuer/client details, scopes, and group-to-role mappings.
    • Added OIDC connection testing and automated role synchronization on login.
    • Updated sign-in to show an SSO button and optionally block local password login; user management now shows whether users are Local or OIDC.
  • Documentation

    • Added a full OIDC setup guide (v4) covering configuration, group claims, mapping behavior, and limitations.

@rajnandan1

Copy link
Copy Markdown
Owner

I am reviewing the PR, hope it is good and could be merged without much change. thanks a lot

@luckylinux

luckylinux commented Jun 11, 2026

Copy link
Copy Markdown

I am happy to see somebody trying to bring forward this 👍. For once this does NOT seem to be an AI SLOP / complete Rewrite of the Codebase, so that's already a good start 😉.

I don't know much at all about Typescript, so I cannot provide much Feedback on that Side. I just flew very quickly over the Changes and I didn't see alarming red Flags such as Code Obfuscation, base64 encoded Values, etc, so it should be safe.

Unsure about package-lock.json, though ‎package.json only added "openid-client": "^6.8.4", which seems legit.

users.ts.save seeems to be the previous Version of user.ts. Is this an intentional "Backup" / standard Practice for Typescript Projects ? Or is this some leftover from some IDE that's not needed?

https://github.com/Plattenspatz/kener/blob/d5f32f0a020d8917a6e22e021851933b0e511a6a/src/lib/server/db/repositories/users.ts.save

Not sure about this Change here and why || !passwordStored.password_hash was added:
https://github.com/rajnandan1/kener/pull/733/changes#diff-4ab424ee6affa8ac732f371070db69df4ed6abcc3a6e6868ac7dcdf2722d8f2aR74

if (!passwordStored || !passwordStored.password_hash) {
      return fail(401, { error: "Invalid password or Email", values: { email } });
    }

Is this related to the Comment Make password_hash nullable for OIDC users who have no local password. ? Is that Section only covering Local Authentication ?

@rajnandan1 rajnandan1 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR. fundamentals are right, PKCE + state + nonce, OIDC and local accounts kept separate by sub, email collisions rejected instead of merged. good work

Blockers

  • remove src/lib/server/db/repositories/users.ts.save, backup file got committed
  • test connection poisons the live config cache. TestOidcConnection clears the cache then getOidcConfig(settings) writes the unsaved client submitted settings into cachedConfig. after admin tests an edited config all real logins use those unsaved credentials. build a local Configuration for test, do not touch the shared cache
  • cache key is only issuer_url. changing client_id/secret for the same issuer does not invalidate. also clearing depends on the browser making a second clearOidcCache call after storeSiteData, if that fails we keep stale credentials silently. clear the cache server side in the storeSiteData handler when key is oidcSettings
  • KENER_BASE_PATH breaks the redirect_uri. ${url.origin}/account/oidc/callback omits the base path but the state cookies are set with path = KENER_BASE_PATH. same for the hardcoded href="/account/oidc/login" on signin page
  • users table header has 7 columns now but loading/empty rows still have colspan={6}

Security

  • client_secret goes to the browser via getSiteDataByKey. anyone with settings.read can see the IdP secret. mask it on read, update only when a new value is submitted
  • allow_local_login false + broken IdP = nobody can sign in, no escape hatch. always allow local login for owner or add an env var override. this is needed before release
  • testOidcConnection fetches a client supplied url server side. settings.write gated so ok, but require https in non dev
  • auto_create_users true + default role member means every IdP account can self provision into kener. make deafult false imo

Minor

  • updateUserOidcSub is dead code, never called
  • email/name never refreshed from IdP on next logins, worth syncing along with roles noh?
  • openid-client v6 enforces https for discovery, http issuer in local dev will fail with an unhelpful error
  • migration drops NOT NULL on password_hash for pg/mysql but code always inserts "" anyway. pick one. down() does not restore NOT NULL
  • no server side validation in upsertOidcGroupRoleMapping, empty group or bad role_id surfaces a raw db error
  • RoleRecord imported from $lib/server/types/db.js in a client component, shared types go in src/lib/types
  • == vs === in users page and a stray whitespace edit in (manage)/+layout.svelte, run npm run prettify
  • callback checks cookies before the IdP error param, an error after the 10 min cookie expiry shows a generic 400

also please add a docs page under src/routes/(docs)/docs/content, setup + redirect uri + group mapping + the lockout caveat

fix the blockers and we can merge

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please remove this file

</div>
<!-- Resend Invitation -->
{#if !toEditUser.has_password}
{#if !toEditUser.has_password && toEditUser.auth_provider !== "oidc"}

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we move this global-constant file "oidc"

/**
* Generate a cryptographically random string for state/nonce parameters.
*/
function generateRandomString(length: number = 32): string {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can add this src/lib/server/tool.ts and export from there

@dfjones1981

Copy link
Copy Markdown

Just thought I'd add some encouragement for this feature. Once it's in, we plan to properly trial kener. Good luck!

Add OIDC as a login option with provider configuration,
group-to-role mapping, and automatic user provisioning.

Relates to rajnandan1#388
@Plattenspatz
Plattenspatz force-pushed the feature/oidc-support branch from d5f32f0 to 0a9c881 Compare June 22, 2026 10:46
@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds full OpenID Connect (OIDC) SSO support: a DB migration and repository layer for auth_provider/oidc_sub user fields and oidc_group_role_mappings table, a controller implementing Authorization Code Flow with PKCE and role synchronization, login/callback routes, a management UI page, updated sign-in page with OIDC-aware login controls, and documentation.

Changes

OIDC Authentication Feature

Layer / File(s) Summary
DB types, site types, migration, constants, and seed data
package.json, src/lib/server/types/db.ts, src/lib/types/site.ts, src/lib/global-constants.ts, migrations/20260610120000_add_oidc_support.ts, src/lib/server/db/seedSiteData.ts
UserRecord/UserRecordInsert/UserRecordPublic gain auth_provider and oidc_sub fields; new OidcGroupRoleMappingRecord/OidcGroupRoleMappingInsert DB interfaces and OidcSettings/OidcGroupRoleMapping site types are defined; migration conditionally adds columns and the oidc_group_role_mappings table; AUTH_PROVIDER_LOCAL/AUTH_PROVIDER_OIDC constants added; seed data includes a default disabled OIDC config; openid-client dependency added.
Repository OIDC methods and DbImpl wiring
src/lib/server/db/repositories/users.ts, src/lib/server/db/dbimpl.ts
userColumns and insertUser include auth_provider/oidc_sub; updateUserProfile and OIDC methods (getUserByOidcSub, getAllOidcGroupRoleMappings, getOidcGroupRoleMappingByGroup, upsertOidcGroupRoleMapping, deleteOidcGroupRoleMapping, getOidcRoleIdsForGroups) added to UsersRepository and wired onto DbImpl.
OIDC controller: discovery, PKCE, callback, user provisioning, role sync
src/lib/server/tool.ts, src/lib/server/controllers/oidcController.ts, src/lib/server/controllers/siteDataKeys.ts, src/lib/server/controllers/controller.ts
Implements GetOidcSettings, cached discovery with ClearOidcConfigCache, BuildAuthorizationUrl with PKCE/random state using GenerateRandomHexString, HandleCallback (token exchange + claim normalization), FindOrCreateOidcUser (user provisioning with auto-create enforcement), SyncOidcUserRoles (preserves manually assigned roles), GenerateOidcSession, and TestOidcConnection. Registers oidcSettings site data key and re-exports the module.
Permissions wiring and manage API OIDC actions
src/lib/allPerms.ts, src/routes/(manage)/manage/api/+server.ts
Maps OIDC actions to settings.read/settings.write and the /manage/app/oidc route to settings.read. Extends the manage API dispatcher with group-role mapping CRUD, connection testing, and masked settings retrieval. storeSiteData preserves client_secret on partial saves and clears the OIDC config cache on update.
OIDC login/callback routes and sign-in page integration
src/routes/(account)/account/oidc/login/+server.ts, src/routes/(account)/account/oidc/callback/+server.ts, src/routes/(account)/account/signin/+page.server.ts, src/routes/(account)/account/signin/+page.svelte
Login route sets HTTP-only OIDC cookies and redirects to the provider. Callback route validates cookies, runs HandleCallback/FindOrCreateOidcUser, sets session cookie, and redirects. Sign-in server blocks password login for OIDC accounts or when local login is disabled. Sign-in page conditionally renders the OIDC sign-in button, error alert, and local login form.
OIDC management page, navigation, and users table updates
src/routes/(manage)/manage/app/oidc/+page.svelte, src/routes/(manage)/+layout.svelte, src/routes/(manage)/manage/app/users/+page.svelte, src/routes/(docs)/docs.json, src/routes/(docs)/docs/content/v4/oidc.md, .gitignore
Creates the /manage/app/oidc settings page with connection test, group-role mapping management, and an informational card. Adds the "OpenID Connect" nav entry. Users table gains an "Auth" column with OIDC/Local badges and adjusted verified/resend-invitation conditions. OIDC documentation page and sidebar entry added.

Sequence Diagram(s)

sequenceDiagram
  actor User
  participant SignInPage
  participant LoginRoute as /account/oidc/login
  participant OidcController
  participant IdentityProvider
  participant CallbackRoute as /account/oidc/callback
  participant UsersRepository

  User->>SignInPage: clicks "Sign in with {Provider}"
  SignInPage->>LoginRoute: GET /account/oidc/login
  LoginRoute->>OidcController: GetOidcSettings()
  LoginRoute->>OidcController: BuildAuthorizationUrl(settings, callbackUrl)
  OidcController->>IdentityProvider: discovery(issuer_url)
  IdentityProvider-->>OidcController: provider metadata
  OidcController-->>LoginRoute: authUrl, state, nonce, codeVerifier
  LoginRoute-->>User: 302 redirect + sets oidc-state/nonce/code-verifier cookies

  User->>IdentityProvider: authenticates
  IdentityProvider->>CallbackRoute: GET /account/oidc/callback?code=...&state=...
  CallbackRoute->>OidcController: HandleCallback(settings, callbackUrl, url, state, nonce, codeVerifier)
  OidcController->>IdentityProvider: authorizationCodeGrant(code, PKCE)
  IdentityProvider-->>OidcController: tokens + ID token claims
  OidcController-->>CallbackRoute: sub, email, name, groups

  CallbackRoute->>OidcController: FindOrCreateOidcUser(settings, oidcData)
  OidcController->>UsersRepository: getUserByOidcSub(sub)
  OidcController->>UsersRepository: getOidcRoleIdsForGroups(groups)
  OidcController->>UsersRepository: insertUser or SyncOidcUserRoles
  OidcController-->>CallbackRoute: UserRecordPublic

  CallbackRoute->>OidcController: GenerateOidcSession(user)
  OidcController-->>CallbackRoute: token + cookieConfig
  CallbackRoute-->>User: 302 redirect to /manage/app/site-configurations + session cookie
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Suggested reviewers

  • rajnandan1

Poem

🐇 Hop through the discovery endpoint with glee,
A PKCE dance for security!
State and nonce, a cryptographic pair,
Groups map to roles with the greatest of care.
The rabbit signs in with a provider's key,
No password needed — OIDC runs free! 🔑

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title 'Feature: OpenID Connect (OIDC) authentication' clearly and concisely summarizes the main change introduced in the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 markdownlint-cli2 (0.22.1)
src/routes/(docs)/docs/content/v4/oidc.md

markdownlint-cli2 v0.22.1 (markdownlint v0.40.0)
Error: Unable to use configuration file '/coderabbit-0.markdownlint-cli2.jsonc'; ENOENT: no such file or directory, open '/coderabbit-0.markdownlint-cli2.jsonc'
at throwForConfigurationFile (file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2.mjs:48:9)
at readOptionsOrConfig (file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2.mjs:169:5)
at async main (file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2.mjs:927:21)
at async file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2-bin.mjs:14:22 {
[cause]: Error: ENOENT: no such file or directory, open '/coderabbit-0.markdownlint-cli2.jsonc'
at async open (node:internal/fs/promises:640:25)
at async Object.readFile (node:internal/fs/promises:1287:14)
at async readOptionsOrConfig (file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2.mjs:141:17)
at async main (file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2.mjs:927:21)
at async file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2-bin.mjs:14:22 {
errno: -2,
code: 'ENOENT',
syscall: 'open',
path: '/coderabbit-0.markdownlint-cli2.jsonc'
}
}


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds full OpenID Connect (OIDC) SSO support to Kener, implementing the Authorization Code Flow with PKCE, auto-provisioning of new users on first login, group-to-role synchronization on every login, and an admin settings page for configuring the provider and managing group-role mappings.

  • OIDC auth flow: New login and callback server routes handle PKCE state/nonce cookie lifecycle; oidcController.ts covers discovery caching, token exchange, and role sync.
  • Admin UI: A new Svelte page lets admins configure the provider, test discovery, and manage group-role mappings; client_secret is masked on reads and preserved correctly on saves.
  • Database: Migration adds auth_provider/oidc_sub columns to users and a new oidc_group_role_mappings table; repository adds getUserByOidcSub and mapping CRUD methods.

Confidence Score: 3/5

Safe to review further but not ready to merge: the user provisioning path has a data integrity bug and two previously flagged auth-path issues remain unaddressed.

The insertUser function silently discards the is_active and is_verified fields added to UserRecordInsert in this PR. Every OIDC user is created with is_verified = 0 even though the provisioning code explicitly passes is_verified: 1. Combined with the still-open email unique-constraint issue in FindOrCreateOidcUser and the deleteOidcGroupRoleMapping missing-ID guard, the login and user-management paths have multiple present defects that need fixes before this feature is production-ready.

src/lib/server/db/repositories/users.ts (insertUser drops is_verified), src/lib/server/controllers/oidcController.ts (email-update unique constraint), src/routes/(manage)/manage/api/+server.ts (deleteOidcGroupRoleMapping id validation)

Important Files Changed

Filename Overview
src/lib/server/db/repositories/users.ts Adds getUserByOidcSub and OIDC group-role mapping CRUD. insertUser silently drops is_active/is_verified fields passed by the OIDC provisioning caller (P1).
src/lib/server/controllers/oidcController.ts Core OIDC logic: discovery caching, PKCE auth-code flow, user provisioning, and role sync. Email-update unique-constraint race (pre-existing P1 thread) remains open; otherwise flow is sound.
src/routes/(account)/account/oidc/callback/+server.ts OIDC callback handler: validates state/nonce/PKCE cookies, calls FindOrCreateOidcUser, sets session cookie. KENER_BASE_PATH mismatch flagged in previous thread; redirect/error handling looks correct.
src/routes/(account)/account/oidc/login/+server.ts Initiates OIDC auth-code flow with PKCE; KENER_BASE_PATH mismatch flagged in previous thread. Cookie security attributes (httpOnly, sameSite=lax, 10-min maxAge) are appropriate.
src/routes/(manage)/manage/api/+server.ts Adds OIDC group-role mapping CRUD and testOidcConnection actions. Missing ID validation on deleteOidcGroupRoleMapping flagged in previous thread; client_secret preservation logic in storeSiteData is correct.
migrations/20260610120000_add_oidc_support.ts Adds auth_provider/oidc_sub columns and oidc_group_role_mappings table. Uses hasColumn/hasTable guards for idempotency; raw SQL ALTER TABLE concern flagged in previous thread.
src/routes/(manage)/manage/app/oidc/+page.svelte Admin OIDC configuration UI. Correctly uses getOidcSettingsMasked and strips unchanged client_secret before saving. Group-role mapping CRUD with confirmation dialog is well-implemented.
src/routes/(account)/account/signin/+page.server.ts Loads OIDC settings for the login page and blocks local login for OIDC users. Logic is clean; KENER_FORCE_LOCAL_LOGIN escape hatch is a good safeguard.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant U as User Browser
    participant K as Kener (login route)
    participant IdP as Identity Provider
    participant CB as Kener (callback route)
    participant DB as Database

    U->>K: GET /account/oidc/login
    K->>K: Generate state, nonce, PKCE verifier
    K->>U: Set oidc-state/nonce/code-verifier cookies
    K->>U: 302 → IdP authorization URL

    U->>IdP: Authenticate
    IdP->>U: "302 → /account/oidc/callback?code=…&state=…"

    U->>CB: GET /account/oidc/callback
    CB->>CB: Validate state cookie, nonce cookie
    CB->>IdP: Exchange code for tokens (PKCE)
    IdP->>CB: ID token + access token
    CB->>CB: Extract sub, email, name, groups claims
    CB->>DB: getUserByOidcSub(sub)
    alt User exists
        CB->>DB: SyncOidcUserRoles(userId, groups)
        CB->>DB: updateUserProfile(userId, name, email)
    else "New user (auto_create_users=true)"
        CB->>DB: getOidcRoleIdsForGroups(groups)
        CB->>DB: insertUser(email, name, roles, oidc_sub)
    end
    CB->>CB: GenerateOidcSession → JWT
    CB->>U: Set session cookie
    CB->>U: 302 → /manage/app/site-configurations
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant U as User Browser
    participant K as Kener (login route)
    participant IdP as Identity Provider
    participant CB as Kener (callback route)
    participant DB as Database

    U->>K: GET /account/oidc/login
    K->>K: Generate state, nonce, PKCE verifier
    K->>U: Set oidc-state/nonce/code-verifier cookies
    K->>U: 302 → IdP authorization URL

    U->>IdP: Authenticate
    IdP->>U: "302 → /account/oidc/callback?code=…&state=…"

    U->>CB: GET /account/oidc/callback
    CB->>CB: Validate state cookie, nonce cookie
    CB->>IdP: Exchange code for tokens (PKCE)
    IdP->>CB: ID token + access token
    CB->>CB: Extract sub, email, name, groups claims
    CB->>DB: getUserByOidcSub(sub)
    alt User exists
        CB->>DB: SyncOidcUserRoles(userId, groups)
        CB->>DB: updateUserProfile(userId, name, email)
    else "New user (auto_create_users=true)"
        CB->>DB: getOidcRoleIdsForGroups(groups)
        CB->>DB: insertUser(email, name, roles, oidc_sub)
    end
    CB->>CB: GenerateOidcSession → JWT
    CB->>U: Set session cookie
    CB->>U: 302 → /manage/app/site-configurations
Loading

Reviews (3): Last reviewed commit: "Address review feedback: security, cache..." | Re-trigger Greptile

Comment thread src/lib/server/db/repositories/users.ts.save Outdated
Comment thread src/routes/(account)/account/oidc/login/+server.ts Outdated
Comment thread src/routes/(account)/account/oidc/callback/+server.ts Outdated
Comment thread migrations/20260610120000_add_oidc_support.ts Outdated
Comment thread src/routes/(manage)/manage/app/oidc/+page.svelte Outdated
Comment thread src/routes/(manage)/manage/app/oidc/+page.svelte Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@migrations/20260610120000_add_oidc_support.ts`:
- Around line 21-29: The migration is missing error handling for the raw SQL
statements on lines 26 and 28, and the down() function does not restore the NOT
NULL constraint on password_hash, leaving the database schema in an inconsistent
state after rollback. Wrap the raw SQL ALTER TABLE and MODIFY statements in a
try/catch block to handle errors properly, or refactor to use
knex.schema.alterTable() following the pattern demonstrated in lines 7-19.
Additionally, implement the down() function to explicitly restore the NOT NULL
constraint for both PostgreSQL (using ALTER COLUMN password_hash SET NOT NULL)
and MySQL/MySQL2 (using MODIFY password_hash VARCHAR(255) NOT NULL) to ensure
schema symmetry on rollback.

In `@src/lib/server/controllers/oidcController.ts`:
- Around line 24-35: In the GetOidcSettings function, remove the unnecessary
`typeof raw === "string"` type guard check on line 29 since GetSiteDataByKey
already returns parsed JSON objects when data_type is "object". Simply assign
raw directly to settings without the conditional JSON.parse, as raw will always
be an object and the string branch is dead code.
- Around line 141-149: Add a guard condition before calling client.fetchUserInfo
in the oidcController.ts file to check that tokens.access_token exists. If
access_token is missing or undefined, throw an error with a meaningful message
that explains the OIDC provider did not return an access_token (e.g., some
configurations only issue an id_token). Only proceed with the fetchUserInfo call
if the access_token is confirmed to be present, removing the non-null assertion
from the method call.

In `@src/lib/server/db/repositories/users.ts`:
- Around line 32-33: The getUsersByRoleId method is missing the auth_provider
and oidc_sub columns in its query selection. Update the column projection in the
getUsersByRoleId method to include both auth_provider and oidc_sub so that the
returned objects conform to the UserRecordPublic type contract, matching the
updated projections added in the nearby code.

In `@src/routes/`(account)/account/oidc/callback/+server.ts:
- Line 38: The callbackUrl constant definition is missing the KENER_BASE_PATH
prefix, which will cause incorrect callback URL generation when the application
is deployed with a non-root base path. Update the callbackUrl construction to
include the KENER_BASE_PATH prefix (following the same pattern used in the login
route) so that the full callback URL includes the base path when present,
ensuring proper token exchange during the OIDC callback flow.

In `@src/routes/`(account)/account/oidc/login/+server.ts:
- Line 11: The callbackUrl variable in both the OIDC login route handler and
OIDC callback route handler does not include the KENER_BASE_PATH environment
variable prefix, causing incorrect redirect URIs when the application is
deployed with a base path. In both locations where callbackUrl is constructed,
first extract the KENER_BASE_PATH environment variable (defaulting to an empty
string if not set), then include it in the URL construction by concatenating it
between url.origin and the route path. This ensures the OIDC provider redirects
to the correct endpoint regardless of whether a base path is configured.

In `@src/routes/`(account)/account/signin/+page.svelte:
- Around line 83-91: The Button component with href="/account/oidc/login" and
the forgot password link with href="/account/forgot" use hardcoded paths that
don't account for the KENER_BASE_PATH environment variable, causing 404 errors
when a base path is configured. Import the resolve function from "$app/paths" at
the top of the file, then apply it to both href attributes by wrapping the
hardcoded paths with the resolve function, following the clientResolver(resolve,
path) pattern used elsewhere in the codebase.

In `@src/routes/`(manage)/manage/app/oidc/+page.svelte:
- Around line 2-12: Update all the shadcn-svelte UI component import paths in
this file to follow project conventions. Remove the `/index.js` suffix from each
import statement for Button, Input, Label, Switch, Spinner, Badge, Separator,
Card, Table, Select, and AlertDialog, changing them from
`$lib/components/ui/{component-name}/index.js` to just
`$lib/components/ui/{component-name}`. This applies to all import statements
from line 2 through line 12.
- Around line 94-101: The apiCall function returns parsed JSON without checking
the HTTP response status code, allowing callers to continue with invalid data
even when the server returns an error. After the fetch call in the apiCall
function, check the response status (using response.ok or response.status) and
throw an error if the response indicates failure (non-2xx status). Only parse
and return the JSON response if the HTTP status indicates success.

In `@src/routes/`(manage)/manage/app/users/+page.svelte:
- Line 413: The new Auth column added at line 413 increases the table from 6 to
7 columns, but the loading and empty state rows still have colspan set to 6,
breaking table alignment. Find the loading state row and empty state row (likely
Table.Row components with colspan attributes) in the same file and update both
colspan values from 6 to 7 to match the new table column count.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a7e74222-142e-4416-8058-ef39beb021c5

📥 Commits

Reviewing files that changed from the base of the PR and between 6764b47 and 0a9c881.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (20)
  • migrations/20260610120000_add_oidc_support.ts
  • package.json
  • src/lib/allPerms.ts
  • src/lib/server/controllers/controller.ts
  • src/lib/server/controllers/oidcController.ts
  • src/lib/server/controllers/siteDataKeys.ts
  • src/lib/server/db/dbimpl.ts
  • src/lib/server/db/repositories/users.ts
  • src/lib/server/db/repositories/users.ts.save
  • src/lib/server/db/seedSiteData.ts
  • src/lib/server/types/db.ts
  • src/lib/types/site.ts
  • src/routes/(account)/account/oidc/callback/+server.ts
  • src/routes/(account)/account/oidc/login/+server.ts
  • src/routes/(account)/account/signin/+page.server.ts
  • src/routes/(account)/account/signin/+page.svelte
  • src/routes/(manage)/+layout.svelte
  • src/routes/(manage)/manage/api/+server.ts
  • src/routes/(manage)/manage/app/oidc/+page.svelte
  • src/routes/(manage)/manage/app/users/+page.svelte

Comment thread migrations/20260610120000_add_oidc_support.ts Outdated
Comment on lines +24 to +35
export async function GetOidcSettings(): Promise<OidcSettings | null> {
const raw = await GetSiteDataByKey("oidcSettings");
if (!raw) return null;

try {
const settings: OidcSettings = typeof raw === "string" ? JSON.parse(raw) : raw;
if (!settings.enabled) return null;
return settings;
} catch {
return null;
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | 💤 Low value

Potential double JSON.parse when data_type is "object".

GetSiteDataByKey already parses JSON when data_type == "object" (see siteDataController.ts:146-155). If oidcSettings is stored with data_type: "object", raw will already be an object, not a string. The current code handles this correctly with the typeof raw === "string" check on line 29, so no bug exists—but the string branch may be dead code.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/server/controllers/oidcController.ts` around lines 24 - 35, In the
GetOidcSettings function, remove the unnecessary `typeof raw === "string"` type
guard check on line 29 since GetSiteDataByKey already returns parsed JSON
objects when data_type is "object". Simply assign raw directly to settings
without the conditional JSON.parse, as raw will always be an object and the
string branch is dead code.

Comment thread src/lib/server/controllers/oidcController.ts
Comment thread src/lib/server/db/repositories/users.ts
Comment thread src/routes/(account)/account/oidc/callback/+server.ts Outdated
Comment thread src/routes/(account)/account/oidc/login/+server.ts Outdated
Comment thread src/routes/(account)/account/signin/+page.svelte
Comment on lines +2 to +12
import { Button } from "$lib/components/ui/button/index.js";
import { Input } from "$lib/components/ui/input/index.js";
import { Label } from "$lib/components/ui/label/index.js";
import { Switch } from "$lib/components/ui/switch/index.js";
import { Spinner } from "$lib/components/ui/spinner/index.js";
import { Badge } from "$lib/components/ui/badge/index.js";
import { Separator } from "$lib/components/ui/separator/index.js";
import * as Card from "$lib/components/ui/card/index.js";
import * as Table from "$lib/components/ui/table/index.js";
import * as Select from "$lib/components/ui/select/index.js";
import * as AlertDialog from "$lib/components/ui/alert-dialog/index.js";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Use route-level shadcn import paths required by project conventions.

These imports should use $lib/components/ui/{component-name} instead of /index.js paths.

Example import-path adjustments
-import { Button } from "$lib/components/ui/button/index.js";
-import { Input } from "$lib/components/ui/input/index.js";
-import { Label } from "$lib/components/ui/label/index.js";
+import { Button } from "$lib/components/ui/button";
+import { Input } from "$lib/components/ui/input";
+import { Label } from "$lib/components/ui/label";

As per coding guidelines, src/routes/**/*.svelte: Import shadcn-svelte UI components from "$lib/components/ui/{component-name}".

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { Button } from "$lib/components/ui/button/index.js";
import { Input } from "$lib/components/ui/input/index.js";
import { Label } from "$lib/components/ui/label/index.js";
import { Switch } from "$lib/components/ui/switch/index.js";
import { Spinner } from "$lib/components/ui/spinner/index.js";
import { Badge } from "$lib/components/ui/badge/index.js";
import { Separator } from "$lib/components/ui/separator/index.js";
import * as Card from "$lib/components/ui/card/index.js";
import * as Table from "$lib/components/ui/table/index.js";
import * as Select from "$lib/components/ui/select/index.js";
import * as AlertDialog from "$lib/components/ui/alert-dialog/index.js";
import { Button } from "$lib/components/ui/button";
import { Input } from "$lib/components/ui/input";
import { Label } from "$lib/components/ui/label";
import { Switch } from "$lib/components/ui/switch";
import { Spinner } from "$lib/components/ui/spinner";
import { Badge } from "$lib/components/ui/badge";
import { Separator } from "$lib/components/ui/separator";
import * as Card from "$lib/components/ui/card";
import * as Table from "$lib/components/ui/table";
import * as Select from "$lib/components/ui/select";
import * as AlertDialog from "$lib/components/ui/alert-dialog";
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/routes/`(manage)/manage/app/oidc/+page.svelte around lines 2 - 12, Update
all the shadcn-svelte UI component import paths in this file to follow project
conventions. Remove the `/index.js` suffix from each import statement for
Button, Input, Label, Switch, Spinner, Badge, Separator, Card, Table, Select,
and AlertDialog, changing them from
`$lib/components/ui/{component-name}/index.js` to just
`$lib/components/ui/{component-name}`. This applies to all import statements
from line 2 through line 12.

Source: Coding guidelines

Comment thread src/routes/(manage)/manage/app/oidc/+page.svelte Outdated
Comment thread src/routes/(manage)/manage/app/users/+page.svelte
@Plattenspatz

Copy link
Copy Markdown
Author

@rajnandan1
Thanks for the detailed review! I'm working through all the feedback and will push an update shortly.
Already done:

  • All blockers (cache poisoning, cache invalidation, KENER_BASE_PATH, colspan, users.ts.save)
  • All security items (masked secret, lockout recovery via KENER_FORCE_LOCAL_LOGIN env var, auto_create_users default)
  • All minor items (dead code, email/name sync, migration cleanup, validation, constants)
  • Bot feedback from CodeRabbit and Greptile (getUsersByRoleId columns, access_token guard, apiCall error handling, OidcSettings shared import)
  • Docs page added

Just running final tests before pushing. Update coming soon.

@Plattenspatz

Copy link
Copy Markdown
Author

@rajnandan1 Thanks for the detailed review! I'm working through all the feedback and will push an update shortly. Already done:

* All blockers (cache poisoning, cache invalidation, KENER_BASE_PATH, colspan, users.ts.save)

* All security items (masked secret, lockout recovery via KENER_FORCE_LOCAL_LOGIN env var, auto_create_users default)

* All minor items (dead code, email/name sync, migration cleanup, validation, constants)

* Bot feedback from CodeRabbit and Greptile (getUsersByRoleId columns, access_token guard, apiCall error handling, OidcSettings shared import)

* Docs page added

Just running final tests before pushing. Update coming soon.

Done.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/routes/(account)/account/signin/+page.svelte (1)

162-169: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Forgot password link needs base path resolution.

The href="/account/forgot" is hardcoded and will break when KENER_BASE_PATH is configured. Apply resolve() consistently as done for the OIDC login link.

Suggested fix
                   <Button
                     variant="link"
                     size="sm"
                     class="text-muted-foreground absolute top-0 right-0 h-auto p-0 text-xs"
-                    href="/account/forgot"
+                    href={resolve("/account/forgot")}
                   >
                     Forgot?
                   </Button>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/routes/`(account)/account/signin/+page.svelte around lines 162 - 169, The
href attribute in the Button component that displays "Forgot?" text is hardcoded
to "/account/forgot" and does not account for the KENER_BASE_PATH configuration.
Apply the resolve() function to the href value in this Button component,
consistent with how it is applied to the OIDC login link elsewhere in the page,
to ensure the path is correctly resolved when a base path is configured.
♻️ Duplicate comments (1)
src/lib/server/controllers/oidcController.ts (1)

137-141: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

Remove redundant non-null assertion.

The ! on tokens.access_token! at line 140 is unnecessary since the guard on lines 137-139 ensures access_token exists.

Suggested fix
     if (!tokens.access_token) {
       throw new Error("No email in ID token and no access_token available for userinfo lookup");
     }
-    const userinfo = await client.fetchUserInfo(config, tokens.access_token!, sub);
+    const userinfo = await client.fetchUserInfo(config, tokens.access_token, sub);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/server/controllers/oidcController.ts` around lines 137 - 141, In the
oidcController's token handling block, the guard condition at lines 137-139
already validates that tokens.access_token exists by throwing an error if it is
falsy. Therefore, the non-null assertion operator (!) on tokens.access_token in
the subsequent client.fetchUserInfo call is redundant. Remove the exclamation
mark from tokens.access_token! in the client.fetchUserInfo invocation to clean
up the code, as the guard ensures the value is defined and TypeScript will
recognize this.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/lib/server/tool.ts`:
- Around line 538-541: The GenerateRandomHexString function generates excessive
random bytes and discards half of them through slicing. Instead of generating
length random bytes, calculate the minimum bytes needed to produce length hex
characters by using Math.ceil(length / 2) as the argument to crypto.randomBytes.
This ensures only the necessary entropy is generated while still producing the
correct number of hex characters after conversion and slicing.

In `@src/routes/`(account)/account/signin/+page.server.ts:
- Around line 11-13: The file has duplicate imports from $lib/global-constants
on lines 11 and 13, importing the module as both `constants` and `GC`. Remove
the duplicate import statement on line 11 that imports as `constants`, keeping
only the import on line 13 that uses the `GC` alias per coding guidelines. Then
locate and update any usage of the `constants` alias throughout the file
(including line 57) to use `GC` instead.

In `@src/routes/`(manage)/manage/app/oidc/+page.svelte:
- Around line 37-42: The RoleRecord interface defined locally in this component
duplicates the type definition that already exists in $lib/server/types/db.js.
Remove the local RoleRecord interface definition and instead import it from the
shared database types file using an import statement at the top of the file.
This ensures a single source of truth and prevents type drift between the
component and the shared types.

---

Outside diff comments:
In `@src/routes/`(account)/account/signin/+page.svelte:
- Around line 162-169: The href attribute in the Button component that displays
"Forgot?" text is hardcoded to "/account/forgot" and does not account for the
KENER_BASE_PATH configuration. Apply the resolve() function to the href value in
this Button component, consistent with how it is applied to the OIDC login link
elsewhere in the page, to ensure the path is correctly resolved when a base path
is configured.

---

Duplicate comments:
In `@src/lib/server/controllers/oidcController.ts`:
- Around line 137-141: In the oidcController's token handling block, the guard
condition at lines 137-139 already validates that tokens.access_token exists by
throwing an error if it is falsy. Therefore, the non-null assertion operator (!)
on tokens.access_token in the subsequent client.fetchUserInfo call is redundant.
Remove the exclamation mark from tokens.access_token! in the
client.fetchUserInfo invocation to clean up the code, as the guard ensures the
value is defined and TypeScript will recognize this.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ae5f68ee-d9ef-423f-bc17-fab2c0cfdeb5

📥 Commits

Reviewing files that changed from the base of the PR and between 0a9c881 and 47fbbe7.

📒 Files selected for processing (18)
  • .gitignore
  • migrations/20260610120000_add_oidc_support.ts
  • src/lib/allPerms.ts
  • src/lib/global-constants.ts
  • src/lib/server/controllers/oidcController.ts
  • src/lib/server/db/dbimpl.ts
  • src/lib/server/db/repositories/users.ts
  • src/lib/server/db/seedSiteData.ts
  • src/lib/server/tool.ts
  • src/routes/(account)/account/oidc/callback/+server.ts
  • src/routes/(account)/account/oidc/login/+server.ts
  • src/routes/(account)/account/signin/+page.server.ts
  • src/routes/(account)/account/signin/+page.svelte
  • src/routes/(docs)/docs.json
  • src/routes/(docs)/docs/content/v4/oidc.md
  • src/routes/(manage)/manage/api/+server.ts
  • src/routes/(manage)/manage/app/oidc/+page.svelte
  • src/routes/(manage)/manage/app/users/+page.svelte

Comment thread src/lib/server/tool.ts
Comment thread src/routes/(account)/account/signin/+page.server.ts Outdated
Comment on lines +37 to +42
interface RoleRecord {
id: string;
role_name: string;
readonly: number;
status: string;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Import RoleRecord from DB types instead of redefining it.

The RoleRecord interface duplicates the type definition from $lib/server/types/db.js. Import it instead to prevent drift.

♻️ Refactor to use imported type
  import clientResolver from "$lib/client/resolver.js";
  import type { OidcSettings } from "$lib/types/site.js";
+ import type { RoleRecord } from "$lib/server/types/db.js";

  // ============ Types ============

  interface GroupRoleMapping {
    id: number;
    oidc_group: string;
    role_id: string;
    created_at: string;
    updated_at: string;
  }

- interface RoleRecord {
-   id: string;
-   role_name: string;
-   readonly: number;
-   status: string;
- }
-
  // ============ State ============
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/routes/`(manage)/manage/app/oidc/+page.svelte around lines 37 - 42, The
RoleRecord interface defined locally in this component duplicates the type
definition that already exists in $lib/server/types/db.js. Remove the local
RoleRecord interface definition and instead import it from the shared database
types file using an import statement at the top of the file. This ensures a
single source of truth and prevents type drift between the component and the
shared types.

- Fix test connection cache poisoning (local config for test)
- Cache key includes client_id/secret, server-side invalidation
- Mask client_secret on read, preserve on unchanged save
- Add KENER_BASE_PATH to all redirect URIs
- Add KENER_FORCE_LOCAL_LOGIN env var for lockout recovery
- Remove dead code (updateUserOidcSub, users.ts.save)
- Sync email/name from IdP on subsequent logins
- Server-side validation for group-role mappings
- Move generateRandomString to tool.ts, oidc to global-constants
- Fix colspan, strict equality, RoleRecord import, callback order
- Guard against missing access_token
- Fix getUsersByRoleId missing auth_provider/oidc_sub columns
- Default auto_create_users to false
- Add OIDC documentation page
- Add *.save to .gitignore
@Plattenspatz
Plattenspatz force-pushed the feature/oidc-support branch from 47fbbe7 to c96fb89 Compare June 23, 2026 10:49

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/lib/server/controllers/oidcController.ts`:
- Around line 10-15: The imports in oidcController.ts do not follow the
project's established conventions for server-side code. Replace the relative
path import of db from "../db/db.js" with the standardized alias path import db
from "$lib/server/db/db" to maintain consistency across server operations.
Additionally, replace the $lib alias import for global constants (GC from
"$lib/global-constants.js") with a relative path import
(../../global-constants.js) as per the server-layer guidelines. These changes
align the file with the documented import conventions for src/lib/server/**/*.ts
files.
- Around line 230-235: Before calling db.updateUserProfile with the
oidcData.email, add a guard to check if that email is already owned by another
user account, similar to the validation done in the provisioning path. If
oidcData.email is owned by a different user (compare against user.id), either
skip updating the email field or handle the conflict appropriately to prevent
duplicate account identifiers and maintain the "no account merging" contract.
This check should happen before the updateUserProfile call and should protect
against the case where the IdP's email for the user changes to point to another
account's email.

In `@src/routes/`(account)/account/signin/+page.server.ts:
- Around line 59-71: The current code checks if a user exists in the database
before validating if local login is globally disabled, which enables account
enumeration attacks since different error messages are returned for non-existent
vs existing users. Move the OIDC local-login validation check (the condition
checking oidcSettings.allow_local_login and forceLocalLogin) to execute before
the GetUserByEmail call, so that the disabled local login error is returned
consistently regardless of whether the email exists in the database.

In `@src/routes/`(manage)/manage/api/+server.ts:
- Around line 692-699: The code in the getOidcSettingsMasked action is mutating
the settings object returned by GetSiteDataByKey and the masking logic likely
treats any falsy client_secret as missing (meaning it preserves the old secret),
when instead it should only preserve the secret if the field is completely
omitted. Create a clone of the settings object before applying the MaskString
operation on the client_secret field, and modify the logic that handles
client_secret updates (around line 723) to check for undefined/omitted fields
rather than falsy values, so that an empty string is treated as an intentional
clear of the secret rather than a request to keep the existing one.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 0dba185a-576a-4aba-8cfa-173f03fda8ac

📥 Commits

Reviewing files that changed from the base of the PR and between 47fbbe7 and c96fb89.

📒 Files selected for processing (18)
  • .gitignore
  • migrations/20260610120000_add_oidc_support.ts
  • src/lib/allPerms.ts
  • src/lib/global-constants.ts
  • src/lib/server/controllers/oidcController.ts
  • src/lib/server/db/dbimpl.ts
  • src/lib/server/db/repositories/users.ts
  • src/lib/server/db/seedSiteData.ts
  • src/lib/server/tool.ts
  • src/routes/(account)/account/oidc/callback/+server.ts
  • src/routes/(account)/account/oidc/login/+server.ts
  • src/routes/(account)/account/signin/+page.server.ts
  • src/routes/(account)/account/signin/+page.svelte
  • src/routes/(docs)/docs.json
  • src/routes/(docs)/docs/content/v4/oidc.md
  • src/routes/(manage)/manage/api/+server.ts
  • src/routes/(manage)/manage/app/oidc/+page.svelte
  • src/routes/(manage)/manage/app/users/+page.svelte

Comment on lines +10 to +15
import db from "../db/db.js";
import { GenerateToken, CookieConfig } from "./commonController.js";
import type { OidcSettings } from "$lib/types/site.js";
import type { UserRecordPublic } from "../types/db.js";
import { GetSiteDataByKey } from "./siteDataController.js";
import GC from "$lib/global-constants.js";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Align server imports with the project import conventions.

oidcController.ts is under src/lib/server/**/*.ts, but Line 10 imports the db singleton by relative path and Line 15 imports global constants through $lib. Use the documented paths for this layer.

Proposed import adjustment
-import db from "../db/db.js";
+import db from "$lib/server/db/db";
 import { GenerateToken, CookieConfig } from "./commonController.js";
 import type { OidcSettings } from "$lib/types/site.js";
 import type { UserRecordPublic } from "../types/db.js";
 import { GetSiteDataByKey } from "./siteDataController.js";
-import GC from "$lib/global-constants.js";
+import GC from "../../global-constants.js";

As per coding guidelines, "src/lib/server/**/*.ts: Use the db singleton instance via import db from "$lib/server/db/db" for all database operations" and "src/lib/server/**/*.ts: Import status constants using relative path from src/lib/global-constants.js in server code."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import db from "../db/db.js";
import { GenerateToken, CookieConfig } from "./commonController.js";
import type { OidcSettings } from "$lib/types/site.js";
import type { UserRecordPublic } from "../types/db.js";
import { GetSiteDataByKey } from "./siteDataController.js";
import GC from "$lib/global-constants.js";
import db from "$lib/server/db/db";
import { GenerateToken, CookieConfig } from "./commonController.js";
import type { OidcSettings } from "$lib/types/site.js";
import type { UserRecordPublic } from "../types/db.js";
import { GetSiteDataByKey } from "./siteDataController.js";
import GC from "../../global-constants.js";
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/server/controllers/oidcController.ts` around lines 10 - 15, The
imports in oidcController.ts do not follow the project's established conventions
for server-side code. Replace the relative path import of db from "../db/db.js"
with the standardized alias path import db from "$lib/server/db/db" to maintain
consistency across server operations. Additionally, replace the $lib alias
import for global constants (GC from "$lib/global-constants.js") with a relative
path import (../../global-constants.js) as per the server-layer guidelines.
These changes align the file with the documented import conventions for
src/lib/server/**/*.ts files.

Source: Coding guidelines

Comment on lines +230 to +235
// Sync roles and update profile data from the IdP
await SyncOidcUserRoles(user.id, oidcData.groups, settings);
await db.updateUserProfile(user.id, {
name: oidcData.name,
email: oidcData.email,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Check email ownership before syncing an existing OIDC profile.

The provisioning path rejects oidcData.email when another account already owns it, but the existing-user sync path updates the email without the same guard. If the IdP email changes to another user's email, this can create duplicate account identifiers or fail on a DB constraint instead of preserving the “no account merging” contract.

Proposed guard before `updateUserProfile`
   // Sync roles and update profile data from the IdP
   await SyncOidcUserRoles(user.id, oidcData.groups, settings);
+  const existingByEmail = await db.getUserByEmail(oidcData.email);
+  if (existingByEmail && existingByEmail.id !== user.id) {
+    throw new Error(
+      `Another account with the email "${oidcData.email}" already exists. ` +
+      "OIDC and local accounts are kept separate. " +
+      "Please contact an administrator.",
+    );
+  }
   await db.updateUserProfile(user.id, {
     name: oidcData.name,
     email: oidcData.email,
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/server/controllers/oidcController.ts` around lines 230 - 235, Before
calling db.updateUserProfile with the oidcData.email, add a guard to check if
that email is already owned by another user account, similar to the validation
done in the provisioning path. If oidcData.email is owned by a different user
(compare against user.id), either skip updating the email field or handle the
conflict appropriately to prevent duplicate account identifiers and maintain the
"no account merging" contract. This check should happen before the
updateUserProfile call and should protect against the case where the IdP's email
for the user changes to point to another account's email.

Comment on lines 59 to +71
const userDB = await GetUserByEmail(email);
if (!userDB) {
return fail(401, { error: "User does not exist", values: { email } });
}
// Local login can be enabled by setting Env-Variable "KENER_FORCE_LOCAL_LOGIN" == "true".
// This prevents lockout when the IdP is misconfigured or unreachable.
const forceLocalLogin = process.env.KENER_FORCE_LOCAL_LOGIN === "true";
if (oidcSettings && !oidcSettings.allow_local_login && !forceLocalLogin) {
return fail(403, {
error: "Local login is disabled. Please use SSO.",
values: { email },
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Enforce disabled local login before looking up the user.

When local login is disabled, this still returns different responses for unknown vs existing emails, leaving the password endpoint usable for account enumeration. Move the global OIDC local-login gate before GetUserByEmail().

🔒 Proposed fix
-    const userDB = await GetUserByEmail(email);
-    if (!userDB) {
-      return fail(401, { error: "User does not exist", values: { email } });
-    }
     // Local login can be enabled by setting Env-Variable "KENER_FORCE_LOCAL_LOGIN" == "true".
     // This prevents lockout when the IdP is misconfigured or unreachable.
     const forceLocalLogin = process.env.KENER_FORCE_LOCAL_LOGIN === "true";
     if (oidcSettings && !oidcSettings.allow_local_login && !forceLocalLogin) {
       return fail(403, {
         error: "Local login is disabled. Please use SSO.",
         values: { email },
       });
     }
+
+    const userDB = await GetUserByEmail(email);
+    if (!userDB) {
+      return fail(401, { error: "User does not exist", values: { email } });
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/routes/`(account)/account/signin/+page.server.ts around lines 59 - 71,
The current code checks if a user exists in the database before validating if
local login is globally disabled, which enables account enumeration attacks
since different error messages are returned for non-existent vs existing users.
Move the OIDC local-login validation check (the condition checking
oidcSettings.allow_local_login and forceLocalLogin) to execute before the
GetUserByEmail call, so that the disabled local login error is returned
consistently regardless of whether the email exists in the database.

Comment on lines +692 to +699
} else if (action == "getOidcSettingsMasked") {
const raw = await GetSiteDataByKey("oidcSettings");
if (raw && typeof raw === "object") {
const settings = raw as Record<string, unknown>;
if (settings.client_secret && typeof settings.client_secret === "string") {
settings.client_secret = MaskString(settings.client_secret);
}
resp = settings;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Preserve the secret only when the field is omitted.

Line 723 treats any falsy client_secret as missing, so saving client_secret: "" silently keeps the old secret. The UI only deletes the field when unchanged, so an empty string should be respected as an intentional clear. Also mask a cloned settings object to avoid mutating the value returned by GetSiteDataByKey.

Proposed fix
     } else if (action == "getOidcSettingsMasked") {
       const raw = await GetSiteDataByKey("oidcSettings");
       if (raw && typeof raw === "object") {
-        const settings = raw as Record<string, unknown>;
+        const settings = { ...(raw as Record<string, unknown>) };
         if (settings.client_secret && typeof settings.client_secret === "string") {
           settings.client_secret = MaskString(settings.client_secret);
         }
         resp = settings;
@@
       if (key === "oidcSettings" && typeof element === "string") {
         try {
           const newSettings = JSON.parse(element);
-          if (!newSettings.client_secret) {
+          if (!Object.prototype.hasOwnProperty.call(newSettings, "client_secret")) {
             const existing = await GetSiteDataByKey("oidcSettings");
             if (existing && typeof existing === "object") {
               newSettings.client_secret = (existing as Record<string, unknown>).client_secret;
               element = JSON.stringify(newSettings);
             }

Also applies to: 719-728

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/routes/`(manage)/manage/api/+server.ts around lines 692 - 699, The code
in the getOidcSettingsMasked action is mutating the settings object returned by
GetSiteDataByKey and the masking logic likely treats any falsy client_secret as
missing (meaning it preserves the old secret), when instead it should only
preserve the secret if the field is completely omitted. Create a clone of the
settings object before applying the MaskString operation on the client_secret
field, and modify the logic that handles client_secret updates (around line 723)
to check for undefined/omitted fields rather than falsy values, so that an empty
string is treated as an intentional clear of the secret rather than a request to
keep the existing one.

@Plattenspatz

Copy link
Copy Markdown
Author

Thanks for the detailed review! I've addressed all feedback in the second commit:
Blockers:

  • Removed users.ts.save, added *.save to .gitignore
  • TestOidcConnection now uses a local config — never touches the shared cache
  • Cache key includes issuer_url, client_id, and client_secret; cache is cleared server-side in storeSiteData when oidcSettings is saved
  • KENER_BASE_PATH included in all redirect URIs, signin button uses resolve()
  • colspan updated to 7

Security:

  • client_secret is masked on read via getOidcSettingsMasked; only overwritten when the admin provides a new value
  • Added KENER_FORCE_LOCAL_LOGIN env var as emergency override when the IdP is down
  • auto_create_users defaults to false

Minor:

  • Removed dead code (updateUserOidcSub)
  • Email/name synced from IdP on every login via new updateUserProfile method
  • Migration no longer touches password_hash — OIDC users store ""
  • Server-side validation for group-role mappings (empty group, invalid role_id)
  • RoleRecord defined as local interface, OidcSettings imported from shared types
  • generateRandomString moved to tool.ts, "oidc" moved to global-constants
  • Callback checks IdP error before cookies
  • Strict equality everywhere, consolidated duplicate constants/GC import
  • Fixed getUsersByRoleId missing auth_provider/oidc_sub columns
  • apiCall handles non-2xx responses
  • Guard against missing access_token before fetchUserInfo
  • resolve() applied to forgot password link

Added:

  • Documentation page under src/routes/(docs)/docs/content/v4/oidc.md

Ready for re-review when you have time.

@lamergameryt

Copy link
Copy Markdown

Commenting here to just say that this is a crucial (and blocking) feature for us. We want to test this out once OIDC support is available.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants