Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 18 additions & 15 deletions backend/security/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import invitationsRoutes from './routes/invitations'
import internalRoutes from './routes/internal'
import apiTokensRoutes, { orgTokensRouter } from './routes/api-tokens'
import { tokenAuthRateLimiter } from './middleware/api-token-auth'
import { oidcService } from './services/oidc'
import { initializeAllTenants } from './services/oidc'

dotenv.config()

Expand Down Expand Up @@ -96,15 +96,15 @@ async function startServer() {
})

try {
console.log('🔧 Initializing OIDC service...')
if (oidcService.isConfigured()) {
await oidcService.initialize()
console.log('✅ OIDC service initialized successfully')
} else {
console.log('⚠️ OIDC service not configured - local auth only')
}
console.log('🔧 Initializing OIDC service(s)...')
// One client per configured tenant, each against its OWN Authentik.
// initializeAllTenants warms them in parallel and starts each one's
// self-heal loop; a tenant whose Authentik is down is logged and left to
// the background retry rather than blocking the others from coming up.
await initializeAllTenants()
console.log('✅ OIDC service(s) initialized')
} catch (error) {
console.error('❌ Failed to initialize OIDC service:', error)
console.error('❌ Failed to initialize OIDC service(s):', error)
console.log('⚠️ Continuing with local authentication only')
}

Expand All @@ -118,12 +118,15 @@ async function startServer() {
// human ran `kubectl rollout restart`. This self-heals on its own once
// Authentik comes back, with zero request traffic required. Requests
// arriving in the meantime also get a lazy re-init attempt via
// oidcService.ensureInitialized() (see routes/auth.ts, authentikPassword.ts,
// AuthentikIdentityProvider.ts) — both paths share the same in-flight
// promise + cooldown so they never double-fire against Authentik.
if (oidcService.isConfigured() && !oidcService.isInitialized()) {
oidcService.startBackgroundRetry()
}
// getOidcService().ensureInitialized() (see routes/auth.ts,
// authentikPassword.ts, AuthentikIdentityProvider.ts) — both paths share
// the same in-flight promise + cooldown so they never double-fire against
// Authentik.
//
// The retry loops are started by initializeAllTenants() above, per tenant,
// so there is no separate kick-off here. Each tenant self-heals
// independently: one tenant's Authentik being down neither blocks nor
// resets another's.

const portNumber = typeof PORT === 'string' ? parseInt(PORT, 10) : PORT
httpServer.listen(portNumber, () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ import crypto from 'crypto'
import jwt from 'jsonwebtoken'
import { v4 as uuidv4 } from 'uuid'
import { db as defaultDb } from '../../config/database'
import { oidcService as defaultOidc } from '../../services/oidc'
import { getOidcService, type OIDCServiceLike } from '../../services/oidc'
import { currentTenant } from './tenants'
import { authentikPasswordLogin as defaultPasswordLogin } from '../../services/authentikPassword'
import { runInternalProvision } from '../../services/organizationProvisioning'
import {
Expand Down Expand Up @@ -212,7 +213,7 @@ export function emailVerificationEnabled(): boolean {

export interface AuthentikProviderDeps {
db: Db
oidc: typeof defaultOidc
oidc: OIDCServiceLike
passwordLoginFn: (email: string, password: string) => Promise<BrokeredUser>
/** Drives Authentik enrollment + OIDC sync; returns the synced user projection. */
signupFn: (input: SignupInput) => Promise<BrokeredUser>
Expand Down Expand Up @@ -251,7 +252,23 @@ function maskContact(v: string): string {

export class AuthentikIdentityProvider implements IdentityProvider {
private db: Db
private oidc: typeof defaultOidc
/**
* Injected override (tests). Left undefined in production so `oidc` resolves
* the CURRENT tenant's client on each access — this provider is constructed
* once, outside any request, so binding an instance here would permanently
* pin it to whichever tenant happened to be ambient at construction.
*/
private oidcOverride?: OIDCServiceLike

/**
* The OIDC client for the tenant serving the CURRENT request, resolved on
* every access rather than captured once. An injected override always wins,
* so tests are unaffected.
*/
private get oidc(): OIDCServiceLike {
return this.oidcOverride ?? getOidcService()
}

private passwordLoginFn: (email: string, password: string) => Promise<BrokeredUser>
private signupFn: (input: SignupInput) => Promise<BrokeredUser>
private notifications: NotificationClient
Expand All @@ -270,7 +287,7 @@ export class AuthentikIdentityProvider implements IdentityProvider {

constructor(deps: Partial<AuthentikProviderDeps> = {}) {
this.db = deps.db ?? defaultDb
this.oidc = deps.oidc ?? defaultOidc
this.oidcOverride = deps.oidc
this.passwordLoginFn =
deps.passwordLoginFn ??
(async (email, password) => {
Expand Down Expand Up @@ -715,7 +732,7 @@ export class AuthentikIdentityProvider implements IdentityProvider {
async issueM2MToken(input: M2MTokenInput): Promise<M2MToken> {
if (this.issueM2MFn) return this.issueM2MFn(input)
// client-credentials grant against the global token endpoint.
const issuer = process.env.AUTHENTIK_ISSUER_URL || 'http://localhost:9000/application/o/fuzefront/'
const issuer = currentTenant('issuer').issuerUrl
const tokenEndpoint = new URL('/application/o/token/', new URL(issuer).origin).toString()
const body = new URLSearchParams({
grant_type: 'client_credentials',
Expand Down
Loading
Loading