Feature Description
Refactor the existing authentication code behind an AuthProvider interface. The default provider implements the current local username/password flow. Additional providers (OIDC, SAML, LDAP) can be registered without modifying the core authentication path.
Problem/Use Case
Authentication is currently implemented inline in the user routes and session middleware. Adding a second authentication method (e.g. SSO via OIDC, which is a common request from teams using Logtide internally) means either tangling the code with conditional branches or fragmenting flows across routes. Both approaches accumulate technical debt fast.
By extracting the boundary at v1.0, we can:
- Add SSO/SAML/LDAP support as separate plugin modules without touching login, session, or user management code
- Allow operators to choose their identity backend at deployment time
- Allow downstream distributions to ship their own provider (e.g. one that reads users from a centralized directory) without patching the core
Proposed Solution
A minimal, narrow interface:
interface AuthProvider {
readonly id: string
readonly displayName: string
authenticate(input: AuthInput): Promise<AuthResult>
resolveUser(externalId: string): Promise<User | null>
listMethods?(): AuthMethodDescriptor[] // optional, for UI rendering
}
type AuthInput =
| { type: 'password'; email: string; password: string }
| { type: 'oidc_callback'; code: string; state: string }
| { type: 'saml_assertion'; samlResponse: string }
Providers register at boot. The login route delegates to the configured provider(s). Sessions remain agnostic of how the user authenticated — they just store userId and providerId.
The default LocalAuthProvider wraps the existing logic and lives in the same repo. SSO providers can ship later as separate packages or as opt-in modules guarded by the auth.sso capability.
Alternatives Considered
- Keep auth inline and add SSO with conditional branches. Works for one or two methods, becomes unmaintainable beyond that. Rejected.
- Adopt an existing auth library (Passport, Lucia, Better Auth). Tempting, but adds a heavy dependency for a problem that has a narrow shape here. The provider interface above is small enough that owning it is cheaper than integrating and customizing a framework.
- Defer until SSO is actually being implemented. That's exactly the moment when retrofitting this is most expensive. Doing it now lets SSO drop in cleanly later.
Implementation Details (Optional)
- Step 1: extract the current logic into
LocalAuthProvider with no behavior changes. All existing tests should pass.
- Step 2: introduce a
AuthProviderRegistry and route the login endpoint through it.
- Step 3: update the user model to include a
provider_id column (default local) and a provider_external_id for non-local providers.
- Step 4: document the interface in
docs/architecture/auth.md with a worked example of a minimal custom provider.
- The session layer is not changed in this issue. Sessions remain server-side, opaque tokens, same storage. Only the "who are you proving?" step is abstracted.
- No new authentication methods are introduced in this issue — that's a separate piece of work, gated behind
auth.sso.
Priority
Target Users
- Teams who need SSO integration with their existing identity provider (Okta, Google Workspace, Azure AD)
- Operators running Logtide alongside other internal tools and wanting unified login
- Downstream distributions needing to plug in their own user directory
Contribution
Feature Description
Refactor the existing authentication code behind an
AuthProviderinterface. The default provider implements the current local username/password flow. Additional providers (OIDC, SAML, LDAP) can be registered without modifying the core authentication path.Problem/Use Case
Authentication is currently implemented inline in the user routes and session middleware. Adding a second authentication method (e.g. SSO via OIDC, which is a common request from teams using Logtide internally) means either tangling the code with conditional branches or fragmenting flows across routes. Both approaches accumulate technical debt fast.
By extracting the boundary at v1.0, we can:
Proposed Solution
A minimal, narrow interface:
Providers register at boot. The login route delegates to the configured provider(s). Sessions remain agnostic of how the user authenticated — they just store
userIdandproviderId.The default
LocalAuthProviderwraps the existing logic and lives in the same repo. SSO providers can ship later as separate packages or as opt-in modules guarded by theauth.ssocapability.Alternatives Considered
Implementation Details (Optional)
LocalAuthProviderwith no behavior changes. All existing tests should pass.AuthProviderRegistryand route the login endpoint through it.provider_idcolumn (defaultlocal) and aprovider_external_idfor non-local providers.docs/architecture/auth.mdwith a worked example of a minimal custom provider.auth.sso.Priority
Target Users
Contribution