@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
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { PoppyClient } from './client_registry.js';
|
|
2
|
+
export declare const SESSION_JWT_TYP = "poppy-session+jwt";
|
|
3
|
+
export declare const DELEGATION_TYP = "pap-delegation+jwt";
|
|
4
|
+
export declare class SessionJwtError extends Error {
|
|
5
|
+
}
|
|
6
|
+
export interface VerifiedSessionJwt {
|
|
7
|
+
clientId: string;
|
|
8
|
+
sid: string;
|
|
9
|
+
userId: string;
|
|
10
|
+
agentId: string | null;
|
|
11
|
+
jti: string;
|
|
12
|
+
iat: number;
|
|
13
|
+
}
|
|
14
|
+
export interface VerifySessionJwtInput {
|
|
15
|
+
/** Valor do(s) header(s) `Poppy-Session`. */
|
|
16
|
+
header: string | string[] | undefined;
|
|
17
|
+
method: string;
|
|
18
|
+
url: string;
|
|
19
|
+
/** Access token da request, ou `null`. */
|
|
20
|
+
accessToken: string | null;
|
|
21
|
+
/** O identificador da empresa: o issuer (§3.2). */
|
|
22
|
+
audience: string;
|
|
23
|
+
/** Janela de aceitação do `iat`, em segundos. */
|
|
24
|
+
maxAge: number;
|
|
25
|
+
/** Resolve o agente pelo `iss` (client metadata + `jwks_uri`). */
|
|
26
|
+
resolveClient: (clientId: unknown) => Promise<PoppyClient>;
|
|
27
|
+
now: Date;
|
|
28
|
+
}
|
|
29
|
+
/** Verifica o JWT (§5.3, passos 1–3). Lança {@link SessionJwtError}. */
|
|
30
|
+
export declare function verifySessionJwt(input: VerifySessionJwtInput): Promise<VerifiedSessionJwt>;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* O header `Poppy-Session` (Draft 0.3, §5.2–§5.3): um JWT que o personal agent assina para uma
|
|
3
|
+
* request, com uma chave delegada. Uma chave do `jwks_uri` assina a delegação
|
|
4
|
+
* (`pap-delegation+jwt`), que vai no header `pap_dlg` do JWT e atesta a chave delegada em `cnf.jwk`.
|
|
5
|
+
*
|
|
6
|
+
* Só as checagens do JWT em si: o `client_id` do token, a Session e o replay ficam no `PoppyService`.
|
|
7
|
+
*/
|
|
8
|
+
import { calculateJwkThumbprint, decodeJwt, decodeProtectedHeader, importJWK, jwtVerify, } from 'jose';
|
|
9
|
+
import { accessTokenHash, normalizeHtu } from './dpop.js';
|
|
10
|
+
export const SESSION_JWT_TYP = 'poppy-session+jwt';
|
|
11
|
+
export const DELEGATION_TYP = 'pap-delegation+jwt';
|
|
12
|
+
/** Delegação: `ES256` obrigatório; `PS384` permite a assinatura cega (§5.2, §10.7). */
|
|
13
|
+
const DELEGATION_ALGS = ['ES256', 'PS384'];
|
|
14
|
+
const DELEGATION_MAX_LIFETIME_S = 24 * 3600;
|
|
15
|
+
const CLOCK_SKEW_S = 5;
|
|
16
|
+
/** Mesmos caracteres dos Conversation IDs (§8.2). */
|
|
17
|
+
const SESSION_ID = /^[A-Za-z0-9_-]{1,256}$/;
|
|
18
|
+
/** Tamanho da coluna `user_id`/`agent_id`. */
|
|
19
|
+
const MAX_ID_LENGTH = 255;
|
|
20
|
+
export class SessionJwtError extends Error {
|
|
21
|
+
}
|
|
22
|
+
const fail = (why) => new SessionJwtError(why);
|
|
23
|
+
/** Passos 1–4 de "Delegated keys" (§5.2): devolve a chave delegada, já conferida. */
|
|
24
|
+
async function verifyDelegation(delegation, client, now) {
|
|
25
|
+
if (typeof delegation !== 'string' || !delegation)
|
|
26
|
+
throw fail('pap_dlg is required');
|
|
27
|
+
let payload;
|
|
28
|
+
try {
|
|
29
|
+
({ payload } = await jwtVerify(delegation, client.keys, {
|
|
30
|
+
algorithms: DELEGATION_ALGS,
|
|
31
|
+
typ: DELEGATION_TYP,
|
|
32
|
+
issuer: client.clientId,
|
|
33
|
+
currentDate: now,
|
|
34
|
+
clockTolerance: CLOCK_SKEW_S,
|
|
35
|
+
requiredClaims: ['iat', 'exp', 'jti'],
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
throw fail('delegation verification failed');
|
|
40
|
+
}
|
|
41
|
+
if (payload.exp - payload.iat > DELEGATION_MAX_LIFETIME_S) {
|
|
42
|
+
throw fail('delegation lasts more than 24 hours');
|
|
43
|
+
}
|
|
44
|
+
const jwk = payload.cnf?.jwk;
|
|
45
|
+
if (!jwk || jwk.kty !== 'EC' || jwk.crv !== 'P-256' || 'd' in jwk) {
|
|
46
|
+
throw fail('cnf.jwk must be a public P-256 key');
|
|
47
|
+
}
|
|
48
|
+
return { jwk: jwk, exp: payload.exp };
|
|
49
|
+
}
|
|
50
|
+
/** Verifica o JWT (§5.3, passos 1–3). Lança {@link SessionJwtError}. */
|
|
51
|
+
export async function verifySessionJwt(input) {
|
|
52
|
+
const token = input.header;
|
|
53
|
+
if (typeof token !== 'string' || !token)
|
|
54
|
+
throw fail('exactly one Poppy-Session header required');
|
|
55
|
+
let header;
|
|
56
|
+
let iss;
|
|
57
|
+
try {
|
|
58
|
+
header = decodeProtectedHeader(token);
|
|
59
|
+
iss = decodeJwt(token).iss;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
throw fail('malformed Session JWT');
|
|
63
|
+
}
|
|
64
|
+
if (header.typ !== SESSION_JWT_TYP)
|
|
65
|
+
throw fail(`typ must be ${SESSION_JWT_TYP}`);
|
|
66
|
+
if (header.alg !== 'ES256')
|
|
67
|
+
throw fail('alg must be ES256');
|
|
68
|
+
let client;
|
|
69
|
+
try {
|
|
70
|
+
client = await input.resolveClient(iss);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
throw fail('the client_id was refused');
|
|
74
|
+
}
|
|
75
|
+
const delegation = await verifyDelegation(header.pap_dlg, client, input.now);
|
|
76
|
+
if (header.kid !== (await calculateJwkThumbprint(delegation.jwk, 'sha256'))) {
|
|
77
|
+
throw fail('kid must be the thumbprint of the delegated key');
|
|
78
|
+
}
|
|
79
|
+
let claims;
|
|
80
|
+
try {
|
|
81
|
+
const key = await importJWK(delegation.jwk, 'ES256');
|
|
82
|
+
({ payload: claims } = await jwtVerify(token, key, {
|
|
83
|
+
algorithms: ['ES256'],
|
|
84
|
+
typ: SESSION_JWT_TYP,
|
|
85
|
+
issuer: client.clientId,
|
|
86
|
+
currentDate: input.now,
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
throw fail('Session JWT signature verification failed');
|
|
91
|
+
}
|
|
92
|
+
if (typeof claims.aud !== 'string' || claims.aud !== input.audience) {
|
|
93
|
+
throw fail('aud must be the Company identifier, as a single string');
|
|
94
|
+
}
|
|
95
|
+
if (claims.htm !== input.method.toUpperCase())
|
|
96
|
+
throw fail('htm does not match the request');
|
|
97
|
+
const htu = typeof claims.htu === 'string' ? normalizeHtu(claims.htu) : null;
|
|
98
|
+
if (htu === null || htu !== normalizeHtu(input.url))
|
|
99
|
+
throw fail('htu does not match the request');
|
|
100
|
+
const nowS = Math.floor(input.now.getTime() / 1000);
|
|
101
|
+
if (typeof claims.iat !== 'number' || Math.abs(nowS - claims.iat) > input.maxAge) {
|
|
102
|
+
throw fail('iat is outside the acceptance window');
|
|
103
|
+
}
|
|
104
|
+
if (claims.iat > delegation.exp)
|
|
105
|
+
throw fail('signed after the delegation expired');
|
|
106
|
+
if (typeof claims.jti !== 'string' || !claims.jti)
|
|
107
|
+
throw fail('jti is required');
|
|
108
|
+
if (typeof claims.sid !== 'string' || !SESSION_ID.test(claims.sid))
|
|
109
|
+
throw fail('invalid sid');
|
|
110
|
+
if (typeof claims.sub !== 'string' || !claims.sub || claims.sub.length > MAX_ID_LENGTH) {
|
|
111
|
+
throw fail('sub (the User ID) is required');
|
|
112
|
+
}
|
|
113
|
+
const agentId = claims.agent_id;
|
|
114
|
+
if (agentId !== undefined &&
|
|
115
|
+
(typeof agentId !== 'string' || !agentId || agentId.length > MAX_ID_LENGTH)) {
|
|
116
|
+
throw fail('invalid agent_id');
|
|
117
|
+
}
|
|
118
|
+
if (input.accessToken === null) {
|
|
119
|
+
if (claims.ath !== undefined)
|
|
120
|
+
throw fail('ath must be absent without an access token');
|
|
121
|
+
}
|
|
122
|
+
else if (claims.ath !== accessTokenHash(input.accessToken)) {
|
|
123
|
+
throw fail('ath does not match the access token');
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
clientId: client.clientId,
|
|
127
|
+
sid: claims.sid,
|
|
128
|
+
userId: claims.sub,
|
|
129
|
+
agentId: agentId ?? null,
|
|
130
|
+
jti: claims.jti,
|
|
131
|
+
iat: claims.iat,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Persistência do Poppy — tabelas LIB-OWNED (ver `schema/ensure.ts`): Sessions,
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* `auth_agent_grants` + `auth_agent_refresh_tokens` (ver `DelegationStore`).
|
|
2
|
+
* Persistência do Poppy — tabelas LIB-OWNED (ver `schema/ensure.ts`): Sessions, access tokens (só o
|
|
3
|
+
* hash), pedidos de sign-in e de entrada de navegador, e `jti` vistos. Os refresh tokens
|
|
4
|
+
* reaproveitam `auth_agent_grants` + `auth_agent_refresh_tokens` (ver `DelegationStore`).
|
|
6
5
|
*
|
|
7
6
|
* Mesmas regras do `DelegationStore`: query builder puro, datas comparadas em JS
|
|
8
7
|
* depois de buscar a linha pela chave, transições de estado atômicas por
|
|
@@ -12,12 +11,14 @@ export declare const SESSION_TABLE = "auth_poppy_sessions";
|
|
|
12
11
|
export declare const TOKEN_TABLE = "auth_poppy_tokens";
|
|
13
12
|
export declare const REQUEST_TABLE = "auth_poppy_requests";
|
|
14
13
|
export interface SessionRow {
|
|
14
|
+
/** Derivado de `client_id` + `sid` (§5.3). */
|
|
15
15
|
id: string;
|
|
16
16
|
clientId: string;
|
|
17
17
|
userId: string;
|
|
18
|
-
|
|
18
|
+
agentId: string | null;
|
|
19
|
+
/** Conta que a Session já usou: recusamos outra com `invalid_session` (§5.4). */
|
|
19
20
|
accountId: string | null;
|
|
20
|
-
/**
|
|
21
|
+
/** Colunas da Draft 0.1, sem uso: o acesso à conta não passa pela Session. */
|
|
21
22
|
grantId: string | null;
|
|
22
23
|
scope: string;
|
|
23
24
|
expiresAt: Date;
|
|
@@ -25,21 +26,20 @@ export interface SessionRow {
|
|
|
25
26
|
createdAt: Date;
|
|
26
27
|
updatedAt: Date;
|
|
27
28
|
}
|
|
29
|
+
/** Access token (§4.5). */
|
|
28
30
|
export interface TokenRow {
|
|
29
31
|
tokenHash: string;
|
|
30
|
-
sessionId: string;
|
|
31
32
|
clientId: string;
|
|
32
|
-
userId: string;
|
|
33
33
|
accountId: string | null;
|
|
34
34
|
grantId: string | null;
|
|
35
35
|
scope: string;
|
|
36
36
|
resource: string | null;
|
|
37
|
-
/** Thumbprint da chave DPoP; `null` = Bearer
|
|
37
|
+
/** Thumbprint da chave DPoP; `null` = Bearer. */
|
|
38
38
|
jkt: string | null;
|
|
39
39
|
expiresAt: Date;
|
|
40
40
|
createdAt: Date;
|
|
41
41
|
}
|
|
42
|
-
export type RequestKind = 'authorize' | 'device' | 'mediated';
|
|
42
|
+
export type RequestKind = 'authorize' | 'device' | 'mediated' | 'browser';
|
|
43
43
|
export type RequestStatus = 'pending' | 'approved' | 'denied' | 'consumed' | 'failed';
|
|
44
44
|
export interface RequestRow {
|
|
45
45
|
id: string;
|
|
@@ -70,19 +70,11 @@ export declare class PoppyStore {
|
|
|
70
70
|
/** Estende a vida da Session (uso). */
|
|
71
71
|
touchSession(id: string, expiresAt: Date, at: Date): Promise<void>;
|
|
72
72
|
/**
|
|
73
|
-
*
|
|
74
|
-
*
|
|
73
|
+
* Registra a conta que a Session usa (§5.4). Atômico: só se nenhuma foi registrada ou é a MESMA.
|
|
74
|
+
* `false` = outra conta chegou nesse meio-tempo.
|
|
75
75
|
*/
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
grantId: string;
|
|
79
|
-
scope: string;
|
|
80
|
-
expiresAt: Date;
|
|
81
|
-
at: Date;
|
|
82
|
-
}): Promise<boolean>;
|
|
83
|
-
/** Sign-out (§4.9): as Sessions logadas com o grant continuam, deslogadas. */
|
|
84
|
-
signOutSessionsOfGrant(grantId: string, at: Date): Promise<number>;
|
|
85
|
-
/** Encerra todas as Sessions de um agente (bloqueio no console): os tokens param na hora. */
|
|
76
|
+
bindSessionAccount(id: string, accountId: string, at: Date): Promise<boolean>;
|
|
77
|
+
/** Encerra todas as Sessions de um agente (bloqueio no console): param na hora. */
|
|
86
78
|
endSessionsOfClient(clientId: string, at: Date): Promise<number>;
|
|
87
79
|
insertToken(row: TokenRow): Promise<void>;
|
|
88
80
|
findToken(hash: string): Promise<TokenRow | null>;
|
|
@@ -102,6 +94,9 @@ export declare class PoppyStore {
|
|
|
102
94
|
codeHash?: string;
|
|
103
95
|
attempts?: number;
|
|
104
96
|
expiresAt?: Date;
|
|
97
|
+
clientId?: string;
|
|
98
|
+
sessionId?: string;
|
|
99
|
+
data?: Record<string, unknown>;
|
|
105
100
|
}): Promise<boolean>;
|
|
106
101
|
/** Conta uma tentativa errada (atômico sobre o valor lido). */
|
|
107
102
|
bumpAttempts(id: string, from: number): Promise<boolean>;
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Persistência do Poppy — tabelas LIB-OWNED (ver `schema/ensure.ts`): Sessions,
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* `auth_agent_grants` + `auth_agent_refresh_tokens` (ver `DelegationStore`).
|
|
2
|
+
* Persistência do Poppy — tabelas LIB-OWNED (ver `schema/ensure.ts`): Sessions, access tokens (só o
|
|
3
|
+
* hash), pedidos de sign-in e de entrada de navegador, e `jti` vistos. Os refresh tokens
|
|
4
|
+
* reaproveitam `auth_agent_grants` + `auth_agent_refresh_tokens` (ver `DelegationStore`).
|
|
6
5
|
*
|
|
7
6
|
* Mesmas regras do `DelegationStore`: query builder puro, datas comparadas em JS
|
|
8
7
|
* depois de buscar a linha pela chave, transições de estado atômicas por
|
|
@@ -35,6 +34,7 @@ function toSession(row) {
|
|
|
35
34
|
id: row.id,
|
|
36
35
|
clientId: row.client_id,
|
|
37
36
|
userId: row.user_id,
|
|
37
|
+
agentId: row.agent_id ?? null,
|
|
38
38
|
accountId: row.account_id ?? null,
|
|
39
39
|
grantId: row.grant_id ?? null,
|
|
40
40
|
scope: row.scope ?? '',
|
|
@@ -47,9 +47,7 @@ function toSession(row) {
|
|
|
47
47
|
function toToken(row) {
|
|
48
48
|
return {
|
|
49
49
|
tokenHash: row.token_hash,
|
|
50
|
-
sessionId: row.session_id,
|
|
51
50
|
clientId: row.client_id,
|
|
52
|
-
userId: row.user_id,
|
|
53
51
|
accountId: row.account_id ?? null,
|
|
54
52
|
grantId: row.grant_id ?? null,
|
|
55
53
|
scope: row.scope ?? '',
|
|
@@ -103,6 +101,7 @@ export class PoppyStore {
|
|
|
103
101
|
id: row.id,
|
|
104
102
|
client_id: row.clientId,
|
|
105
103
|
user_id: row.userId,
|
|
104
|
+
agent_id: row.agentId,
|
|
106
105
|
account_id: row.accountId,
|
|
107
106
|
grant_id: row.grantId,
|
|
108
107
|
scope: row.scope,
|
|
@@ -126,33 +125,18 @@ export class PoppyStore {
|
|
|
126
125
|
.update({ expires_at: expiresAt, updated_at: at });
|
|
127
126
|
}
|
|
128
127
|
/**
|
|
129
|
-
*
|
|
130
|
-
*
|
|
128
|
+
* Registra a conta que a Session usa (§5.4). Atômico: só se nenhuma foi registrada ou é a MESMA.
|
|
129
|
+
* `false` = outra conta chegou nesse meio-tempo.
|
|
131
130
|
*/
|
|
132
|
-
async
|
|
131
|
+
async bindSessionAccount(id, accountId, at) {
|
|
133
132
|
const result = await this.conn()
|
|
134
133
|
.from(SESSION_TABLE)
|
|
135
134
|
.where('id', id)
|
|
136
|
-
.whereNull('
|
|
137
|
-
.
|
|
138
|
-
.update({
|
|
139
|
-
account_id: patch.accountId,
|
|
140
|
-
grant_id: patch.grantId,
|
|
141
|
-
scope: patch.scope,
|
|
142
|
-
expires_at: patch.expiresAt,
|
|
143
|
-
updated_at: patch.at,
|
|
144
|
-
});
|
|
135
|
+
.where((q) => q.whereNull('account_id').orWhere('account_id', accountId))
|
|
136
|
+
.update({ account_id: accountId, updated_at: at });
|
|
145
137
|
return affected(result) > 0;
|
|
146
138
|
}
|
|
147
|
-
/**
|
|
148
|
-
async signOutSessionsOfGrant(grantId, at) {
|
|
149
|
-
const result = await this.conn()
|
|
150
|
-
.from(SESSION_TABLE)
|
|
151
|
-
.where('grant_id', grantId)
|
|
152
|
-
.update({ grant_id: null, scope: '', updated_at: at });
|
|
153
|
-
return affected(result);
|
|
154
|
-
}
|
|
155
|
-
/** Encerra todas as Sessions de um agente (bloqueio no console): os tokens param na hora. */
|
|
139
|
+
/** Encerra todas as Sessions de um agente (bloqueio no console): param na hora. */
|
|
156
140
|
async endSessionsOfClient(clientId, at) {
|
|
157
141
|
const result = await this.conn()
|
|
158
142
|
.from(SESSION_TABLE)
|
|
@@ -161,13 +145,15 @@ export class PoppyStore {
|
|
|
161
145
|
.update({ ended_at: at, grant_id: null, scope: '', updated_at: at });
|
|
162
146
|
return affected(result);
|
|
163
147
|
}
|
|
164
|
-
// ───
|
|
148
|
+
// ─── access tokens ────────────────────────────────────────────────────────
|
|
165
149
|
async insertToken(row) {
|
|
166
150
|
await this.conn().table(TOKEN_TABLE).insert({
|
|
167
151
|
token_hash: row.tokenHash,
|
|
168
|
-
session_id
|
|
152
|
+
// `session_id`/`user_id` são NOT NULL desde a Draft 0.1 e o schema só é aditivo; o sign-in
|
|
153
|
+
// não conhece a Session nem o User ID (§4.4).
|
|
154
|
+
session_id: '',
|
|
169
155
|
client_id: row.clientId,
|
|
170
|
-
user_id:
|
|
156
|
+
user_id: '',
|
|
171
157
|
account_id: row.accountId,
|
|
172
158
|
grant_id: row.grantId,
|
|
173
159
|
scope: row.scope,
|
|
@@ -238,6 +224,12 @@ export class PoppyStore {
|
|
|
238
224
|
update.attempts = patch.attempts;
|
|
239
225
|
if (patch.expiresAt !== undefined)
|
|
240
226
|
update.expires_at = patch.expiresAt;
|
|
227
|
+
if (patch.clientId !== undefined)
|
|
228
|
+
update.client_id = patch.clientId;
|
|
229
|
+
if (patch.sessionId !== undefined)
|
|
230
|
+
update.session_id = patch.sessionId;
|
|
231
|
+
if (patch.data !== undefined)
|
|
232
|
+
update.data = JSON.stringify(patch.data);
|
|
241
233
|
const result = await this.conn()
|
|
242
234
|
.from(REQUEST_TABLE)
|
|
243
235
|
.where('id', id)
|
|
@@ -1,32 +1,39 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* O cookie do site para o navegador de um personal agent (§
|
|
3
|
-
*
|
|
2
|
+
* O cookie do site para o navegador de um personal agent (§6): a entrada na Session, e o
|
|
3
|
+
* acompanhamento do acesso à conta autorizado nela a cada request.
|
|
4
4
|
*/
|
|
5
5
|
import type { HttpContext } from '@adonisjs/core/http';
|
|
6
|
-
import type {
|
|
6
|
+
import type { PoppyBrowserJoin, PoppySessionState } from './service.js';
|
|
7
7
|
export interface PoppyWebSessionMarker {
|
|
8
8
|
sessionId: string;
|
|
9
9
|
clientId: string;
|
|
10
10
|
userId: string;
|
|
11
|
+
/** Conta, grant e scopes autorizados na entrada (§6, passo 2); `null` = entrada deslogada. */
|
|
12
|
+
accountId: string | null;
|
|
13
|
+
grantId: string | null;
|
|
14
|
+
scopes: string[];
|
|
11
15
|
}
|
|
12
16
|
/**
|
|
13
|
-
* Liga o navegador à Session: regenera a sessão do app (session fixation)
|
|
14
|
-
*
|
|
15
|
-
* mecanismo do app — o hook `web.login`, ou a sessão de conta do authkit +
|
|
16
|
-
* `adonisAuth.guard`.
|
|
17
|
+
* Liga o navegador à Session (§6, passo 3): regenera a sessão do app (session fixation) e a marca
|
|
18
|
+
* como do agente. Uma entrada com access token faz o login na conta autorizada, no lugar de qualquer
|
|
19
|
+
* outro, com o mecanismo do app — o hook `web.login`, ou a sessão de conta do authkit +
|
|
20
|
+
* `adonisAuth.guard`. Uma entrada sem access token não mexe no login do navegador — a não ser que
|
|
21
|
+
* uma entrada anterior o tenha logado: aí o sign-in dela termina aqui.
|
|
22
|
+
*/
|
|
23
|
+
export declare function loginPoppyBrowser(httpCtx: HttpContext, result: PoppyBrowserJoin): Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* Estado da Session Poppy deste navegador (preenchido por {@link poppyWebSession}).
|
|
26
|
+
*
|
|
27
|
+
* @experimental Poppy (Personal Agent Protocol) Draft 0.3 — acompanha a spec em desenvolvimento e PODE MUDAR
|
|
28
|
+
* de forma incompatível fora de majors enquanto ela for draft.
|
|
17
29
|
*/
|
|
18
|
-
export declare function loginPoppyBrowser(httpCtx: HttpContext, result: PoppyBrowserSessionResult): Promise<void>;
|
|
19
|
-
/** Estado da Session Poppy deste navegador (preenchido por {@link poppyWebSession}). *
|
|
20
|
-
/** Estado da Session Poppy deste navegador (preenchido por {@link poppyWebSession}). * @experimental Poppy (Personal Agent Protocol) Draft 0.1 — acompanha a spec em desenvolvimento e PODE MUDAR
|
|
21
|
-
/** Estado da Session Poppy deste navegador (preenchido por {@link poppyWebSession}). * de forma incompatível fora de majors enquanto ela for draft.
|
|
22
|
-
/** Estado da Session Poppy deste navegador (preenchido por {@link poppyWebSession}). */
|
|
23
30
|
export declare function poppyWebSessionOf(ctx: HttpContext): PoppySessionState | null;
|
|
24
31
|
/**
|
|
25
|
-
* Middleware do site:
|
|
26
|
-
*
|
|
27
|
-
*
|
|
32
|
+
* Middleware do site: o cookie age pela conta e pelos scopes autorizados na entrada e deixa de agir
|
|
33
|
+
* quando o sign-in termina (sign-out, agente desconectado); não sobrevive à Session (§6). Um login
|
|
34
|
+
* antigo não volta. Coloque-o depois do middleware de sessão.
|
|
28
35
|
*
|
|
29
|
-
* @experimental Poppy (Personal Agent Protocol) Draft 0.
|
|
36
|
+
* @experimental Poppy (Personal Agent Protocol) Draft 0.3 — acompanha a spec em desenvolvimento e PODE MUDAR
|
|
30
37
|
* de forma incompatível fora de majors enquanto ela for draft.
|
|
31
38
|
*/
|
|
32
39
|
export declare function poppyWebSession(): (httpCtx: HttpContext, next: () => Promise<void>) => Promise<void>;
|
|
@@ -3,10 +3,11 @@ import { syncAdonisAuthLogin, syncAdonisAuthLogout } from '../../host/adonis_aut
|
|
|
3
3
|
import { POPPY_WEB_SESSION_KEY } from '../../host/poppy_session_key.js';
|
|
4
4
|
import { personalAgentsFor } from '../runtime.js';
|
|
5
5
|
/**
|
|
6
|
-
* Liga o navegador à Session: regenera a sessão do app (session fixation)
|
|
7
|
-
*
|
|
8
|
-
* mecanismo do app — o hook `web.login`, ou a sessão de conta do authkit +
|
|
9
|
-
* `adonisAuth.guard`.
|
|
6
|
+
* Liga o navegador à Session (§6, passo 3): regenera a sessão do app (session fixation) e a marca
|
|
7
|
+
* como do agente. Uma entrada com access token faz o login na conta autorizada, no lugar de qualquer
|
|
8
|
+
* outro, com o mecanismo do app — o hook `web.login`, ou a sessão de conta do authkit +
|
|
9
|
+
* `adonisAuth.guard`. Uma entrada sem access token não mexe no login do navegador — a não ser que
|
|
10
|
+
* uma entrada anterior o tenha logado: aí o sign-in dela termina aqui.
|
|
10
11
|
*/
|
|
11
12
|
export async function loginPoppyBrowser(httpCtx, result) {
|
|
12
13
|
// Sem augmentation no barrel (ver `augmentation_isolation.spec.ts`): a sessão é lida sem tipo.
|
|
@@ -14,12 +15,15 @@ export async function loginPoppyBrowser(httpCtx, result) {
|
|
|
14
15
|
const service = await ctx.containerResolver.make('authkit.server');
|
|
15
16
|
const cfg = service.config;
|
|
16
17
|
const login = cfg.personalAgents?.poppy?.web?.login;
|
|
18
|
+
const previous = ctx.session.get(POPPY_WEB_SESSION_KEY);
|
|
17
19
|
await ctx.session.regenerate();
|
|
18
|
-
ctx.session.forget(ACCOUNT_SESSION_KEY);
|
|
19
20
|
const marker = {
|
|
20
21
|
sessionId: result.sessionId,
|
|
21
22
|
clientId: result.clientId,
|
|
22
23
|
userId: result.userId,
|
|
24
|
+
accountId: result.accountId,
|
|
25
|
+
grantId: result.grantId,
|
|
26
|
+
scopes: result.scopes,
|
|
23
27
|
};
|
|
24
28
|
ctx.session.put(POPPY_WEB_SESSION_KEY, marker);
|
|
25
29
|
if (login) {
|
|
@@ -38,24 +42,27 @@ export async function loginPoppyBrowser(httpCtx, result) {
|
|
|
38
42
|
if (account)
|
|
39
43
|
await syncAdonisAuthLogin(ctx, cfg, account);
|
|
40
44
|
}
|
|
41
|
-
else {
|
|
45
|
+
else if (previous?.accountId) {
|
|
46
|
+
ctx.session.forget(ACCOUNT_SESSION_KEY);
|
|
42
47
|
await syncAdonisAuthLogout(ctx, cfg);
|
|
43
48
|
}
|
|
44
49
|
}
|
|
45
50
|
const states = new WeakMap();
|
|
46
|
-
/**
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
51
|
+
/**
|
|
52
|
+
* Estado da Session Poppy deste navegador (preenchido por {@link poppyWebSession}).
|
|
53
|
+
*
|
|
54
|
+
* @experimental Poppy (Personal Agent Protocol) Draft 0.3 — acompanha a spec em desenvolvimento e PODE MUDAR
|
|
55
|
+
* de forma incompatível fora de majors enquanto ela for draft.
|
|
56
|
+
*/
|
|
50
57
|
export function poppyWebSessionOf(ctx) {
|
|
51
58
|
return states.get(ctx) ?? null;
|
|
52
59
|
}
|
|
53
60
|
/**
|
|
54
|
-
* Middleware do site:
|
|
55
|
-
*
|
|
56
|
-
*
|
|
61
|
+
* Middleware do site: o cookie age pela conta e pelos scopes autorizados na entrada e deixa de agir
|
|
62
|
+
* quando o sign-in termina (sign-out, agente desconectado); não sobrevive à Session (§6). Um login
|
|
63
|
+
* antigo não volta. Coloque-o depois do middleware de sessão.
|
|
57
64
|
*
|
|
58
|
-
* @experimental Poppy (Personal Agent Protocol) Draft 0.
|
|
65
|
+
* @experimental Poppy (Personal Agent Protocol) Draft 0.3 — acompanha a spec em desenvolvimento e PODE MUDAR
|
|
59
66
|
* de forma incompatível fora de majors enquanto ela for draft.
|
|
60
67
|
*/
|
|
61
68
|
export function poppyWebSession() {
|
|
@@ -65,16 +72,18 @@ export function poppyWebSession() {
|
|
|
65
72
|
if (!marker?.sessionId)
|
|
66
73
|
return next();
|
|
67
74
|
const runtime = await personalAgentsFor(ctx);
|
|
68
|
-
const state = runtime?.poppy ? await runtime.poppy.
|
|
75
|
+
const state = runtime?.poppy ? await runtime.poppy.browserState(marker) : null;
|
|
69
76
|
const service = await ctx.containerResolver.make('authkit.server');
|
|
70
77
|
const cfg = service.config;
|
|
71
78
|
const login = cfg.personalAgents?.poppy?.web?.login;
|
|
72
79
|
if (!state?.active || state.clientId !== marker.clientId) {
|
|
73
|
-
// A Session terminou: o cookie também.
|
|
80
|
+
// A Session terminou: o cookie também — e o login dela, não o do próprio navegador.
|
|
74
81
|
ctx.session.forget(POPPY_WEB_SESSION_KEY);
|
|
75
|
-
|
|
82
|
+
if (marker.accountId) {
|
|
83
|
+
ctx.session.forget(ACCOUNT_SESSION_KEY);
|
|
84
|
+
await syncAdonisAuthLogout(ctx, cfg);
|
|
85
|
+
}
|
|
76
86
|
await ctx.session.regenerate();
|
|
77
|
-
await syncAdonisAuthLogout(ctx, cfg);
|
|
78
87
|
return next();
|
|
79
88
|
}
|
|
80
89
|
states.set(ctx, state);
|
|
@@ -89,13 +98,22 @@ export function poppyWebSession() {
|
|
|
89
98
|
return next();
|
|
90
99
|
}
|
|
91
100
|
const current = ctx.session.get(ACCOUNT_SESSION_KEY);
|
|
92
|
-
if (state.signedIn && state.accountId
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
101
|
+
if (state.signedIn && state.accountId) {
|
|
102
|
+
if (current !== state.accountId) {
|
|
103
|
+
ctx.session.put(ACCOUNT_SESSION_KEY, state.accountId);
|
|
104
|
+
const account = await cfg.accountStore.findById(state.accountId);
|
|
105
|
+
if (account)
|
|
106
|
+
await syncAdonisAuthLogin(ctx, cfg, account);
|
|
107
|
+
}
|
|
97
108
|
}
|
|
98
|
-
else if (
|
|
109
|
+
else if (marker.accountId) {
|
|
110
|
+
// O sign-in da entrada terminou: o navegador sai, e a Session segue deslogada.
|
|
111
|
+
ctx.session.put(POPPY_WEB_SESSION_KEY, {
|
|
112
|
+
...marker,
|
|
113
|
+
accountId: null,
|
|
114
|
+
grantId: null,
|
|
115
|
+
scopes: [],
|
|
116
|
+
});
|
|
99
117
|
ctx.session.forget(ACCOUNT_SESSION_KEY);
|
|
100
118
|
await syncAdonisAuthLogout(ctx, cfg);
|
|
101
119
|
}
|
|
@@ -87,7 +87,7 @@ export class AdminSessionsService {
|
|
|
87
87
|
/**
|
|
88
88
|
* Revogação total também corta os personal agents: "sair de todas as sessões",
|
|
89
89
|
* reset de senha, a ação do admin e a exclusão de conta não podem deixar um
|
|
90
|
-
* agente agindo na conta (delegações do PACT e
|
|
90
|
+
* agente agindo na conta (delegações do PACT e refresh tokens do Poppy). Sem
|
|
91
91
|
* `personalAgents` é no-op — e aí nem resolve o banco. Diferente do
|
|
92
92
|
* `recordSubRevocation`, uma falha AQUI propaga: não há outra camada que corte
|
|
93
93
|
* esses tokens.
|
|
@@ -142,9 +142,9 @@ export class AdminSessionsService {
|
|
|
142
142
|
return [];
|
|
143
143
|
return (await adapter.list()).filter((r) => grantIds.has(r.payload.grantId));
|
|
144
144
|
}
|
|
145
|
-
/** Grants da conta, exceto os espelhos `pgrant_` de
|
|
145
|
+
/** Grants da conta, exceto os espelhos `pgrant_` de refresh tokens do Poppy. */
|
|
146
146
|
async #accountGrants(accountId) {
|
|
147
|
-
// Os grants `pgrant_` são o espelho de um
|
|
147
|
+
// Os grants `pgrant_` são o espelho de um refresh token do Poppy (Bearer de MCP) — aparecem e
|
|
148
148
|
// são revogados como personal agent, não como app OIDC.
|
|
149
149
|
return (await this.#listModelOfAccount('Grant', accountId)).filter((r) => !r.id.startsWith('pgrant_'));
|
|
150
150
|
}
|
|
@@ -38,7 +38,7 @@ export default class AccountAppsController {
|
|
|
38
38
|
scopes: g.scopes,
|
|
39
39
|
})))
|
|
40
40
|
: null;
|
|
41
|
-
//
|
|
41
|
+
// Refresh tokens do Poppy: o agente é desconectado pela mesma lista (§5.5).
|
|
42
42
|
const poppy = agentsRuntime?.poppy;
|
|
43
43
|
const poppyAgents = poppy
|
|
44
44
|
? await Promise.all((await poppy.listGrants(accountId)).map(async (g) => ({
|
|
@@ -2,8 +2,8 @@ import '../augmentations.js';
|
|
|
2
2
|
import type { HttpContext } from '@adonisjs/core/http';
|
|
3
3
|
/**
|
|
4
4
|
* Telas do usuário no Poppy: o consentimento do Direct Sign-In (o
|
|
5
|
-
* `authorization_endpoint`, §4.
|
|
6
|
-
* §4.
|
|
5
|
+
* `authorization_endpoint`, §4.2) e a do Device Sign-In (o `verification_uri`,
|
|
6
|
+
* §4.3). Montadas atrás do `accountGuard`: sem sessão, o usuário passa pelo login
|
|
7
7
|
* DESTE app e volta. Abrir o link nunca aprova sozinho — a aprovação é um POST
|
|
8
8
|
* com CSRF. Recusadas durante impersonation.
|
|
9
9
|
*/
|
|
@@ -14,8 +14,8 @@ function chosenScopes(ctx) {
|
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
16
|
* Telas do usuário no Poppy: o consentimento do Direct Sign-In (o
|
|
17
|
-
* `authorization_endpoint`, §4.
|
|
18
|
-
* §4.
|
|
17
|
+
* `authorization_endpoint`, §4.2) e a do Device Sign-In (o `verification_uri`,
|
|
18
|
+
* §4.3). Montadas atrás do `accountGuard`: sem sessão, o usuário passa pelo login
|
|
19
19
|
* DESTE app e volta. Abrir o link nunca aprova sozinho — a aprovação é um POST
|
|
20
20
|
* com CSRF. Recusadas durante impersonation.
|
|
21
21
|
*/
|
|
@@ -1,25 +1,33 @@
|
|
|
1
1
|
import type { HttpContext } from '@adonisjs/core/http';
|
|
2
2
|
/**
|
|
3
|
-
* Endpoints
|
|
4
|
-
* token, revogação, device authorization
|
|
5
|
-
*
|
|
3
|
+
* Endpoints do Poppy (§3, §4, §6): descoberta, protected resource metadata, metadata RFC 8414,
|
|
4
|
+
* token, revogação, device authorization e a entrada do navegador na Session. Os POSTs são isentos
|
|
5
|
+
* de CSRF — ver `authkitCsrfExceptions`.
|
|
6
6
|
*/
|
|
7
7
|
export default class PoppyController {
|
|
8
|
-
/** `GET /.well-known/poppy.json` — só no domínio da empresa
|
|
8
|
+
/** `GET /.well-known/poppy.json` — só no domínio da empresa; `www.` redireciona para ele. */
|
|
9
9
|
discovery(ctx: HttpContext): Promise<void>;
|
|
10
|
-
/** `GET /.well-known/oauth-
|
|
10
|
+
/** `GET /.well-known/oauth-protected-resource` do domínio (§3.2, RFC 9728). */
|
|
11
|
+
protectedResource(ctx: HttpContext): Promise<void>;
|
|
12
|
+
/** `GET /.well-known/oauth-authorization-server{prefix}` (RFC 8414 + `protected_resources`). */
|
|
11
13
|
metadata(ctx: HttpContext): Promise<void>;
|
|
12
14
|
token(ctx: HttpContext): Promise<void>;
|
|
13
15
|
deviceAuthorization(ctx: HttpContext): Promise<void>;
|
|
14
16
|
/** RFC 7009: 200 também para token desconhecido. */
|
|
15
17
|
revoke(ctx: HttpContext): Promise<void>;
|
|
16
|
-
/** `POST auth.mediated.endpoint` (§4.7) — com o Session Token (DPoP). */
|
|
17
|
-
mediatedStart(ctx: HttpContext): Promise<void>;
|
|
18
|
-
/** `POST {mediated}/{sign_in_id}` (§4.7). */
|
|
19
|
-
mediatedCode(ctx: HttpContext): Promise<void>;
|
|
20
18
|
/**
|
|
21
|
-
* `
|
|
22
|
-
*
|
|
19
|
+
* `GET web.browser_session_endpoint` (§6, passo 1): a página com o código de uso único e o cookie
|
|
20
|
+
* que o prende a este navegador. Não mexe em Session nem nos outros cookies.
|
|
21
|
+
*/
|
|
22
|
+
browserSessionPage(ctx: HttpContext): Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* `POST {browser_session_endpoint}/authorize` (§6, passo 2): o agente autoriza o código, com o
|
|
25
|
+
* header da Session e, se logado, o access token. 204; código inválido → 400 `invalid_request`.
|
|
26
|
+
*/
|
|
27
|
+
browserSessionAuthorize(ctx: HttpContext): Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* `POST web.browser_session_endpoint` (§6, passo 3): o navegador submete o código → cookie do app
|
|
30
|
+
* para a Session, limpa o cookie do passo 1 e redireciona para o site; falha → página de erro.
|
|
23
31
|
*/
|
|
24
32
|
browserSession(ctx: HttpContext): Promise<void>;
|
|
25
33
|
}
|