Summary
Following a review of the auth extensibility mechanism (AbstractAuthModule + the Authentication registry), the plugin seam is well-designed and genuinely pluggable — but there are a few improvements that would make it comfortable to run multiple auth types in production. A prototype Okta plugin was used as the reference for a redirect/OAuth-style implementation.
The extension point itself is strong: a plugin declares a type and need only implement authenticate(user, req, res); everything downstream (JWT generation, sessions, scope resolution, RBAC, route gating) is auth-type-agnostic and inherited. This was validated against two structurally opposite implementations — adapt-authoring-auth-local (credential verification) and the Okta prototype (passport redirect → /callback → token). No changes to the core seam are required for those to work.
The items below are enhancements, not blockers.
Proposed improvements
1. Extract an AbstractOAuthModule to remove per-provider boilerplate
Every OAuth plugin currently re-implements the same scaffolding: passport.initialize()/passport.session(), serializeUser/deserializeUser, the / + /callback routes, unsecureRoute calls, and an onAuthenticated handler that generates the token. The default authenticateHandler is credential-shaped (reads req.body.email/password), so OAuth plugins cannot reuse it and must roll their own.
A second abstract layer (AbstractOAuthModule extends AbstractAuthModule) could own the passport wiring and the callback→token flow, reducing a new provider to: config (client id/secret/callback), a passport strategy, and a mapProfile(profile) → { email, firstName, lastName, roles } mapper.
2. Server-advertised login-method discovery
There is currently no endpoint that tells a client which auth types are enabled or which is the default interactive login method — the client has to know to POST to /api/auth/<type> (or redirect to it). This means two redirect-based providers cannot be cleanly arbitrated: coexistence works fine at the token/API layer, but interactive login effectively assumes a single method.
Proposal: a public GET /api/auth/methods (or similar) that lists registered plugins and flags which are interactive/redirect vs credential-submit, so a client can present a chooser instead of hardcoding a single method.
3. Decide on account linking vs one authType per user
AuthToken.initRequestData resolves the plugin from user.authType, so a user is bound to exactly one auth type. A person provisioned via local cannot also authenticate via an IdP. If SSO and local auth need to serve the same users, this needs an account-linking model; if not, it's worth documenting as an explicit constraint.
Scope
This issue is primarily items 1–3 (enhancements to the extensibility seam). Items 1 and 2 are the highest value. Happy to split into separate issues if preferred.
Summary
Following a review of the auth extensibility mechanism (
AbstractAuthModule+ theAuthenticationregistry), the plugin seam is well-designed and genuinely pluggable — but there are a few improvements that would make it comfortable to run multiple auth types in production. A prototype Okta plugin was used as the reference for a redirect/OAuth-style implementation.The extension point itself is strong: a plugin declares a
typeand need only implementauthenticate(user, req, res); everything downstream (JWT generation, sessions, scope resolution, RBAC, route gating) is auth-type-agnostic and inherited. This was validated against two structurally opposite implementations —adapt-authoring-auth-local(credential verification) and the Okta prototype (passport redirect →/callback→ token). No changes to the core seam are required for those to work.The items below are enhancements, not blockers.
Proposed improvements
1. Extract an
AbstractOAuthModuleto remove per-provider boilerplateEvery OAuth plugin currently re-implements the same scaffolding:
passport.initialize()/passport.session(),serializeUser/deserializeUser, the/+/callbackroutes,unsecureRoutecalls, and anonAuthenticatedhandler that generates the token. The defaultauthenticateHandleris credential-shaped (readsreq.body.email/password), so OAuth plugins cannot reuse it and must roll their own.A second abstract layer (
AbstractOAuthModule extends AbstractAuthModule) could own the passport wiring and the callback→token flow, reducing a new provider to: config (client id/secret/callback), a passport strategy, and amapProfile(profile) → { email, firstName, lastName, roles }mapper.2. Server-advertised login-method discovery
There is currently no endpoint that tells a client which auth types are enabled or which is the default interactive login method — the client has to know to POST to
/api/auth/<type>(or redirect to it). This means two redirect-based providers cannot be cleanly arbitrated: coexistence works fine at the token/API layer, but interactive login effectively assumes a single method.Proposal: a public
GET /api/auth/methods(or similar) that lists registered plugins and flags which are interactive/redirect vs credential-submit, so a client can present a chooser instead of hardcoding a single method.3. Decide on account linking vs one
authTypeper userAuthToken.initRequestDataresolves the plugin fromuser.authType, so a user is bound to exactly one auth type. A person provisioned vialocalcannot also authenticate via an IdP. If SSO and local auth need to serve the same users, this needs an account-linking model; if not, it's worth documenting as an explicit constraint.Scope
This issue is primarily items 1–3 (enhancements to the extensibility seam). Items 1 and 2 are the highest value. Happy to split into separate issues if preferred.