Skip to content

Feature Request: OpenID Connect (OIDC) / OAuth2 Authentication #78

Description

@eddiebquinn

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions