diff --git a/.env.example b/.env.example index 63817705..ac0229ce 100644 --- a/.env.example +++ b/.env.example @@ -113,6 +113,12 @@ OIDC_WELL_KNOWN_URL= # Required for some providers to link with existing accounts, make sure you trust your provider to properly verify email addresses # OIDC_ALLOW_DANGEROUS_EMAIL_LINKING=1 +# Enable PKCE (Proof Key for Code Exchange) for the OIDC provider +# OIDC_PKCE_ENABLED=1 + +# Set the ID token signed response algorithm if your provider requires a specific algorithm (e.g. ES256, RS256) +# OIDC_ID_TOKEN_SIGNED_RESPONSE_ALG= + # Push notification, Web Push: https://www.npmjs.com/package/web-push # generate web push keys using this command: npx web-push generate-vapid-keys --json # or use the online tool: https://vapidkeys.com/ diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 65b6f29e..86332f23 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -91,6 +91,8 @@ Used for magic-link login and invites. - `OIDC_CLIENT_SECRET` - `OIDC_WELL_KNOWN_URL`: OpenID well-known discovery URL. - `OIDC_ALLOW_DANGEROUS_EMAIL_LINKING`: Optional flag to allow email-based account linking. +- `OIDC_PKCE_ENABLED`: Optional flag to enable PKCE (Proof Key for Code Exchange). +- `OIDC_ID_TOKEN_SIGNED_RESPONSE_ALG`: Optional ID token signed response algorithm (e.g. `ES256`, `RS256`). ### Web push notifications @@ -144,6 +146,20 @@ OIDC_CLIENT_SECRET="" OIDC_WELL_KNOWN_URL="https://keycloak.example.com/realms/My_Realm/.well-known/openid-configuration" ``` +### OIDC (Kanidm) + +```bash +DATABASE_URL="postgresql://postgres:strong-password@localhost:5432/splitpro" +NEXTAUTH_SECRET="" +NEXTAUTH_URL="https://splitpro.example.com" +OIDC_NAME="kanidm" +OIDC_CLIENT_ID="" +OIDC_CLIENT_SECRET="" +OIDC_WELL_KNOWN_URL="https://kanidm.example.com/oauth2/openid//.well-known/openid-configuration" +OIDC_PKCE_ENABLED=1 +OIDC_ID_TOKEN_SIGNED_RESPONSE_ALG=ES256 +``` + ## Security notes - Rotate `NEXTAUTH_SECRET` if it is ever exposed. diff --git a/src/env.ts b/src/env.ts index 516381af..4be046bc 100644 --- a/src/env.ts +++ b/src/env.ts @@ -71,6 +71,8 @@ export const env = createEnv({ OIDC_CLIENT_SECRET: z.string().optional(), OIDC_WELL_KNOWN_URL: z.string().optional(), OIDC_ALLOW_DANGEROUS_EMAIL_LINKING: z.boolean().optional(), + OIDC_PKCE_ENABLED: z.boolean().optional().default(false), + OIDC_ID_TOKEN_SIGNED_RESPONSE_ALG: z.string().optional(), UPLOAD_MAX_FILE_SIZE_MB: z.coerce.number().int().positive().default(10), }, @@ -140,6 +142,8 @@ export const env = createEnv({ OIDC_CLIENT_SECRET: process.env.OIDC_CLIENT_SECRET, OIDC_WELL_KNOWN_URL: process.env.OIDC_WELL_KNOWN_URL, OIDC_ALLOW_DANGEROUS_EMAIL_LINKING: Boolean(process.env.OIDC_ALLOW_DANGEROUS_EMAIL_LINKING), + OIDC_PKCE_ENABLED: Boolean(process.env.OIDC_PKCE_ENABLED), + OIDC_ID_TOKEN_SIGNED_RESPONSE_ALG: process.env.OIDC_ID_TOKEN_SIGNED_RESPONSE_ALG, UPLOAD_MAX_FILE_SIZE_MB: process.env.UPLOAD_MAX_FILE_SIZE_MB ? Number(process.env.UPLOAD_MAX_FILE_SIZE_MB) : 10, diff --git a/src/server/auth.ts b/src/server/auth.ts index dff93589..14ddef25 100644 --- a/src/server/auth.ts +++ b/src/server/auth.ts @@ -1,4 +1,5 @@ import { PrismaAdapter } from '@next-auth/prisma-adapter'; +import { Prisma } from '@prisma/client'; import { type GetServerSidePropsContext } from 'next'; import { type DefaultSession, type NextAuthOptions, type User, getServerSession } from 'next-auth'; import { type Adapter, type AdapterAccount, type AdapterUser } from 'next-auth/adapters'; @@ -71,32 +72,21 @@ const SplitProPrismaAdapter = (...args: Parameters): Adapt return prismaCreateUser(user); }, linkAccount: async (account: AdapterAccount) => { - // oxlint-disable-next-line typescript/no-unsafe-assignment const originalLinkAccount = prismaAdapter.linkAccount; - if (!originalLinkAccount) { throw new Error('Adapter is missing the linkAccount method.'); } - - // Keycloak and Gitlab provide some non-standard fields that do not exist in the prisma schema. + // OIDC providers can provide non-standard fields that do not exist in the prisma schema. // We strip them out before passing them on to the original adapter. - if (account.provider === 'keycloak') { - const { - 'not-before-policy': _notBeforePolicy, - refresh_expires_in: _refresh_expires_in, - ...standardAccountData - } = account as AdapterAccount & Record; + const knownAccountFields = new Set(Object.values(Prisma.AccountScalarFieldEnum)); - return originalLinkAccount(standardAccountData as AdapterAccount); - } else if (account.provider === 'gitlab') { - const { created_at: _createdAt, ...standardAccountData } = account as AdapterAccount & - Record; - - return originalLinkAccount(standardAccountData as AdapterAccount); - } + const sanitised = Object.fromEntries( + Object.entries(account as Record).filter(([k]) => + knownAccountFields.has(k), + ), + ) as AdapterAccount; - // Default: proceed directly - return originalLinkAccount(account); + return originalLinkAccount(sanitised); }, } as Adapter; }; @@ -253,6 +243,10 @@ function getProviders() { type: 'oauth', wellKnown: env.OIDC_WELL_KNOWN_URL, authorization: { params: { scope: 'openid email profile' } }, + checks: env.OIDC_PKCE_ENABLED ? ['pkce'] : [], + client: env.OIDC_ID_TOKEN_SIGNED_RESPONSE_ALG + ? { id_token_signed_response_alg: env.OIDC_ID_TOKEN_SIGNED_RESPONSE_ALG } + : undefined, allowDangerousEmailAccountLinking: env.OIDC_ALLOW_DANGEROUS_EMAIL_LINKING, idToken: true, profile(profile) {