@agility-luhn/iam
v1.5.1
Published
IAM (Identity and Access Management) para Luhn: autenticação, autorização, MFA, controle de rotas e papéis.
Maintainers
Readme
@agility-luhn/iam
IAM (Identity and Access Management) para aplicações LUHN. Distribuído como plugin do runtime: ao registrar
o plugin em createLuhnServer({ plugins: [createIamPlugin(...)] }), o app hospedeiro ganha as entidades de
identidade, services de autenticação/senha/menu, e o enforcement automático de Authorization: Bearer <jwt>
em todas as rotas (exceto allowlist pública).
Pacote público do monorepo LUHN, publicado como @agility-luhn/iam no npm.
1. O que o pacote entrega
| Camada | Conteúdo |
|---------------------------|----------|
| Modelagem .luhn | luhn/iam.luhn — application; declarações organizadas nas pastas de cada módulo |
| Artefatos gerados | src/luhn/ — interfaces/Zod/services/controllers/handles + DDL Postgres + OpenAPI |
| Plugin para o runtime | src/plugin.ts — createIamPlugin(options) |
| Hashing de senha | src/passwords.ts — argon2id com parâmetros OWASP 2024 |
| Assinatura JWT | src/jwt.ts — IamJwtSigner (HS256 default, RS256 opcional) |
| Tokens opacos | src/tokens.ts — geração, hash SHA-256 e verificação em tempo constante |
| Implementação dos services | src/services/ — auth, password, menu (handlers reais) |
| Compilador embarcado | src/compile-iam.ts — regera os artefatos sob src/luhn |
| Entrypoint público | src/index.ts |
2. Modelo de domínio (luhn/)
A aplicação iam declara quatro módulos. Cada módulo mantém suas declarações na pasta de mesmo nome:
application iam (v0.1.0)
├── module identity (owner true) — records e businesses de identidade
├── module auth — login, refresh, logout e me
├── module password — define, change, requestRecovery e completeRecovery
└── module access — myRoutesO arquivo luhn/iam.luhn contém somente a declaração da application. As demais declarações ficam em
luhn/<module>/, fazendo do diretório a fonte de verdade para o módulo de cada símbolo.
2.1 Tabelas Postgres geradas
Schemas: iam. Tabelas:
iam.user,iam.user_roles,iam.user_units.iam.role,iam.role_permissions.iam.app.iam.app_module,iam.app_module_routes.
Cada registro de iam.app_module pertence obrigatoriamente a uma aplicação por meio de app_id.
Tipos físicos relevantes:
- Enums (
user_status,mfa_type,http_method, etc.) são enum nativo Postgres — armazenam o nome ('ACTIVE','GET','POST', ...), não o ordinal. passwordHashetotpSecretmapeiam paraTEXT(encriptografia em repouso é responsabilidade do banco).registrationTokenerecoveryTokenarmazenam apenas hash SHA-256 (veja src/tokens.ts).
O IAM não tem multitenancy próprio. Todas as suas businesses usam
@scope global. O app hospedeiro decide se acopla as entidades de IAM ao seu conceito deconta/unidade(via mixin, FK extra, ou políticas RLS).
3. Plugin do Runtime
import { createLuhnServer } from '@agility-luhn/runtime';
import { createIamPlugin, IamJwtSigner, PasswordHasher } from '@agility-luhn/iam';
const iamPlugin = createIamPlugin({
jwt: new IamJwtSigner({
signingKey: process.env.JWT_SECRET!, // HS256: segredo simétrico
issuer: 'meu-app',
audience: 'meu-app-api',
accessTtlSeconds: 15 * 60, // default 15 min
refreshTtlSeconds: 14 * 24 * 60 * 60 // default 14 dias
}),
hasher: new PasswordHasher(), // argon2id (64 MiB / 3 it / 4 lanes)
mailer: async ({ to, subject, body }) => { /* opcional */ }
});
const server = await createLuhnServer({
luhnDir: './luhn',
setup,
plugins: [iamPlugin]
});
await server.listen(3300);3.1 Contrato do plugin
createIamPlugin(options): LuhnPlugin devolve:
name:'@agility-luhn/iam'.luhnDirs: [<pasta luhn do pacote>]— adiciona as entidades/services do IAM à compilação do app.install(ctx)— registra os handlers TS dos services (auth.login,auth.refresh,auth.logout,auth.me,password.define,password.change,password.requestRecovery,password.completeRecovery,access.myRoutes) injetando dependências (db,jwt,hasher,mailer?).authenticate(req)— lêAuthorization: Bearer <jwt>, valida comjwt.verify, exigetoken_type='access', e devolveSecurityContext { userId, tenantId, roles, permissions }.authorize(req)— política default:- libera apenas a allowlist pública abaixo;
- exige
req.user !== undefinedem qualquer outra rota (incluindo CRUD/api/<mod>/<entidade>).
dispose()— no-op atual; previsto para fechar caches/Redis em versões futuras.
3.2 Allowlist pública (sem token)
| Método | Path | Por quê |
|--------|-------------------------------------|---------|
| POST | /api/auth/login | bootstrap do token |
| POST | /api/auth/refresh | renovação de access via refresh token |
| POST | /api/password/define | primeiro acesso (token de cadastro por e-mail) |
| POST | /api/password/request-recovery | usuário esqueceu a senha |
| POST | /api/password/complete-recovery | consome token de recuperação |
Qualquer outra rota (/api/auth/me, /api/auth/logout, /api/password/change, /api/access/my-routes, CRUDs
do app inteiro) responde 401 UNAUTHORIZED sem token e 403 FORBIDDEN quando há token mas autorização nega.
4. Services Expostos
4.1 auth.login — POST /api/auth/login
// request
{ "login": "admin", "password": "...", "unitId": "...", "mfaCode": "123456" }
// response
{
"accessToken": "...", "refreshToken": "...",
"expiresIn": 900, "tokenType": "Bearer",
"user": { /* identity.user */ },
"unitId": "...", "mfaRequired": false
}Erros: INVALID_CREDENTIALS, USER_INACTIVE, USER_PENDING, PASSWORD_EXPIRED, MFA_REQUIRED,
MFA_INVALID, INVALID_UNIT.
Internamente: busca o business identity.user, compara hash com hasher.verify, atualiza last_login_at,
emite par access_token + refresh_token via IamJwtSigner.
4.2 auth.refresh — POST /api/auth/refresh
Consome refresh token, valida token_type='refresh', devolve novo access_token (rotacionando refresh quando
configurado).
4.3 auth.logout — POST /api/auth/logout
Invalida o refresh token (idealmente via blacklist por jti).
4.4 auth.me — POST /api/auth/me
Retorna o usuário corrente derivado de req.user.userId (resolvido pelo authenticate).
4.5 password.define — POST /api/password/define
Consome registrationToken (hash conferido com verifyToken), grava passwordHash (argon2id), marca usuário
como ACTIVE + emailVerified. Erros: INVALID_TOKEN, EXPIRED_TOKEN, WEAK_PASSWORD.
4.6 password.change — POST /api/password/change
Exige autenticação. Valida senha atual, aplica nova com hasher, limpa passwordExpired.
4.7 password.requestRecovery — POST /api/password/request-recovery
Gera recoveryToken opaco (32 bytes, base64url), persiste só o hash com expiração, dispara e-mail via
mailer (quando configurado).
4.8 password.completeRecovery — POST /api/password/complete-recovery
Consome recoveryToken, redefine senha, limpa token.
4.9 access.myRoutes — POST /api/access/my-routes
Cruza identity.appModule (+ routes) com os papéis do usuário corrente para devolver a árvore
de menu autorizada. Base para a UI montar a navegação após login.
5. Helpers Framework
5.1 PasswordHasher (src/passwords.ts)
const hasher = new PasswordHasher({ memoryCost: 65536, timeCost: 3, parallelism: 4 });
const hash = await hasher.hash(plain);
const ok = await hasher.verify(hash, plain);
const stale = hasher.needsRehash(hash); // true quando parâmetros mudaramPadrões alinhados ao OWASP 2024+: argon2id, 64 MiB de memória, 3 iterações, 4 lanes. Construtor aceita override para CI/testes.
5.2 IamJwtSigner (src/jwt.ts)
const signer = new IamJwtSigner({
algorithm: 'HS256', // default; suporta 'RS256' com par de chaves
signingKey: process.env.JWT_SECRET!,
verificationKey: undefined, // RS256: chave pública PEM
issuer: 'meu-app',
audience: 'meu-app-api',
accessTtlSeconds: 900,
refreshTtlSeconds: 1209600
});
const access = signer.signAccessToken({ sub, conta_id, login, roles, mfa: true });
const refresh = signer.signRefreshToken({ sub, conta_id, jti });
const claims = signer.verify(token, 'access');Claims: sub, conta_id, unidade_id?, login, roles[], mfa?, token_type: 'access' | 'refresh'.
5.3 Tokens opacos (src/tokens.ts)
const { raw, hash } = generateOpaqueToken(32); // entrega `raw` por e-mail; grava `hash` no banco
const ok = verifyToken(rawRecebido, hashArmazenado); // comparação timing-safe
const valid = tokenIsValid(expiraEmIso);Tokens em claro nunca são persistidos. O banco recebe apenas o SHA-256 hex; a verificação usa
crypto.timingSafeEqual.
6. Compilação e Regeneração
Sempre que luhn/iam.luhn mudar, regere os artefatos:
pnpm --filter @agility-luhn/iam compile # luhn compile -o ./src/luhn
pnpm --filter @agility-luhn/iam build # tsc -p tsconfig.jsonO compile-iam.ts:
- usa
iamSetup(src/compile-iam.ts) — driverpostgres, runtime importPath@agility-luhn/runtime/src/services. - escreve em
src/luhn/(artefatos TS + DDLdatabase.json+openapi.json+objects-map.json). - imprime contagem de artefatos, erros e avisos.
tsconfig.json exclui compile-iam.ts do build do pacote (é script, não código de runtime).
7. Bootstrap: usuário admin
O playground inclui seed-admin.js (template) para gerar o hash argon2id do primeiro
usuário e inseri-lo em iam.user com status='ACTIVE'. Em produção, prefira usar o fluxo
registrationToken → password.define.
8. Integração com Banco (migração de rotas)
O catálogo identity.appModule (+ routes) é a base do access.myRoutes. O app hospedeiro deve
popular esse catálogo a partir das rotas compiladas da aplicação.
9. Limitações Atuais
- MFA: campos modelados (
mfaEnabled,mfaType,mfaSecret), mas o fluxo TOTP/SMS/email ainda é placeholder emauth.login. - Refresh blacklist: depende da app hospedeira definir storage (Redis/coluna
refresh_tokens). - Audit log: ainda não emitido pelo plugin.
- Policy beyond roles:
authorizeatual é all-or-nothing por presença de usuário; políticas finas (route_actions, scopes) ficam a cargo do app ou de evoluções daauthorizechain. - Multi-tenant: nenhum filtro automático por
conta_idem queries CRUD — oSecurityContexté exposto, mas a propagação parahandleCrudRouteestá no roadmap.
10. Convenções
- Comunicação no projeto: pt-BR. Comentários do código: EN.
exactOptionalPropertyTypes: trueem todo o monorepo → opcionais saem com| undefinedexplícito.- Cross-package imports usam o nome do pacote (
@agility-luhn/runtime), nunca caminhos relativos para outraspackages/. - Após alterar generators do compiler, regere IAM e playground para validar.
