Plug & Play Auth & Login
LoginFlow is the ZinTrust Plug & Play auth/login surface.
It moves repeated login orchestration out of one-off controllers and into an explicit provider and issuer contract.
Problem to solve
Service code often repeats the same login workflow:
- load the account
- verify credentials
- issue a token or session
- record audit context
- return a normalized login result
Available runtime
ts
import { Auth, ErrorFactory, LoginFlow } from '@zintrust/core';
LoginFlow.registerProvider('password', {
identify: async ({ email }) => User.where('email', '=', email).first(),
verify: async (user, { password }) => {
if (!user) {
throw ErrorFactory.createUnauthorizedError('Invalid credentials');
}
const ok = await Auth.compare(password, String(user.password ?? ''));
if (!ok) {
throw ErrorFactory.createUnauthorizedError('Invalid credentials');
}
return {
user,
subject: String(user.id),
claims: {
sub: String(user.id),
email: String(user.email),
},
};
},
});
const result = await LoginFlow.create({
provider: 'password',
context: {
requestId: req.getHeader('x-request-id'),
},
})
// This exact object becomes provider.identify({ email }, context)
.identify({ email })
// This exact object becomes provider.verify(identity, { password }, context)
.verify({ password })
.issue('jwt')
.audit()
.run();
res.json({
token: result.issued,
user: result.verified.user,
});How inputs are passed
Nothing in LoginFlow picks up email or password by magic.
.identify({ email })stores{ email }and later passes it toprovider.identify(input, context)..verify({ password })stores{ password }and later passes it toprovider.verify(identity, input, context).contextis exactly what the application supplied toLoginFlow.create({ context }).
So this:
ts
LoginFlow.create({ provider: 'password', context: { requestId } })
.identify({ email })
.verify({ password });becomes this at runtime:
ts
provider.identify({ email }, { requestId });
provider.verify(identity, { password }, { requestId });What the core owns
LoginFlow provides:
- explicit provider registration
- staged orchestration for
identify(),verify(),issue(), andaudit() - a built-in
jwtissuer backed byJwtManager.signAccessToken(...) - a built-in
traceauditor used by.audit()when no custom auditor is supplied - normalized staged failures with
stagemetadata
What the application still owns
Applications still decide:
- how an account is located
- how credentials are verified
- which claims are issued
- whether to use the built-in
jwtissuer or a custom issuer - whether to use the built-in
traceauditor or a custom audit sink
Custom issuer example
ts
LoginFlow.registerIssuer('session', async ({ verified, context }) => {
return context.sessions.create({
sub: verified.subject,
user: verified.user,
});
});
const result = await LoginFlow.create({
provider: 'password',
context: {
sessions: SessionManager.create({ cookieName: 'SID' }),
},
})
.identify({ email })
.verify({ password })
.issue('session')
.run();Custom audit example
ts
LoginFlow.registerAuditor('auth-log', async ({ status, stage, verified, context }) => {
Logger.info('login flow finished', {
status,
stage,
subject: verified?.subject,
requestId: context.requestId,
});
});
await LoginFlow.create({
provider: 'password',
context: { requestId: req.getHeader('x-request-id') },
})
.identify({ email })
.verify({ password })
.issue('jwt')
.audit('auth-log')
.run();Product-fit rules
The login Plug & Play surface should:
- keep provider registration explicit
- avoid retaining request objects after the flow completes
- support Node and Workers runtimes
- keep transport-specific concerns outside the core orchestration contract
- keep provider and issuer registries bounded and explicitly manageable
Current related docs
- Use authentication for JWT configuration, route protection, and revocation setup.
- Use plug-and-play-performance for the retention rules that keep login orchestration from turning into a process-global cache.