Summary
Add support for external identity providers via OpenID Connect (OIDC), allowing users to authenticate with providers like Authentik, Keycloak, Authelia, or any standard OIDC-compliant IdP. This is a common requirement for self-hosters who centralise authentication across their homelab services.
Proposed Implementation
Heads up: all the code snippets below are vibe coded — I haven't tested any of them and can't verify they'd actually work as-is. They're more of a proof of concept to show the approach rather than copy-pasteable implementations.
The codebase is already well-positioned for this asit uses NextAuth v5 (Auth.js), which has built-in OIDC provider support. Realistically the following would need to be done
1. Prisma Schema Changes
Make User.password optional - OIDC users won't have a local password:
model User {
id String @id @default(uuid())
name String
email String @unique
- password String
+ password String?
// ... rest unchanged
}
Add an Account model - required by NextAuth for OAuth provider token storage:
model Account {
id String @id @default(uuid())
userId String
type String
provider String
providerAccountId String
refresh_token String?
access_token String?
expires_at Int?
token_type String?
scope String?
id_token String?
session_state String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
@@index([userId])
}
Add the relation to the User model:
model User {
// ... existing fields
+ accounts Account[]
}
2. Auth Configuration (src/auth.ts)
Add a generic OIDC provider alongside the existing Credentials provider, gated behind an environment variable:
import { type OIDCConfig } from "next-auth/providers";
const providers = [
Credentials({
// ... existing credentials config unchanged
}),
];
// Only add OIDC provider if configured
if (process.env.OIDC_ENABLED === "true") {
const { default: OIDCProvider } = await import("next-auth/providers");
providers.push({
id: "oidc",
name: process.env.OIDC_PROVIDER_NAME || "OIDC",
type: "oidc",
issuer: process.env.OIDC_ISSUER,
clientId: process.env.OIDC_CLIENT_ID,
clientSecret: process.env.OIDC_CLIENT_SECRET,
});
}
3. Callback Handling - Auto-Create Users on First OIDC Login
In the NextAuth callbacks, handle first-time OIDC users by seeding their default data (job sources, job statuses) the same way signup() in auth.actions.ts does:
callbacks: {
async signIn({ user, account }) {
if (account?.provider === "oidc" && user?.email) {
const existingUser = await prisma.user.findUnique({
where: { email: user.email },
});
if (!existingUser) {
// Create user + seed default job sources/statuses
// (extract the seeding logic from auth.actions.ts signup() into a shared helper)
}
}
return true;
},
// ... existing callbacks
}
4. Sign-In UI (src/components/auth/SigninForm.tsx)
Add an OIDC button above or below the existing credentials form, conditionally rendered based on a config endpoint or env var passed to the client:
{oidcEnabled && (
<Button
onClick={() => signIn("oidc", { callbackUrl: "/dashboard" })}
variant="outline"
className="w-full"
>
Sign in with {oidcProviderName || "OIDC"}
</Button>
)}
5. Environment Variables
# OIDC Configuration (optional - local credentials always available)
OIDC_ENABLED=false
OIDC_PROVIDER_NAME=Authentik
OIDC_ISSUER=https://auth.example.com/application/o/jobsync/
OIDC_CLIENT_ID=
OIDC_CLIENT_SECRET=
These would also need to be plumbed through docker-compose.yml.
6. Guard Existing Password-Required Code
A few places assume password is always present and would need null checks:
auth.actions.ts - signup() still requires a password (unchanged for local signup)
auth.ts - the Credentials authorize() function calls bcrypt.compare(password, user.password), this is fine since it only runs for credentials login, but should guard against user.password being null
SignupForm - unchanged, only used for local account creation
Files Affected
| File |
Change |
prisma/schema.prisma |
Add Account model, make password optional |
src/auth.ts |
Add OIDC provider, add signIn callback for auto-provisioning |
src/auth.config.ts |
Minor: no changes expected |
src/actions/auth.actions.ts |
Extract user-seeding logic into shared helper |
src/components/auth/SigninForm.tsx |
Add OIDC sign-in button |
docker-compose.yml |
Add OIDC env vars |
.env.example |
Add OIDC env vars |
Considerations
- Account linking: If a user signs up locally and later logs in via OIDC with the same email, NextAuth can link the accounts automatically via the
Account model. This should be opt-in or at least documented.
- Disable local login: Some users may want to disable local credentials entirely - a
DISABLE_LOCAL_AUTH env var could be added, but this could be a follow-up.
- Breaking change: Making
password optional requires a migration, but existing users are unaffected since all current users have passwords set.
Summary
Add support for external identity providers via OpenID Connect (OIDC), allowing users to authenticate with providers like Authentik, Keycloak, Authelia, or any standard OIDC-compliant IdP. This is a common requirement for self-hosters who centralise authentication across their homelab services.
Proposed Implementation
The codebase is already well-positioned for this asit uses NextAuth v5 (Auth.js), which has built-in OIDC provider support. Realistically the following would need to be done
1. Prisma Schema Changes
Make
User.passwordoptional - OIDC users won't have a local password:Add an
Accountmodel - required by NextAuth for OAuth provider token storage:Add the relation to the
Usermodel:2. Auth Configuration (
src/auth.ts)Add a generic OIDC provider alongside the existing Credentials provider, gated behind an environment variable:
3. Callback Handling - Auto-Create Users on First OIDC Login
In the NextAuth callbacks, handle first-time OIDC users by seeding their default data (job sources, job statuses) the same way
signup()inauth.actions.tsdoes:4. Sign-In UI (
src/components/auth/SigninForm.tsx)Add an OIDC button above or below the existing credentials form, conditionally rendered based on a config endpoint or env var passed to the client:
5. Environment Variables
These would also need to be plumbed through
docker-compose.yml.6. Guard Existing Password-Required Code
A few places assume
passwordis always present and would need null checks:auth.actions.ts-signup()still requires a password (unchanged for local signup)auth.ts- the Credentialsauthorize()function callsbcrypt.compare(password, user.password), this is fine since it only runs for credentials login, but should guard againstuser.passwordbeing nullSignupForm- unchanged, only used for local account creationFiles Affected
prisma/schema.prismaAccountmodel, makepasswordoptionalsrc/auth.tssignIncallback for auto-provisioningsrc/auth.config.tssrc/actions/auth.actions.tssrc/components/auth/SigninForm.tsxdocker-compose.yml.env.exampleConsiderations
Accountmodel. This should be opt-in or at least documented.DISABLE_LOCAL_AUTHenv var could be added, but this could be a follow-up.passwordoptional requires a migration, but existing users are unaffected since all current users have passwords set.