@adonis-agora/authkit-server 0.85.1 → 0.87.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/build/index.d.ts +1 -2
- package/build/index.js +0 -1
- package/build/src/agents/config.d.ts +1 -1
- package/build/src/agents/delegation_service.js +1 -1
- package/build/src/agents/poppy/client_registry.d.ts +5 -0
- package/build/src/agents/poppy/client_registry.js +35 -1
- package/build/src/agents/poppy/config.d.ts +111 -80
- package/build/src/agents/poppy/config.js +55 -35
- package/build/src/agents/poppy/dpop.d.ts +2 -2
- package/build/src/agents/poppy/dpop.js +3 -3
- package/build/src/agents/poppy/email_code_sign_in.d.ts +4 -4
- package/build/src/agents/poppy/email_code_sign_in.js +5 -5
- package/build/src/agents/poppy/errors.d.ts +7 -2
- package/build/src/agents/poppy/errors.js +5 -2
- package/build/src/agents/poppy/resource.d.ts +30 -25
- package/build/src/agents/poppy/resource.js +24 -20
- package/build/src/agents/poppy/safe_fetch.js +8 -2
- package/build/src/agents/poppy/service.d.ts +69 -62
- package/build/src/agents/poppy/service.js +540 -546
- package/build/src/agents/poppy/session_jwt.d.ts +30 -0
- package/build/src/agents/poppy/session_jwt.js +133 -0
- package/build/src/agents/poppy/store.d.ts +17 -22
- package/build/src/agents/poppy/store.js +22 -30
- package/build/src/agents/poppy/web_session.d.ts +23 -16
- package/build/src/agents/poppy/web_session.js +42 -24
- package/build/src/host/admin_sessions_service.js +3 -3
- package/build/src/host/controllers/account_apps_controller.js +1 -1
- package/build/src/host/controllers/poppy_consent_controller.d.ts +2 -2
- package/build/src/host/controllers/poppy_consent_controller.js +2 -2
- package/build/src/host/controllers/poppy_controller.d.ts +19 -11
- package/build/src/host/controllers/poppy_controller.js +93 -68
- package/build/src/host/csrf.d.ts +2 -4
- package/build/src/host/csrf.js +2 -3
- package/build/src/host/register_auth_host.d.ts +53 -0
- package/build/src/host/register_auth_host.js +66 -22
- package/build/src/schema/ensure.js +14 -9
- package/package.json +3 -3
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { PoppyError } from '../../agents/poppy/errors.js';
|
|
2
|
-
import { publicRequestUrl, writePoppyAuthError } from '../../agents/poppy/resource.js';
|
|
2
|
+
import { publicRequestUrl, writePoppyAuthError, } from '../../agents/poppy/resource.js';
|
|
3
3
|
import { loginPoppyBrowser } from '../../agents/poppy/web_session.js';
|
|
4
4
|
import { personalAgentsFor } from '../../agents/runtime.js';
|
|
5
5
|
function noStore(ctx) {
|
|
@@ -15,6 +15,9 @@ function oauthError(ctx, error) {
|
|
|
15
15
|
noStore(ctx);
|
|
16
16
|
for (const [k, v] of Object.entries(error.headers))
|
|
17
17
|
ctx.response.header(k, v);
|
|
18
|
+
// Limite de taxa: 429 com `Retry-After`, sem corpo próprio (§5.6).
|
|
19
|
+
if (error.status === 429)
|
|
20
|
+
return ctx.response.status(429).send('');
|
|
18
21
|
if (error.code === 'invalid_dpop_proof' || error.code === 'use_dpop_nonce') {
|
|
19
22
|
ctx.response.header('WWW-Authenticate', `DPoP error="${error.code}"`);
|
|
20
23
|
}
|
|
@@ -31,25 +34,43 @@ function form(ctx) {
|
|
|
31
34
|
return ctx.request.all();
|
|
32
35
|
}
|
|
33
36
|
/**
|
|
34
|
-
* Endpoints
|
|
35
|
-
* token, revogação, device authorization
|
|
36
|
-
*
|
|
37
|
+
* Endpoints do Poppy (§3, §4, §6): descoberta, protected resource metadata, metadata RFC 8414,
|
|
38
|
+
* token, revogação, device authorization e a entrada do navegador na Session. Os POSTs são isentos
|
|
39
|
+
* de CSRF — ver `authkitCsrfExceptions`.
|
|
37
40
|
*/
|
|
38
41
|
export default class PoppyController {
|
|
39
|
-
/** `GET /.well-known/poppy.json` — só no domínio da empresa
|
|
42
|
+
/** `GET /.well-known/poppy.json` — só no domínio da empresa; `www.` redireciona para ele. */
|
|
40
43
|
async discovery(ctx) {
|
|
41
44
|
const poppy = await poppyOr404(ctx);
|
|
42
45
|
if (!poppy)
|
|
43
46
|
return;
|
|
44
47
|
// `host()` mantém a porta (dev em `localhost:3333`): o `browser_session_endpoint` sai dele.
|
|
45
|
-
const
|
|
48
|
+
const host = ctx.request.host() ?? ctx.request.hostname() ?? '';
|
|
49
|
+
const redirect = poppy.discoveryRedirect(host);
|
|
50
|
+
if (redirect) {
|
|
51
|
+
ctx.response.header('Location', redirect);
|
|
52
|
+
return ctx.response.status(308).send('');
|
|
53
|
+
}
|
|
54
|
+
const doc = poppy.discoveryDocument(host);
|
|
46
55
|
if (!doc)
|
|
47
56
|
return ctx.response.notFound();
|
|
48
57
|
ctx.response.header('Access-Control-Allow-Origin', '*');
|
|
49
58
|
ctx.response.header('Cache-Control', 'public, max-age=300');
|
|
50
59
|
return ctx.response.json(doc);
|
|
51
60
|
}
|
|
52
|
-
/** `GET /.well-known/oauth-
|
|
61
|
+
/** `GET /.well-known/oauth-protected-resource` do domínio (§3.2, RFC 9728). */
|
|
62
|
+
async protectedResource(ctx) {
|
|
63
|
+
const poppy = await poppyOr404(ctx);
|
|
64
|
+
if (!poppy)
|
|
65
|
+
return;
|
|
66
|
+
const doc = poppy.protectedResourceMetadata(ctx.request.host() ?? ctx.request.hostname() ?? '');
|
|
67
|
+
if (!doc)
|
|
68
|
+
return ctx.response.notFound();
|
|
69
|
+
ctx.response.header('Access-Control-Allow-Origin', '*');
|
|
70
|
+
ctx.response.header('Cache-Control', 'public, max-age=300');
|
|
71
|
+
return ctx.response.json(doc);
|
|
72
|
+
}
|
|
73
|
+
/** `GET /.well-known/oauth-authorization-server{prefix}` (RFC 8414 + `protected_resources`). */
|
|
53
74
|
async metadata(ctx) {
|
|
54
75
|
const poppy = await poppyOr404(ctx);
|
|
55
76
|
if (!poppy)
|
|
@@ -64,7 +85,7 @@ export default class PoppyController {
|
|
|
64
85
|
return;
|
|
65
86
|
withNonce(ctx, poppy);
|
|
66
87
|
try {
|
|
67
|
-
const body = await poppy.token(form(ctx), ctx.request.header('dpop'));
|
|
88
|
+
const body = await poppy.token(form(ctx), ctx.request.header('dpop'), ctx);
|
|
68
89
|
noStore(ctx);
|
|
69
90
|
return ctx.response.json(body);
|
|
70
91
|
}
|
|
@@ -105,89 +126,93 @@ export default class PoppyController {
|
|
|
105
126
|
throw error;
|
|
106
127
|
}
|
|
107
128
|
}
|
|
108
|
-
/**
|
|
109
|
-
|
|
129
|
+
/**
|
|
130
|
+
* `GET web.browser_session_endpoint` (§6, passo 1): a página com o código de uso único e o cookie
|
|
131
|
+
* que o prende a este navegador. Não mexe em Session nem nos outros cookies.
|
|
132
|
+
*/
|
|
133
|
+
async browserSessionPage(ctx) {
|
|
110
134
|
const poppy = await poppyOr404(ctx);
|
|
111
|
-
if (!poppy?.cfg.
|
|
135
|
+
if (!poppy?.cfg.web?.browserSession)
|
|
112
136
|
return poppy ? ctx.response.notFound() : undefined;
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
try {
|
|
123
|
-
const body = await poppy.mediatedStart(auth.principal, auth.jkt, ctx.request.body(), ctx);
|
|
124
|
-
noStore(ctx);
|
|
125
|
-
return ctx.response.json(body);
|
|
126
|
-
}
|
|
127
|
-
catch (error) {
|
|
128
|
-
if (error instanceof PoppyError)
|
|
129
|
-
return oauthError(ctx, error);
|
|
130
|
-
throw error;
|
|
131
|
-
}
|
|
137
|
+
const { code, cookie, maxAge } = await poppy.startBrowserJoin();
|
|
138
|
+
const name = joinCookieName(poppy);
|
|
139
|
+
ctx.response.append('Set-Cookie', `${name}=${cookie}; Path=/; Max-Age=${maxAge}; ${cookieSecure(poppy)}HttpOnly; SameSite=Strict`);
|
|
140
|
+
browserHeaders(ctx);
|
|
141
|
+
// `code` só tem caracteres base64url; o `action` é o path do próprio endpoint.
|
|
142
|
+
const action = new URL(poppy.urls.browserSession).pathname;
|
|
143
|
+
return ctx.response
|
|
144
|
+
.status(200)
|
|
145
|
+
.send(`<!doctype html><html><head><meta charset="utf-8"><title>Connecting</title></head><body><form method="post" action="${action}"><input type="hidden" name="code" value="${code}"><button>Continue</button></form></body></html>`);
|
|
132
146
|
}
|
|
133
|
-
/**
|
|
134
|
-
|
|
147
|
+
/**
|
|
148
|
+
* `POST {browser_session_endpoint}/authorize` (§6, passo 2): o agente autoriza o código, com o
|
|
149
|
+
* header da Session e, se logado, o access token. 204; código inválido → 400 `invalid_request`.
|
|
150
|
+
*/
|
|
151
|
+
async browserSessionAuthorize(ctx) {
|
|
135
152
|
const poppy = await poppyOr404(ctx);
|
|
136
|
-
if (!poppy?.cfg.
|
|
153
|
+
if (!poppy?.cfg.web?.browserSession)
|
|
137
154
|
return poppy ? ctx.response.notFound() : undefined;
|
|
138
|
-
|
|
139
|
-
method: ctx.request.method(),
|
|
140
|
-
url: publicRequestUrl(ctx, poppy),
|
|
141
|
-
headers: ctx.request.headers(),
|
|
142
|
-
});
|
|
143
|
-
if (!auth.ok) {
|
|
144
|
-
const { ok: _ok, ...rest } = auth;
|
|
145
|
-
return writePoppyAuthError(ctx, { ok: false, ...rest });
|
|
146
|
-
}
|
|
155
|
+
noStore(ctx);
|
|
147
156
|
try {
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
return ctx.response.
|
|
157
|
+
const result = await poppy.authorizeBrowserJoin({
|
|
158
|
+
method: ctx.request.method(),
|
|
159
|
+
url: publicRequestUrl(ctx, poppy),
|
|
160
|
+
headers: ctx.request.headers(),
|
|
161
|
+
}, ctx.request.body()?.code);
|
|
162
|
+
if (!result.ok)
|
|
163
|
+
return writePoppyAuthError(ctx, result);
|
|
164
|
+
return ctx.response.status(204).send('');
|
|
156
165
|
}
|
|
157
166
|
catch (error) {
|
|
158
|
-
if (error instanceof PoppyError)
|
|
159
|
-
return
|
|
167
|
+
if (error instanceof PoppyError) {
|
|
168
|
+
return ctx.response.status(400).json({ error: 'invalid_request' });
|
|
169
|
+
}
|
|
160
170
|
throw error;
|
|
161
171
|
}
|
|
162
172
|
}
|
|
163
173
|
/**
|
|
164
|
-
* `POST web.browser_session_endpoint` (§
|
|
165
|
-
* para a Session e
|
|
174
|
+
* `POST web.browser_session_endpoint` (§6, passo 3): o navegador submete o código → cookie do app
|
|
175
|
+
* para a Session, limpa o cookie do passo 1 e redireciona para o site; falha → página de erro.
|
|
166
176
|
*/
|
|
167
177
|
async browserSession(ctx) {
|
|
168
178
|
const poppy = await poppyOr404(ctx);
|
|
169
179
|
if (!poppy?.cfg.web?.browserSession)
|
|
170
180
|
return poppy ? ctx.response.notFound() : undefined;
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
181
|
+
browserHeaders(ctx);
|
|
182
|
+
const name = joinCookieName(poppy);
|
|
183
|
+
const result = await poppy.completeBrowserJoin(ctx.request.input('code'), readCookie(ctx.request.header('cookie'), name));
|
|
184
|
+
if (!result)
|
|
174
185
|
return badBrowserRequest(ctx);
|
|
175
|
-
let result;
|
|
176
|
-
try {
|
|
177
|
-
result = await poppy.verifyBrowserAssertion(ctx.request.input('assertion'), new URL(publicRequestUrl(ctx, poppy)).origin);
|
|
178
|
-
}
|
|
179
|
-
catch (error) {
|
|
180
|
-
if (error instanceof PoppyError)
|
|
181
|
-
return badBrowserRequest(ctx);
|
|
182
|
-
throw error;
|
|
183
|
-
}
|
|
184
186
|
await loginPoppyBrowser(ctx, result);
|
|
185
|
-
ctx.response.
|
|
187
|
+
ctx.response.append('Set-Cookie', `${name}=; Path=/; Max-Age=0; ${cookieSecure(poppy)}HttpOnly; SameSite=Strict`);
|
|
188
|
+
ctx.response.header('Location', poppy.cfg.web.redirectTo);
|
|
186
189
|
return ctx.response.status(303).send('');
|
|
187
190
|
}
|
|
188
191
|
}
|
|
189
|
-
|
|
192
|
+
/** `__Host-` exige `Secure`; em dev com o issuer em http o cookie sai sem os dois. */
|
|
193
|
+
function joinCookieName(poppy) {
|
|
194
|
+
return new URL(poppy.urls.origin).protocol === 'https:' ? '__Host-poppy_join' : 'poppy_join';
|
|
195
|
+
}
|
|
196
|
+
function cookieSecure(poppy) {
|
|
197
|
+
return new URL(poppy.urls.origin).protocol === 'https:' ? 'Secure; ' : '';
|
|
198
|
+
}
|
|
199
|
+
function readCookie(header, name) {
|
|
200
|
+
for (const part of (header ?? '').split(';')) {
|
|
201
|
+
const eq = part.indexOf('=');
|
|
202
|
+
if (eq > 0 && part.slice(0, eq).trim() === name)
|
|
203
|
+
return part.slice(eq + 1).trim();
|
|
204
|
+
}
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
/** Sem cache, sem moldura e sem referência da página que abriu (§6, passo 1). */
|
|
208
|
+
function browserHeaders(ctx) {
|
|
209
|
+
noStore(ctx);
|
|
190
210
|
ctx.response.header('Content-Type', 'text/html; charset=utf-8');
|
|
211
|
+
ctx.response.header('Content-Security-Policy', "frame-ancestors 'none'");
|
|
212
|
+
ctx.response.header('Cross-Origin-Opener-Policy', 'same-origin');
|
|
213
|
+
ctx.response.header('Referrer-Policy', 'no-referrer');
|
|
214
|
+
}
|
|
215
|
+
function badBrowserRequest(ctx) {
|
|
191
216
|
return ctx.response
|
|
192
217
|
.status(400)
|
|
193
218
|
.send('<!doctype html><html><head><meta charset="utf-8"><title>Bad request</title></head><body><h1>Invalid browser session request</h1></body></html>');
|
package/build/src/host/csrf.d.ts
CHANGED
|
@@ -11,10 +11,8 @@ export interface AuthkitCsrfOptions {
|
|
|
11
11
|
*/
|
|
12
12
|
personalAgentsPrefix?: string | false;
|
|
13
13
|
/**
|
|
14
|
-
* Prefixo do Poppy: isenta token/revoke/device authorization
|
|
15
|
-
*
|
|
16
|
-
* Default: o do config. As telas `{prefix}/oauth/authorize` e `{prefix}/device`
|
|
17
|
-
* continuam protegidas. `false` desliga.
|
|
14
|
+
* Prefixo do Poppy: isenta token/revoke/device authorization e a entrada do navegador
|
|
15
|
+
* (o código com o cookie `SameSite=Strict` é o que protege, §6).
|
|
18
16
|
*/
|
|
19
17
|
poppyPrefix?: string | false;
|
|
20
18
|
}
|
package/build/src/host/csrf.js
CHANGED
|
@@ -44,9 +44,8 @@ export function authkitCsrfExceptions(url, options = {}) {
|
|
|
44
44
|
(path === `${poppyPrefix}/oauth/token` ||
|
|
45
45
|
path === `${poppyPrefix}/oauth/revoke` ||
|
|
46
46
|
path === `${poppyPrefix}/oauth/device` ||
|
|
47
|
-
path === `${poppyPrefix}/
|
|
48
|
-
path
|
|
49
|
-
path === `${poppyPrefix}/browser-session`);
|
|
47
|
+
path === `${poppyPrefix}/browser-session` ||
|
|
48
|
+
path === `${poppyPrefix}/browser-session/authorize`);
|
|
50
49
|
return (url.includes(mountPath) ||
|
|
51
50
|
url.includes('/authkit/pat') ||
|
|
52
51
|
(backchannel !== null && url === backchannel) ||
|
|
@@ -29,6 +29,21 @@ export declare const adminGuard: (ctx: any, next: () => Promise<void>) => Promis
|
|
|
29
29
|
* (o `adminGuard` 404a quando `config.admin.enabled` é false). Mantenha-os em
|
|
30
30
|
* sincronia; os guards garantem que a divergência não vire um bypass.
|
|
31
31
|
*/
|
|
32
|
+
/**
|
|
33
|
+
* Rotas do fluxo de login que o host pode querer reforçar por conta própria
|
|
34
|
+
* (throttle mais apertado, WAF, log de auditoria, feature flag…).
|
|
35
|
+
*
|
|
36
|
+
* A lib continua aplicando o throttle padrão (`rateLimit`), mas estas são as
|
|
37
|
+
* rotas em que o host tem contexto para ir além — ex.: limitar `/identifier`
|
|
38
|
+
* por e-mail em vez de só por IP.
|
|
39
|
+
*/
|
|
40
|
+
export type AuthHostRouteKey = 'interaction' | 'identifier' | 'login' | 'mfa' | 'mfaCustomBegin' | 'mfaCustomVerify' | 'passwordExpired' | 'passkeyOptions' | 'passkeyVerify' | 'magic' | 'magicConsume' | 'otpVerify' | 'consent' | 'switchIdentifier';
|
|
41
|
+
/**
|
|
42
|
+
* Middleware do AdonisJS aceito por `route.use` — função, import lazy, o nome de um
|
|
43
|
+
* middleware do router, ou a tupla `[middleware, options]`. Tipado frouxo de propósito:
|
|
44
|
+
* a lib não conhece os middlewares do host e não deve inferir a forma deles.
|
|
45
|
+
*/
|
|
46
|
+
export type AuthHostMiddleware = unknown;
|
|
32
47
|
export interface AuthHostOptions {
|
|
33
48
|
/**
|
|
34
49
|
* Onde o provider OIDC é montado. Deve casar com o final do issuer. OPCIONAL:
|
|
@@ -58,6 +73,26 @@ export interface AuthHostOptions {
|
|
|
58
73
|
* NÃO espelhe o config aqui — omitir a chave já herda dele.
|
|
59
74
|
*/
|
|
60
75
|
rateLimit?: RateLimitConfigInput;
|
|
76
|
+
/**
|
|
77
|
+
* Middleware EXTRA por rota do fluxo de login, aplicado DEPOIS do throttle da lib.
|
|
78
|
+
*
|
|
79
|
+
* É o escape hatch para quem quer política própria sem fork: a rota continua sendo
|
|
80
|
+
* da lib (handler, validação, OIDC), mas o host pendura o que quiser nela —
|
|
81
|
+
* `throttle` do AdonisJS, verificação de origem, auditoria:
|
|
82
|
+
*
|
|
83
|
+
* ```ts
|
|
84
|
+
* registerAuthHost(router, {
|
|
85
|
+
* extraMiddleware: {
|
|
86
|
+
* identifier: [throttle({ limit: 5, window: '1 minute' })],
|
|
87
|
+
* },
|
|
88
|
+
* })
|
|
89
|
+
* ```
|
|
90
|
+
*
|
|
91
|
+
* Não é opção de POLÍTICA (não é travada pelo `defineConfig`): é encanamento.
|
|
92
|
+
* Rodeu antes do throttle da lib? Não: a ordem é lib → host, então o seu middleware
|
|
93
|
+
* vê a requisição depois da barreira padrão.
|
|
94
|
+
*/
|
|
95
|
+
extraMiddleware?: Partial<Record<AuthHostRouteKey, readonly AuthHostMiddleware[]>>;
|
|
61
96
|
/**
|
|
62
97
|
* Console admin opt-in (B6).
|
|
63
98
|
*
|
|
@@ -272,6 +307,24 @@ export interface AuthHostRouteMap {
|
|
|
272
307
|
} | null;
|
|
273
308
|
/** Nomes das rotas nomeadas (as demais herdam o auto-naming do AdonisJS). */
|
|
274
309
|
names: Record<string, string>;
|
|
310
|
+
/**
|
|
311
|
+
* Handles das rotas do fluxo de login (os `Route` do AdonisJS), por chave estável.
|
|
312
|
+
*
|
|
313
|
+
* Serve para o host pendurar middleware DEPOIS do registro (`route.use([...])`) ou
|
|
314
|
+
* inspecionar o que já está aplicado. Para o mesmo efeito de forma declarativa, use
|
|
315
|
+
* a opção `extraMiddleware` ao chamar `registerAuthHost`.
|
|
316
|
+
*/
|
|
317
|
+
routes: Partial<Record<AuthHostRouteKey, ReturnType<Router['post']>>>;
|
|
318
|
+
/** Pattern de cada rota do fluxo (para montar href sem hardcodar). `:uid` incluído. */
|
|
319
|
+
urls: Record<AuthHostRouteKey, string>;
|
|
320
|
+
/**
|
|
321
|
+
* URL concreta de uma rota do fluxo, com o `:uid` já preenchido.
|
|
322
|
+
*
|
|
323
|
+
* ```ts
|
|
324
|
+
* map.interactionUrl('magic', uid) // '/auth/interaction/<uid>/magic'
|
|
325
|
+
* ```
|
|
326
|
+
*/
|
|
327
|
+
interactionUrl: (key: AuthHostRouteKey, uid: string) => string;
|
|
275
328
|
/** Ids dos métodos de sudo cujas rotas foram montadas, na ordem de montagem. */
|
|
276
329
|
sudoMethods: string[];
|
|
277
330
|
/**
|
|
@@ -389,6 +389,7 @@ export function registerAuthHost(router, opts = {}) {
|
|
|
389
389
|
const withLogin = (route) => {
|
|
390
390
|
if (throttles)
|
|
391
391
|
route.use([throttles.login]);
|
|
392
|
+
return route;
|
|
392
393
|
};
|
|
393
394
|
const withIntrospection = (route) => {
|
|
394
395
|
if (throttles)
|
|
@@ -406,6 +407,33 @@ export function registerAuthHost(router, opts = {}) {
|
|
|
406
407
|
const withOtpLogin = (route) => {
|
|
407
408
|
if (throttles)
|
|
408
409
|
route.use([throttles.otpLogin]);
|
|
410
|
+
return route;
|
|
411
|
+
};
|
|
412
|
+
const hostRoutes = {};
|
|
413
|
+
const interactionUrls = {
|
|
414
|
+
interaction: '/auth/interaction/:uid',
|
|
415
|
+
identifier: '/auth/interaction/:uid/identifier',
|
|
416
|
+
login: '/auth/interaction/:uid/login',
|
|
417
|
+
mfa: '/auth/interaction/:uid/mfa',
|
|
418
|
+
mfaCustomBegin: '/auth/interaction/:uid/mfa/custom/:method/begin',
|
|
419
|
+
mfaCustomVerify: '/auth/interaction/:uid/mfa/custom/:method/verify',
|
|
420
|
+
passwordExpired: '/auth/interaction/:uid/password-expired',
|
|
421
|
+
passkeyOptions: '/auth/interaction/:uid/passkey/options',
|
|
422
|
+
passkeyVerify: '/auth/interaction/:uid/passkey/verify',
|
|
423
|
+
magic: '/auth/interaction/:uid/magic',
|
|
424
|
+
magicConsume: '/auth/interaction/:uid/magic',
|
|
425
|
+
otpVerify: '/auth/interaction/:uid/otp-verify',
|
|
426
|
+
consent: '/auth/interaction/:uid/consent',
|
|
427
|
+
switchIdentifier: '/auth/interaction/:uid/switch',
|
|
428
|
+
};
|
|
429
|
+
const keep = (key, route) => {
|
|
430
|
+
hostRoutes[key] = route;
|
|
431
|
+
const extraForRoute = opts.extraMiddleware?.[key];
|
|
432
|
+
if (extraForRoute && extraForRoute.length > 0) {
|
|
433
|
+
// `use` espera o MiddlewareList do AdonisJS; aqui é lista opaca do host.
|
|
434
|
+
route.use([...extraForRoute]);
|
|
435
|
+
}
|
|
436
|
+
return route;
|
|
409
437
|
};
|
|
410
438
|
// ─── Assets estáticos do host-kit (públicos, sem autenticação) ─────────────
|
|
411
439
|
// Bundle do @simplewebauthn/browser servido pelo próprio app, no lugar do
|
|
@@ -452,38 +480,46 @@ export function registerAuthHost(router, opts = {}) {
|
|
|
452
480
|
router.any(`${mount}/*`, [C.oidc]).as('authkit.oidc.wildcard');
|
|
453
481
|
router.any(mount, [C.oidc]).as('authkit.oidc.root');
|
|
454
482
|
// Interaction (login multi-step + consent + signup).
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
483
|
+
// Cada rota passa por `keep(key, …)`: entra em `AuthHostRouteMap.routes`/`urls` e
|
|
484
|
+
// recebe o `extraMiddleware` do host (depois do throttle da lib).
|
|
485
|
+
keep('interaction', router.get('/auth/interaction/:uid', [C.interaction, 'show']));
|
|
486
|
+
// `/identifier` é a ENTRADA do fluxo: recebe só o e-mail e abre/avança a interaction.
|
|
487
|
+
// Sem throttle, um anônimo cria estado de interaction sem limite (Redis) e o passo
|
|
488
|
+
// fica de fora do orçamento que o resto do login respeita. Bucket `login` (por IP),
|
|
489
|
+
// o mesmo do `/magic`: um login normal gasta 2 dos 10 pontos por minuto.
|
|
490
|
+
keep('identifier', withLogin(router.post('/auth/interaction/:uid/identifier', [C.interaction, 'identifier'])));
|
|
491
|
+
withLogin(keep('login', router.post('/auth/interaction/:uid/login', [C.interaction, 'login'])));
|
|
492
|
+
withLogin(keep('mfa', router.post('/auth/interaction/:uid/mfa', [C.interaction, 'mfaVerify'])));
|
|
493
|
+
const customMfaBeginRoute = keep('mfaCustomBegin', router.post('/auth/interaction/:uid/mfa/custom/:method/begin', [
|
|
460
494
|
C.interaction,
|
|
461
495
|
'customMfaBegin',
|
|
462
|
-
]);
|
|
496
|
+
]));
|
|
463
497
|
withLogin(customMfaBeginRoute);
|
|
464
498
|
customMfaBeginRoute.as('authkit.mfa.custom.begin');
|
|
465
|
-
const customMfaVerifyRoute = router.post('/auth/interaction/:uid/mfa/custom/:method/verify', [
|
|
499
|
+
const customMfaVerifyRoute = keep('mfaCustomVerify', router.post('/auth/interaction/:uid/mfa/custom/:method/verify', [
|
|
466
500
|
C.interaction,
|
|
467
501
|
'customMfaVerify',
|
|
468
|
-
]);
|
|
502
|
+
]));
|
|
469
503
|
withLogin(customMfaVerifyRoute);
|
|
470
504
|
customMfaVerifyRoute.as('authkit.mfa.custom.verify');
|
|
471
505
|
// Troca de senha obrigatória quando a senha expirou (password expiration gate).
|
|
472
|
-
withLogin(router.post('/auth/interaction/:uid/password-expired', [
|
|
506
|
+
withLogin(keep('passwordExpired', router.post('/auth/interaction/:uid/password-expired', [
|
|
473
507
|
C.interaction,
|
|
474
508
|
'changeExpiredPassword',
|
|
475
|
-
]));
|
|
509
|
+
])));
|
|
476
510
|
// Passkey como 2º fator alternativo no login (begin/finish; challenge na sessão).
|
|
477
|
-
|
|
478
|
-
|
|
511
|
+
// O `options` também cria estado (challenge) e é anônimo: mesmo bucket `login` do
|
|
512
|
+
// `/identifier` e do `/magic`. Um login normal gasta 1 ponto aqui quando usa passkey.
|
|
513
|
+
keep('passkeyOptions', withLogin(router.post('/auth/interaction/:uid/passkey/options', [C.interaction, 'passkeyOptions'])));
|
|
514
|
+
withLogin(keep('passkeyVerify', router.post('/auth/interaction/:uid/passkey/verify', [C.interaction, 'passkeyVerify'])));
|
|
479
515
|
// Magic link (passwordless): POST emite (throttled), GET consome o token do link.
|
|
480
|
-
withLogin(router.post('/auth/interaction/:uid/magic', [C.interaction, 'magicLinkRequest']));
|
|
481
|
-
router.get('/auth/interaction/:uid/magic', [C.interaction, 'magicLinkConsume']);
|
|
516
|
+
withLogin(keep('magic', router.post('/auth/interaction/:uid/magic', [C.interaction, 'magicLinkRequest'])));
|
|
517
|
+
keep('magicConsume', router.get('/auth/interaction/:uid/magic', [C.interaction, 'magicLinkConsume']));
|
|
482
518
|
// Login por OTP (código digitável): verifica o código no bucket dedicado
|
|
483
519
|
// `authkit_otp_login` (por IP, mais apertado que o login).
|
|
484
|
-
withOtpLogin(router.post('/auth/interaction/:uid/otp-verify', [C.interaction, 'otpVerify']));
|
|
485
|
-
router.post('/auth/interaction/:uid/consent', [C.interaction, 'consent']);
|
|
486
|
-
router.get('/auth/interaction/:uid/switch', [C.interaction, 'switchIdentifier']);
|
|
520
|
+
keep('otpVerify', withOtpLogin(router.post('/auth/interaction/:uid/otp-verify', [C.interaction, 'otpVerify'])));
|
|
521
|
+
keep('consent', router.post('/auth/interaction/:uid/consent', [C.interaction, 'consent']));
|
|
522
|
+
keep('switchIdentifier', router.get('/auth/interaction/:uid/switch', [C.interaction, 'switchIdentifier']));
|
|
487
523
|
// OTP unlock: link enviado por e-mail quando o fator TOTP/recovery é travado.
|
|
488
524
|
router.get('/auth/otp-unlock/:token', [C.interaction, 'otpUnlock']);
|
|
489
525
|
router.get('/auth/interaction/:uid/signup', [C.registration, 'showSignup']);
|
|
@@ -537,6 +573,9 @@ export function registerAuthHost(router, opts = {}) {
|
|
|
537
573
|
// Nomes explícitos: o Router nomeia pela dupla controller.método, e as duas URLs da metadata
|
|
538
574
|
// (RFC 8414 na raiz e debaixo do issuer) usam o mesmo — "poppy.metadata already exists" no boot.
|
|
539
575
|
router.get('/.well-known/poppy.json', [C.poppy, 'discovery']).as('authkit.poppy.discovery');
|
|
576
|
+
router
|
|
577
|
+
.get('/.well-known/oauth-protected-resource', [C.poppy, 'protectedResource'])
|
|
578
|
+
.as('authkit.poppy.protected_resource');
|
|
540
579
|
router
|
|
541
580
|
.get(`/.well-known/oauth-authorization-server${poppyPrefix}`, [C.poppy, 'metadata'])
|
|
542
581
|
.as('authkit.poppy.metadata');
|
|
@@ -548,14 +587,16 @@ export function registerAuthHost(router, opts = {}) {
|
|
|
548
587
|
router
|
|
549
588
|
.post(`${poppyPrefix}/oauth/device`, [C.poppy, 'deviceAuthorization'])
|
|
550
589
|
.as('authkit.poppy.device_authorization');
|
|
551
|
-
//
|
|
552
|
-
withLogin(router
|
|
553
|
-
|
|
554
|
-
.
|
|
555
|
-
.as('authkit.poppy.sign_in_code'));
|
|
590
|
+
// A página grava um pedido sem autenticação: throttle por IP.
|
|
591
|
+
withLogin(router
|
|
592
|
+
.get(`${poppyPrefix}/browser-session`, [C.poppy, 'browserSessionPage'])
|
|
593
|
+
.as('authkit.poppy.browser_session_page'));
|
|
556
594
|
router
|
|
557
595
|
.post(`${poppyPrefix}/browser-session`, [C.poppy, 'browserSession'])
|
|
558
596
|
.as('authkit.poppy.browser_session');
|
|
597
|
+
router
|
|
598
|
+
.post(`${poppyPrefix}/browser-session/authorize`, [C.poppy, 'browserSessionAuthorize'])
|
|
599
|
+
.as('authkit.poppy.browser_session_authorize');
|
|
559
600
|
router
|
|
560
601
|
.group(() => {
|
|
561
602
|
router
|
|
@@ -1026,6 +1067,9 @@ export function registerAuthHost(router, opts = {}) {
|
|
|
1026
1067
|
// inteiro, e não só metade (certo no servidor, errado na UI).
|
|
1027
1068
|
return {
|
|
1028
1069
|
mountPath: mount,
|
|
1070
|
+
routes: hostRoutes,
|
|
1071
|
+
urls: interactionUrls,
|
|
1072
|
+
interactionUrl: (key, uid) => interactionUrls[key].replace(':uid', encodeURIComponent(uid)),
|
|
1029
1073
|
account: {
|
|
1030
1074
|
prefix: accountPrefix(),
|
|
1031
1075
|
api: apiBase,
|
|
@@ -334,22 +334,23 @@ const TABLES = [
|
|
|
334
334
|
},
|
|
335
335
|
},
|
|
336
336
|
/*
|
|
337
|
-
* Personal Agent Protocol (Poppy). Os
|
|
337
|
+
* Personal Agent Protocol (Poppy). Os refresh tokens são grants de
|
|
338
338
|
* `auth_agent_grants` (prefixo `pgrant_`) com o hash em
|
|
339
|
-
* `auth_agent_refresh_tokens`; aqui ficam Sessions,
|
|
339
|
+
* `auth_agent_refresh_tokens`; aqui ficam Sessions, access tokens, pedidos de
|
|
340
340
|
* sign-in e os `jti` vistos. Mesmas regras de data das tabelas acima.
|
|
341
341
|
*/
|
|
342
342
|
{
|
|
343
343
|
name: 'auth_poppy_sessions',
|
|
344
344
|
/**
|
|
345
|
-
* Uma Session: um usuário do agente (`user_id`, opaco) num agente
|
|
346
|
-
*
|
|
347
|
-
*
|
|
345
|
+
* Uma Session: um usuário do agente (`user_id`, opaco) num agente (`client_id`), com o
|
|
346
|
+
* `agent_id` opcional. `account_id` é a conta que já usou. `grant_id` e `scope` ficaram da
|
|
347
|
+
* Draft 0.1, sem uso (o schema só é aditivo).
|
|
348
348
|
*/
|
|
349
349
|
create: (t) => {
|
|
350
350
|
t.string('id').primary();
|
|
351
351
|
t.string('client_id', 2048).notNullable();
|
|
352
352
|
t.string('user_id').notNullable();
|
|
353
|
+
t.string('agent_id').nullable();
|
|
353
354
|
t.string('account_id').nullable().index();
|
|
354
355
|
t.string('grant_id').nullable().index();
|
|
355
356
|
t.text('scope').notNullable();
|
|
@@ -361,6 +362,7 @@ const TABLES = [
|
|
|
361
362
|
columns: {
|
|
362
363
|
client_id: (t) => t.string('client_id', 2048),
|
|
363
364
|
user_id: (t) => t.string('user_id'),
|
|
365
|
+
agent_id: (t) => t.string('agent_id').nullable(),
|
|
364
366
|
account_id: (t) => t.string('account_id').nullable().index(),
|
|
365
367
|
grant_id: (t) => t.string('grant_id').nullable().index(),
|
|
366
368
|
scope: (t) => t.text('scope'),
|
|
@@ -372,7 +374,10 @@ const TABLES = [
|
|
|
372
374
|
},
|
|
373
375
|
{
|
|
374
376
|
name: 'auth_poppy_tokens',
|
|
375
|
-
/**
|
|
377
|
+
/**
|
|
378
|
+
* Access tokens — só o hash. `jkt` = chave DPoP; `null` = Bearer. `session_id` e `user_id`
|
|
379
|
+
* ficaram da Draft 0.1 e saem vazios: o sign-in não conhece a Session.
|
|
380
|
+
*/
|
|
376
381
|
create: (t) => {
|
|
377
382
|
t.string('token_hash', 64).primary();
|
|
378
383
|
t.string('session_id').notNullable().index();
|
|
@@ -402,8 +407,8 @@ const TABLES = [
|
|
|
402
407
|
{
|
|
403
408
|
name: 'auth_poppy_requests',
|
|
404
409
|
/**
|
|
405
|
-
* Pedidos
|
|
406
|
-
* `
|
|
410
|
+
* Pedidos: `authorize` (Direct — vira código após o consentimento), `device` (RFC 8628),
|
|
411
|
+
* `mediated` (aguardando o código de uso único) e `browser` (entrada de um navegador, §6).
|
|
407
412
|
* Códigos só como hash. Linhas expiradas são apagadas no próximo pedido.
|
|
408
413
|
*/
|
|
409
414
|
create: (t) => {
|
|
@@ -448,7 +453,7 @@ const TABLES = [
|
|
|
448
453
|
},
|
|
449
454
|
{
|
|
450
455
|
name: 'auth_poppy_jtis',
|
|
451
|
-
/** `jti` já aceitos (
|
|
456
|
+
/** `jti` já aceitos (client assertions, Session JWTs, provas DPoP), até expirarem — anti-replay entre instâncias. */
|
|
452
457
|
create: (t) => {
|
|
453
458
|
t.string('key', 100).primary();
|
|
454
459
|
t.dateTime('expires_at', { useTz: true, precision: 3 }).notNullable().index();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adonis-agora/authkit-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.87.0",
|
|
4
4
|
"description": "AdonisJS OIDC/OAuth2 provider (Identity Provider) toolkit: ejectable auth server with sessions, rate-limiting, MFA/TOTP, audit log, federated logout and OpenTelemetry metrics.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "dudousxd",
|
|
@@ -117,7 +117,7 @@
|
|
|
117
117
|
},
|
|
118
118
|
"devDependencies": {
|
|
119
119
|
"@adonis-agora/authkit-react": "0.26.0",
|
|
120
|
-
"@adonis-agora/durable": "0.
|
|
120
|
+
"@adonis-agora/durable": "0.54.1",
|
|
121
121
|
"@adonis-agora/telescope": "0.23.0",
|
|
122
122
|
"@adonisjs/ally": "6.3.2",
|
|
123
123
|
"@adonisjs/auth": "10.1.0",
|
|
@@ -133,7 +133,7 @@
|
|
|
133
133
|
"@poppinss/ts-exec": "1.5.1",
|
|
134
134
|
"@tailwindcss/cli": "4.3.3",
|
|
135
135
|
"@tanstack/intent": "0.5.6",
|
|
136
|
-
"@tanstack/react-query": "5.
|
|
136
|
+
"@tanstack/react-query": "5.105.0",
|
|
137
137
|
"@types/better-sqlite3": "9.6.0",
|
|
138
138
|
"@types/ioredis-mock": "8.2.8",
|
|
139
139
|
"@types/koa": "3.0.3",
|