From 9165dbed1dd9b30b075d74c8a718a1a4673f7e95 Mon Sep 17 00:00:00 2001 From: neon <18559658+neonsolstice@users.noreply.github.com> Date: Wed, 2 Sep 2026 05:04:18 +0000 Subject: [PATCH] feat(auth): add SSO-only authentication mode Require an enabled OIDC-linked superuser before disabling password flows. Track session authentication methods and fail closed across the API, web client, WebSockets, and administrative mutations. Closes #1002 --- .env.example | 4 + README.md | 2 + client/src/features/admin/UserFormDrawer.vue | 14 +- client/src/features/admin/UsersPage.vue | 14 +- .../admin/__tests__/UserFormDrawer.spec.ts | 2 +- .../admin/__tests__/UsersPage.spec.ts | 19 +- .../admin/components/UserAttentionBand.vue | 7 +- .../admin/components/UserRosterTable.vue | 4 + client/src/features/auth/LoginPage.vue | 30 +- .../features/auth/__tests__/LoginPage.spec.ts | 30 +- .../auth/composables/useLoginOptions.ts | 34 + .../src/features/auth/composables/useOidc.ts | 12 +- .../src/features/settings/AccountSettings.vue | 29 +- client/src/locales/en.json | 3 + client/src/main.ts | 3 +- .../src/router/__tests__/auth.guard.spec.ts | 48 +- client/src/router/guards/auth.guard.ts | 22 +- docker-compose.yml | 1 + docs/DEVELOPMENT.md | 1 + packages/types/src/auth.ts | 18 + server/.env.example | 4 + server/src/common/common.module.ts | 5 +- .../src/common/guards/jwt-auth.guard.test.ts | 10 + server/src/common/guards/jwt-auth.guard.ts | 3 +- .../authentication-policy.service.test.ts | 53 + .../services/authentication-policy.service.ts | 101 + server/src/common/test-utils/make-user.ts | 1 + server/src/common/types/request-user.ts | 2 + server/src/config/config.test.ts | 13 + server/src/config/config.ts | 1 + server/src/config/env.validation.test.ts | 10 + server/src/config/env.validation.ts | 1 + .../0085_add_authentication_method.sql | 2 + .../src/db/migrations/meta/0085_snapshot.json | 18358 ++++++++++++++++ server/src/db/migrations/meta/_journal.json | 7 + server/src/db/schema/auth.ts | 2 + .../app-settings/app-settings.module.ts | 3 + .../oidc-provider.repository.test.ts | 81 + .../app-settings/oidc-provider.repository.ts | 45 +- .../src/modules/auth/auth.controller.test.ts | 4 +- server/src/modules/auth/auth.controller.ts | 8 +- server/src/modules/auth/auth.service.test.ts | 104 +- server/src/modules/auth/auth.service.ts | 111 +- .../src/modules/auth/dto/oidc-unlink.dto.ts | 5 +- server/src/modules/auth/jwt.strategy.test.ts | 11 +- server/src/modules/auth/jwt.strategy.ts | 5 +- .../modules/auth/magic-link.service.test.ts | 2 +- server/src/modules/auth/magic-link.service.ts | 4 +- .../modules/auth/oidc/oidc.service.test.ts | 28 + server/src/modules/auth/oidc/oidc.service.ts | 34 +- .../authors/author-enrichment.gateway.test.ts | 2 +- .../authors/author-enrichment.gateway.ts | 6 +- .../book-dock/book-dock.gateway.test.ts | 2 +- .../modules/book-dock/book-dock.gateway.ts | 6 +- .../book-metadata-fetch.gateway.ts | 6 +- .../book-request/book-request.gateway.ts | 6 +- .../migration-progress.gateway.test.ts | 2 +- .../migration/migration-progress.gateway.ts | 6 +- .../src/modules/scanner/scan.gateway.test.ts | 2 +- server/src/modules/scanner/scan.gateway.ts | 5 +- .../user/oidc-identity.repository.test.ts | 43 + .../modules/user/oidc-identity.repository.ts | 29 +- server/src/modules/user/user.module.ts | 3 +- .../src/modules/user/user.repository.test.ts | 10 +- server/src/modules/user/user.repository.ts | 30 +- server/src/modules/user/user.service.test.ts | 13 +- server/src/modules/user/user.service.ts | 7 + .../route-inventory/manifest.json | 4 +- .../route-inventory/routes-01.json | 12 + 69 files changed, 19353 insertions(+), 156 deletions(-) create mode 100644 client/src/features/auth/composables/useLoginOptions.ts create mode 100644 server/src/common/services/authentication-policy.service.test.ts create mode 100644 server/src/common/services/authentication-policy.service.ts create mode 100644 server/src/db/migrations/0085_add_authentication_method.sql create mode 100644 server/src/db/migrations/meta/0085_snapshot.json create mode 100644 server/src/modules/app-settings/oidc-provider.repository.test.ts create mode 100644 server/src/modules/user/oidc-identity.repository.test.ts diff --git a/.env.example b/.env.example index 4fcf6ea56..585ffcc23 100644 --- a/.env.example +++ b/.env.example @@ -64,6 +64,10 @@ NODE_MAX_OLD_SPACE_SIZE=2048 # Default false. Enable only in trusted self-hosted networks. # OIDC_ALLOW_LOCAL_ISSUERS=false +# Disable username/password sign-in after at least one active administrator has linked an enabled +# OIDC provider. Set false and restart to recover access if the identity provider is unavailable. +# DISABLE_LOCAL_AUTH=false + # Optional: allow Cloudflare Web Analytics beacon when using automatic setup. # Keep unset/false to preserve strict CSP (`script-src 'self'`). # CSP_ALLOW_CLOUDFLARE_INSIGHTS=false diff --git a/README.md b/README.md index b823fed1a..898307823 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,8 @@ docker compose up -d Open `http://your-server-ip:3000` and complete setup using your `SETUP_BOOTSTRAP_TOKEN`. +After configuring OIDC and linking at least one active administrator, you can set `DISABLE_LOCAL_AUTH=true` and restart BookOrbit to remove and reject password sign-in. BookOrbit refuses to start if that would leave no usable OIDC administrator. Set it back to `false` and restart to recover access during an identity-provider outage. + For the full installation guide including reverse proxy setup, file permissions on NAS, external databases, and environment variable reference, see **[bookorbit.app/installation](https://bookorbit.app/installation)**. ## KOReader Plugin diff --git a/client/src/features/admin/UserFormDrawer.vue b/client/src/features/admin/UserFormDrawer.vue index 4d975c020..7367991ca 100644 --- a/client/src/features/admin/UserFormDrawer.vue +++ b/client/src/features/admin/UserFormDrawer.vue @@ -37,6 +37,7 @@ const props = defineProps<{ canManageSuperuser?: boolean currentUserId?: number canDelete?: boolean + passwordLoginEnabled?: boolean }>() const emit = defineEmits<{ @@ -203,6 +204,14 @@ watch( }, ) +watch( + [() => props.user, () => props.passwordLoginEnabled], + ([user, passwordLoginEnabled]) => { + if (!user && passwordLoginEnabled !== true) isSharedAccount.value = true + }, + { immediate: true }, +) + function selectSection(section: UserFormSection) { activeSection.value = section } @@ -339,7 +348,10 @@ async function handleSubmit() {
-