@adonis-agora/authkit-server 0.73.0 → 0.75.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 +4 -0
- package/build/commands/clients_create.d.ts +2 -0
- package/build/commands/clients_create.js +41 -13
- package/build/commands/commands.json +7 -0
- package/build/index.d.ts +6 -0
- package/build/index.js +7 -0
- package/build/src/commands/import_clients.js +2 -0
- package/build/src/define_config.d.ts +8 -0
- package/build/src/define_config.js +1 -0
- package/build/src/host/access_token_verifier.d.ts +107 -0
- package/build/src/host/access_token_verifier.js +248 -0
- package/build/src/host/admin_api/api_clients_controller.d.ts +4 -1
- package/build/src/host/admin_api/api_clients_controller.js +25 -2
- package/build/src/host/admin_api/dto.d.ts +1 -0
- package/build/src/host/admin_api/dto.js +1 -0
- package/build/src/host/admin_clients_service.d.ts +9 -0
- package/build/src/host/admin_clients_service.js +21 -0
- package/build/src/host/admin_console/console_clients_controller.d.ts +3 -1
- package/build/src/host/admin_console/console_clients_controller.js +25 -2
- package/build/src/host/admin_validators.d.ts +11 -1
- package/build/src/host/admin_validators.js +36 -8
- package/build/src/host/bearer_account.d.ts +26 -0
- package/build/src/host/bearer_account.js +38 -0
- package/build/src/host/client_metadata.d.ts +51 -0
- package/build/src/host/client_metadata.js +98 -0
- package/build/src/host/console_session.d.ts +14 -3
- package/build/src/host/console_session.js +20 -10
- package/build/src/host/impersonation_session.d.ts +5 -0
- package/build/src/host/impersonation_session.js +37 -4
- package/build/src/host/oidc_bearer_guard.d.ts +113 -0
- package/build/src/host/oidc_bearer_guard.js +232 -0
- package/build/src/host/oidc_rp_guard.d.ts +3 -1
- package/build/src/host/oidc_rp_guard.js +6 -2
- package/build/src/host/ui-dist/assets/{index-Dct63ai-.js → index-CIqtoNcO.js} +1 -1
- package/build/src/host/ui-dist/index.html +1 -1
- package/build/src/provider/build_provider.js +47 -17
- package/build/src/provider/oidc_service.d.ts +8 -0
- package/build/src/provider/oidc_service.js +12 -1
- package/build/src/provider/token_exchange.d.ts +26 -0
- package/build/src/provider/token_exchange.js +44 -3
- package/package.json +3 -3
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getAccountLoginUrl } from './account_login_url.js';
|
|
2
2
|
import { ACCOUNT_SESSION_KEY } from './account_session_key.js';
|
|
3
|
+
import { bearerAccountId } from './bearer_account.js';
|
|
3
4
|
import { impersonationState } from './impersonation_session.js';
|
|
4
5
|
/**
|
|
5
6
|
* Helpers públicos para integrar a sessão do console do AuthKit com
|
|
@@ -11,8 +12,18 @@ import { impersonationState } from './impersonation_session.js';
|
|
|
11
12
|
* sessão mudar.
|
|
12
13
|
*/
|
|
13
14
|
/**
|
|
14
|
-
* Retorna o id da conta que o request está representando
|
|
15
|
-
*
|
|
15
|
+
* Retorna o id da conta que o request está representando (ou `null` quando não
|
|
16
|
+
* há identidade).
|
|
17
|
+
*
|
|
18
|
+
* Fonte, nesta ordem:
|
|
19
|
+
* 1. a sessão do console (`ACCOUNT_SESSION_KEY`) — o comportamento de sempre;
|
|
20
|
+
* 2. SEM sessão logada: a conta que um `oidcBearerGuard` autenticou NESTA
|
|
21
|
+
* request via `Authorization: Bearer` (app nativo, SPA, serviço).
|
|
22
|
+
*
|
|
23
|
+
* O fallback (2) só existe depois que o guard bearer RODOU na request (ex.:
|
|
24
|
+
* `middleware.auth({ guards: ['web', 'api'] })` ou `auth.authenticateUsing`);
|
|
25
|
+
* um app que não usa o guard bearer continua exatamente como antes. A sessão
|
|
26
|
+
* sempre ganha: request com cookie de sessão E bearer responde pela sessão.
|
|
16
27
|
*
|
|
17
28
|
* ATENÇÃO: com impersonation ativa isto é a conta PERSONIFICADA, não o admin
|
|
18
29
|
* que a personificou. É o comportamento certo para "como qual conta este
|
|
@@ -21,7 +32,7 @@ import { impersonationState } from './impersonation_session.js';
|
|
|
21
32
|
*/
|
|
22
33
|
export function getAccountId(ctx) {
|
|
23
34
|
const accountId = ctx.session?.get(ACCOUNT_SESSION_KEY);
|
|
24
|
-
return accountId ??
|
|
35
|
+
return accountId ?? bearerAccountId(ctx);
|
|
25
36
|
}
|
|
26
37
|
/**
|
|
27
38
|
* Retorna o id do HUMANO real por trás do request — o id que uma decisão de
|
|
@@ -54,18 +65,17 @@ export function getAccountId(ctx) {
|
|
|
54
65
|
* if (!id || !(await authz.hasRole(id, 'admin'))) throw new Error('forbidden')
|
|
55
66
|
*/
|
|
56
67
|
export function realAccountId(ctx) {
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
if (!ctx.session)
|
|
61
|
-
return null;
|
|
68
|
+
// `impersonationState` cobre as duas fontes: a sessão do console e o access
|
|
69
|
+
// token bearer trocado (`act`, ex.: app nativo) — nos dois, o humano real é o
|
|
70
|
+
// impersonator. Sem impersonation, a conta da sessão ou do bearer.
|
|
62
71
|
return impersonationState(ctx).impersonatorId ?? getAccountId(ctx);
|
|
63
72
|
}
|
|
64
73
|
/**
|
|
65
|
-
* `true` quando o request carrega uma sessão de conta do console.
|
|
74
|
+
* `true` quando o request carrega uma sessão de conta do console. Só a SESSÃO:
|
|
75
|
+
* uma identidade bearer (`oidcBearerGuard`) não conta — use `getAccountId`.
|
|
66
76
|
*/
|
|
67
77
|
export function hasAccountSession(ctx) {
|
|
68
|
-
return
|
|
78
|
+
return ctx.session?.get(ACCOUNT_SESSION_KEY) != null;
|
|
69
79
|
}
|
|
70
80
|
/**
|
|
71
81
|
* URL do login do console, com `return_to` opcional de volta ao destino
|
|
@@ -61,6 +61,11 @@ export interface ImpersonationState {
|
|
|
61
61
|
* do lado do IdP, NÃO impõe expiração na sessão (só `maxAge` faz isso).
|
|
62
62
|
*/
|
|
63
63
|
exchangeExpiresIn?: number;
|
|
64
|
+
/**
|
|
65
|
+
* De onde veio a impersonation: `session` (console/web, keys de sessão) ou
|
|
66
|
+
* `bearer` (access token trocado com `act`, ex.: app nativo). Só quando `active`.
|
|
67
|
+
*/
|
|
68
|
+
source?: 'session' | 'bearer';
|
|
64
69
|
}
|
|
65
70
|
/** Metadados aproveitados da resposta do token-exchange (só o que não é segredo). */
|
|
66
71
|
export interface TokenExchangeResult {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { ACCOUNT_SESSION_KEY } from './account_session_key.js';
|
|
3
|
+
import { bearerAccountId, bearerImpersonation } from './bearer_account.js';
|
|
3
4
|
/**
|
|
4
5
|
* Ergonômico de SESSÃO de browser no RP para "personificar" (impersonate) um
|
|
5
6
|
* usuário e navegar como ele — roteado pelo token-exchange RFC 8693 que o IdP já
|
|
@@ -304,11 +305,16 @@ export async function startImpersonation(ctx, params) {
|
|
|
304
305
|
* false }` puro quando nunca houve impersonation.
|
|
305
306
|
*/
|
|
306
307
|
export function impersonationState(ctx) {
|
|
307
|
-
const impersonatorId = ctx.session
|
|
308
|
-
if (!impersonatorId)
|
|
309
|
-
|
|
308
|
+
const impersonatorId = ctx.session?.get(IMPERSONATOR_SESSION_KEY);
|
|
309
|
+
if (!impersonatorId) {
|
|
310
|
+
// Sessão de console logada manda (mesma regra do `getAccountId`): o bearer só
|
|
311
|
+
// conta quando a request não tem conta na sessão.
|
|
312
|
+
if (ctx.session?.get(ACCOUNT_SESSION_KEY))
|
|
313
|
+
return { active: false };
|
|
314
|
+
return bearerImpersonationState(ctx);
|
|
315
|
+
}
|
|
310
316
|
const targetId = ctx.session.get(ACCOUNT_SESSION_KEY);
|
|
311
|
-
const state = { active: true, targetId, impersonatorId };
|
|
317
|
+
const state = { active: true, targetId, impersonatorId, source: 'session' };
|
|
312
318
|
const impersonationId = ctx.session.get(IMPERSONATION_ID_SESSION_KEY);
|
|
313
319
|
if (impersonationId)
|
|
314
320
|
state.impersonationId = impersonationId;
|
|
@@ -329,6 +335,33 @@ export function impersonationState(ctx) {
|
|
|
329
335
|
}
|
|
330
336
|
return state;
|
|
331
337
|
}
|
|
338
|
+
/**
|
|
339
|
+
* Impersonation pelo ACCESS TOKEN (sem sessão): o `oidcBearerGuard` autenticou
|
|
340
|
+
* um token trocado que carrega `act`. O alvo é o `sub` do token; o impersonator,
|
|
341
|
+
* o `act.sub`; a expiração, o `exp` do token (o token trocado não tem refresh).
|
|
342
|
+
* Só existe depois que o guard bearer rodou na request, como o `getAccountId`.
|
|
343
|
+
*/
|
|
344
|
+
function bearerImpersonationState(ctx) {
|
|
345
|
+
const bearer = bearerImpersonation(ctx);
|
|
346
|
+
const targetId = bearerAccountId(ctx);
|
|
347
|
+
if (!bearer || !targetId)
|
|
348
|
+
return { active: false };
|
|
349
|
+
const state = {
|
|
350
|
+
active: true,
|
|
351
|
+
targetId,
|
|
352
|
+
impersonatorId: bearer.actorId,
|
|
353
|
+
actSub: bearer.actorId,
|
|
354
|
+
source: 'bearer',
|
|
355
|
+
};
|
|
356
|
+
if (bearer.jti)
|
|
357
|
+
state.impersonationId = bearer.jti;
|
|
358
|
+
if (bearer.exp !== null) {
|
|
359
|
+
state.expiresAt = bearer.exp * 1000;
|
|
360
|
+
if (Date.now() > state.expiresAt)
|
|
361
|
+
state.active = false;
|
|
362
|
+
}
|
|
363
|
+
return state;
|
|
364
|
+
}
|
|
332
365
|
/**
|
|
333
366
|
* Encerra a impersonation: restaura `account_user_id = impersonator`, remove
|
|
334
367
|
* TODAS as keys de sessão de impersonation (id, tempos, prova do exchange) e
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { symbols } from '@adonisjs/auth';
|
|
2
|
+
import type { AuthClientResponse, GuardConfigProvider, GuardContract } from '@adonisjs/auth/types';
|
|
3
|
+
import type { SessionUserProviderContract } from '@adonisjs/auth/types/session';
|
|
4
|
+
import type { HttpContext } from '@adonisjs/core/http';
|
|
5
|
+
import type { ConfigProvider } from '@adonisjs/core/types';
|
|
6
|
+
import type { EmitterLike } from '@adonisjs/core/types/events';
|
|
7
|
+
import { type AccessTokenVerifier, type IssueAccessTokenOptions, type RemoteAccessTokenVerifierOptions, type VerifiedAccessToken } from './access_token_verifier.js';
|
|
8
|
+
import { type UnauthorizedAccessConstructor } from './oidc_rp_guard.js';
|
|
9
|
+
type RealUser<UserProvider> = UserProvider extends SessionUserProviderContract<infer U> ? U : never;
|
|
10
|
+
/** Política de aceitação do token, além de "é válido e não expirou". */
|
|
11
|
+
export interface OidcBearerGuardPolicy {
|
|
12
|
+
/** Escopos exigidos (TODOS). Token sem algum deles → 401 `insufficient_scope`. */
|
|
13
|
+
scopes?: string[];
|
|
14
|
+
/**
|
|
15
|
+
* Audience(s) aceitas (RFC 8707 resource indicators). Quando definido, o `aud`
|
|
16
|
+
* do token precisa conter pelo menos uma. Token opaco emitido SEM `resource`
|
|
17
|
+
* não tem `aud` e é recusado.
|
|
18
|
+
*/
|
|
19
|
+
audience?: string | string[];
|
|
20
|
+
/** Allowlist de `client_id` cujos tokens são aceitos. Ausente = qualquer client. */
|
|
21
|
+
clientIds?: string[];
|
|
22
|
+
}
|
|
23
|
+
export type OidcBearerGuardOptions<UserProvider extends SessionUserProviderContract<unknown>> = OidcBearerGuardPolicy & {
|
|
24
|
+
/**
|
|
25
|
+
* User provider — o MESMO tipo do `oidcRpGuard` (`sessionUserProvider(...)`
|
|
26
|
+
* ou `authkitUserProvider()`): o `sub` do token vai para `findById`.
|
|
27
|
+
*/
|
|
28
|
+
provider: UserProvider | ConfigProvider<UserProvider>;
|
|
29
|
+
/**
|
|
30
|
+
* Modo resource server REMOTO (a API não hospeda o issuer): JWT access tokens
|
|
31
|
+
* via JWKS do issuer (RFC 9068) e/ou tokens opacos via introspecção (RFC 7662).
|
|
32
|
+
* Ausente = modo IN-PROCESS: verifica contra o issuer embarcado
|
|
33
|
+
* (`authkit.server`), opaco e JWT.
|
|
34
|
+
*/
|
|
35
|
+
remote?: RemoteAccessTokenVerifierOptions;
|
|
36
|
+
/** Verificador próprio (avançado). Tem precedência sobre `remote`. */
|
|
37
|
+
verifier?: AccessTokenVerifier;
|
|
38
|
+
};
|
|
39
|
+
export type OidcBearerGuardEvents<User> = {
|
|
40
|
+
'oidc_bearer:authentication_succeeded': {
|
|
41
|
+
ctx: HttpContext;
|
|
42
|
+
guardName: string;
|
|
43
|
+
user: User;
|
|
44
|
+
token: VerifiedAccessToken;
|
|
45
|
+
};
|
|
46
|
+
'oidc_bearer:authentication_failed': {
|
|
47
|
+
ctx: HttpContext;
|
|
48
|
+
guardName: string;
|
|
49
|
+
error: Error;
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Guard de `@adonisjs/auth` para APIs chamadas com access token do issuer do
|
|
54
|
+
* authkit — o par do {@link OidcRpGuard} para clientes SEM sessão web: apps
|
|
55
|
+
* nativos (React Native/Expo, RFC 8252), SPAs que falam direto com a API,
|
|
56
|
+
* serviços. Lê `Authorization: Bearer <token>`, verifica o token (assinatura ou
|
|
57
|
+
* adapter, expiração, revogação), aplica a política (escopos, audience, clients)
|
|
58
|
+
* e resolve o user pelo `sub` no user provider.
|
|
59
|
+
*
|
|
60
|
+
* ```ts
|
|
61
|
+
* // config/auth.ts
|
|
62
|
+
* import { oidcBearerGuard, oidcRpGuard } from '@adonis-agora/authkit-server'
|
|
63
|
+
* import { sessionUserProvider } from '@adonisjs/auth/session'
|
|
64
|
+
*
|
|
65
|
+
* const provider = sessionUserProvider({ model: () => import('#models/user') })
|
|
66
|
+
*
|
|
67
|
+
* const authConfig = defineConfig({
|
|
68
|
+
* default: 'web',
|
|
69
|
+
* guards: {
|
|
70
|
+
* web: oidcRpGuard({ provider }),
|
|
71
|
+
* api: oidcBearerGuard({ provider, scopes: ['openid'] }),
|
|
72
|
+
* },
|
|
73
|
+
* })
|
|
74
|
+
*
|
|
75
|
+
* // start/routes.ts — aceita sessão OU bearer:
|
|
76
|
+
* router.get('/me', ...).use(middleware.auth({ guards: ['web', 'api'] }))
|
|
77
|
+
* ```
|
|
78
|
+
*
|
|
79
|
+
* Falha SEMPRE vira `E_UNAUTHORIZED_ACCESS` (401, sem redirect), com o header
|
|
80
|
+
* `WWW-Authenticate: Bearer …` da RFC 6750 §3.
|
|
81
|
+
*/
|
|
82
|
+
export declare class OidcBearerGuard<UserProvider extends SessionUserProviderContract<unknown>> implements GuardContract<RealUser<UserProvider>> {
|
|
83
|
+
#private;
|
|
84
|
+
[symbols.GUARD_KNOWN_EVENTS]: OidcBearerGuardEvents<RealUser<UserProvider>>;
|
|
85
|
+
driverName: 'oidc_bearer';
|
|
86
|
+
authenticationAttempted: boolean;
|
|
87
|
+
isAuthenticated: boolean;
|
|
88
|
+
user?: RealUser<UserProvider>;
|
|
89
|
+
/** O access token verificado desta request (após `authenticate()` com sucesso). */
|
|
90
|
+
accessToken?: VerifiedAccessToken;
|
|
91
|
+
constructor(name: string, ctx: HttpContext, emitter: EmitterLike<OidcBearerGuardEvents<RealUser<UserProvider>>>, userProvider: UserProvider, verifier: AccessTokenVerifier, policy?: OidcBearerGuardPolicy, unauthorized?: UnauthorizedAccessConstructor);
|
|
92
|
+
getUserOrFail(): RealUser<UserProvider>;
|
|
93
|
+
/** `true` quando o token autenticado tem TODOS os escopos pedidos. */
|
|
94
|
+
hasScopes(...scopes: string[]): boolean;
|
|
95
|
+
authenticate(): Promise<RealUser<UserProvider>>;
|
|
96
|
+
/**
|
|
97
|
+
* `authenticate()` sem lançar — só para falha de AUTENTICAÇÃO (mesma regra do
|
|
98
|
+
* `OidcRpGuard#check`): erro de infraestrutura (banco, rede) é relançado.
|
|
99
|
+
*/
|
|
100
|
+
check(): Promise<boolean>;
|
|
101
|
+
/**
|
|
102
|
+
* Para testes (`client.get('/api').loginAs(user)`): emite um access token REAL
|
|
103
|
+
* no issuer embarcado e devolve o header `Authorization`. Só no modo in-process
|
|
104
|
+
* — um resource server remoto não emite tokens.
|
|
105
|
+
*/
|
|
106
|
+
authenticateAsClient(user: RealUser<UserProvider>, options?: IssueAccessTokenOptions): Promise<AuthClientResponse>;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Factory de config pro {@link OidcBearerGuard} — mesmo padrão do `oidcRpGuard()`
|
|
110
|
+
* e do `sessionGuard()` nativo.
|
|
111
|
+
*/
|
|
112
|
+
export declare function oidcBearerGuard<UserProvider extends SessionUserProviderContract<unknown>>(config: OidcBearerGuardOptions<UserProvider>): GuardConfigProvider<(ctx: HttpContext) => OidcBearerGuard<UserProvider>>;
|
|
113
|
+
export {};
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { RuntimeException } from '@adonisjs/core/exceptions';
|
|
2
|
+
import { inProcessAccessTokenVerifier, remoteAccessTokenVerifier, } from './access_token_verifier.js';
|
|
3
|
+
import { clearBearerAccountId, setBearerAccountId, setBearerImpersonation, } from './bearer_account.js';
|
|
4
|
+
import { cachedUnauthorizedAccessConstructor, loadUnauthorizedAccess, } from './oidc_rp_guard.js';
|
|
5
|
+
/**
|
|
6
|
+
* Driver do `E_UNAUTHORIZED_ACCESS` lançado pelo guard. `access_tokens` é o
|
|
7
|
+
* renderer do `@adonisjs/auth` para o guard NATIVO de access tokens: 401 com
|
|
8
|
+
* `{ errors: [{ message }] }` em JSON (ou texto/JSON:API) e NUNCA redirect — o que
|
|
9
|
+
* uma API consumida por app mobile espera.
|
|
10
|
+
*/
|
|
11
|
+
const ERROR_RENDERER = 'access_tokens';
|
|
12
|
+
/** `Authorization: Bearer <token>` (RFC 6750 §2.1). Esquema case-insensitive. */
|
|
13
|
+
const BEARER = /^Bearer\s+([A-Za-z0-9\-._~+/]+=*)\s*$/i;
|
|
14
|
+
/**
|
|
15
|
+
* Guard de `@adonisjs/auth` para APIs chamadas com access token do issuer do
|
|
16
|
+
* authkit — o par do {@link OidcRpGuard} para clientes SEM sessão web: apps
|
|
17
|
+
* nativos (React Native/Expo, RFC 8252), SPAs que falam direto com a API,
|
|
18
|
+
* serviços. Lê `Authorization: Bearer <token>`, verifica o token (assinatura ou
|
|
19
|
+
* adapter, expiração, revogação), aplica a política (escopos, audience, clients)
|
|
20
|
+
* e resolve o user pelo `sub` no user provider.
|
|
21
|
+
*
|
|
22
|
+
* ```ts
|
|
23
|
+
* // config/auth.ts
|
|
24
|
+
* import { oidcBearerGuard, oidcRpGuard } from '@adonis-agora/authkit-server'
|
|
25
|
+
* import { sessionUserProvider } from '@adonisjs/auth/session'
|
|
26
|
+
*
|
|
27
|
+
* const provider = sessionUserProvider({ model: () => import('#models/user') })
|
|
28
|
+
*
|
|
29
|
+
* const authConfig = defineConfig({
|
|
30
|
+
* default: 'web',
|
|
31
|
+
* guards: {
|
|
32
|
+
* web: oidcRpGuard({ provider }),
|
|
33
|
+
* api: oidcBearerGuard({ provider, scopes: ['openid'] }),
|
|
34
|
+
* },
|
|
35
|
+
* })
|
|
36
|
+
*
|
|
37
|
+
* // start/routes.ts — aceita sessão OU bearer:
|
|
38
|
+
* router.get('/me', ...).use(middleware.auth({ guards: ['web', 'api'] }))
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* Falha SEMPRE vira `E_UNAUTHORIZED_ACCESS` (401, sem redirect), com o header
|
|
42
|
+
* `WWW-Authenticate: Bearer …` da RFC 6750 §3.
|
|
43
|
+
*/
|
|
44
|
+
export class OidcBearerGuard {
|
|
45
|
+
#name;
|
|
46
|
+
#ctx;
|
|
47
|
+
#emitter;
|
|
48
|
+
#userProvider;
|
|
49
|
+
#verifier;
|
|
50
|
+
#policy;
|
|
51
|
+
#unauthorized;
|
|
52
|
+
driverName = 'oidc_bearer';
|
|
53
|
+
authenticationAttempted = false;
|
|
54
|
+
isAuthenticated = false;
|
|
55
|
+
user;
|
|
56
|
+
/** O access token verificado desta request (após `authenticate()` com sucesso). */
|
|
57
|
+
accessToken;
|
|
58
|
+
constructor(name, ctx, emitter, userProvider, verifier, policy = {}, unauthorized) {
|
|
59
|
+
this.#name = name;
|
|
60
|
+
this.#ctx = ctx;
|
|
61
|
+
this.#emitter = emitter;
|
|
62
|
+
this.#userProvider = userProvider;
|
|
63
|
+
this.#verifier = verifier;
|
|
64
|
+
this.#policy = policy;
|
|
65
|
+
this.#unauthorized = unauthorized ?? cachedUnauthorizedAccessConstructor();
|
|
66
|
+
}
|
|
67
|
+
#unauthorizedError(message) {
|
|
68
|
+
const Unauthorized = this.#unauthorized ?? cachedUnauthorizedAccessConstructor();
|
|
69
|
+
if (!Unauthorized)
|
|
70
|
+
return new RuntimeException(message);
|
|
71
|
+
return new Unauthorized(message, { guardDriverName: ERROR_RENDERER });
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Falha de autenticação: header `WWW-Authenticate` (RFC 6750 §3), evento, e o
|
|
75
|
+
* `E_UNAUTHORIZED_ACCESS`. Sem token → só o desafio `Bearer`; token recusado →
|
|
76
|
+
* `error="invalid_token"` / `"insufficient_scope"`.
|
|
77
|
+
*/
|
|
78
|
+
#fail(bearerError) {
|
|
79
|
+
let challenge = 'Bearer';
|
|
80
|
+
if (bearerError)
|
|
81
|
+
challenge += ` error="${bearerError}"`;
|
|
82
|
+
if (bearerError === 'insufficient_scope' && this.#policy.scopes?.length) {
|
|
83
|
+
challenge += `, scope="${this.#policy.scopes.join(' ')}"`;
|
|
84
|
+
}
|
|
85
|
+
this.#ctx.response?.header?.('WWW-Authenticate', challenge);
|
|
86
|
+
clearBearerAccountId(this.#ctx);
|
|
87
|
+
const error = this.#unauthorizedError('Unauthorized access');
|
|
88
|
+
this.#emitter.emit('oidc_bearer:authentication_failed', {
|
|
89
|
+
ctx: this.#ctx,
|
|
90
|
+
guardName: this.#name,
|
|
91
|
+
error,
|
|
92
|
+
});
|
|
93
|
+
return error;
|
|
94
|
+
}
|
|
95
|
+
getUserOrFail() {
|
|
96
|
+
if (!this.user) {
|
|
97
|
+
throw this.#unauthorizedError('Cannot access user. Authentication has not been attempted or failed.');
|
|
98
|
+
}
|
|
99
|
+
return this.user;
|
|
100
|
+
}
|
|
101
|
+
/** `true` quando o token autenticado tem TODOS os escopos pedidos. */
|
|
102
|
+
hasScopes(...scopes) {
|
|
103
|
+
const granted = new Set(this.accessToken?.scopes ?? []);
|
|
104
|
+
return scopes.every((s) => granted.has(s));
|
|
105
|
+
}
|
|
106
|
+
#bearerToken() {
|
|
107
|
+
const header = this.#ctx.request.header('authorization');
|
|
108
|
+
if (!header)
|
|
109
|
+
return undefined;
|
|
110
|
+
return BEARER.exec(header)?.[1];
|
|
111
|
+
}
|
|
112
|
+
#policyError(token) {
|
|
113
|
+
const { clientIds, audience, scopes } = this.#policy;
|
|
114
|
+
if (clientIds && (!token.clientId || !clientIds.includes(token.clientId))) {
|
|
115
|
+
return 'invalid_token';
|
|
116
|
+
}
|
|
117
|
+
if (audience !== undefined) {
|
|
118
|
+
const accepted = Array.isArray(audience) ? audience : [audience];
|
|
119
|
+
if (!token.audience.some((a) => accepted.includes(a)))
|
|
120
|
+
return 'invalid_token';
|
|
121
|
+
}
|
|
122
|
+
if (scopes?.length) {
|
|
123
|
+
const granted = new Set(token.scopes);
|
|
124
|
+
if (!scopes.every((s) => granted.has(s)))
|
|
125
|
+
return 'insufficient_scope';
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
async authenticate() {
|
|
130
|
+
if (this.authenticationAttempted) {
|
|
131
|
+
return this.getUserOrFail();
|
|
132
|
+
}
|
|
133
|
+
this.authenticationAttempted = true;
|
|
134
|
+
this.#unauthorized ??= await loadUnauthorizedAccess('oidcBearerGuard');
|
|
135
|
+
const raw = this.#bearerToken();
|
|
136
|
+
if (!raw)
|
|
137
|
+
throw this.#fail();
|
|
138
|
+
const token = await this.#verifier.verify(raw);
|
|
139
|
+
if (!token)
|
|
140
|
+
throw this.#fail('invalid_token');
|
|
141
|
+
const policyError = this.#policyError(token);
|
|
142
|
+
if (policyError)
|
|
143
|
+
throw this.#fail(policyError);
|
|
144
|
+
const guardUser = await this.#userProvider.findById(token.sub);
|
|
145
|
+
if (!guardUser)
|
|
146
|
+
throw this.#fail('invalid_token');
|
|
147
|
+
// Token de impersonation: o ator (o admin) também tem que existir AGORA. Conta
|
|
148
|
+
// apagada/desativada depois da troca derruba o acesso na próxima request, sem
|
|
149
|
+
// esperar o `exp` do token.
|
|
150
|
+
if (token.actor && !(await this.#userProvider.findById(token.actor))) {
|
|
151
|
+
throw this.#fail('invalid_token');
|
|
152
|
+
}
|
|
153
|
+
this.user = guardUser.getOriginal();
|
|
154
|
+
this.accessToken = token;
|
|
155
|
+
this.isAuthenticated = true;
|
|
156
|
+
setBearerAccountId(this.#ctx, String(guardUser.getId()));
|
|
157
|
+
if (token.actor) {
|
|
158
|
+
setBearerImpersonation(this.#ctx, { actorId: token.actor, exp: token.exp, jti: token.jti });
|
|
159
|
+
}
|
|
160
|
+
this.#emitter.emit('oidc_bearer:authentication_succeeded', {
|
|
161
|
+
ctx: this.#ctx,
|
|
162
|
+
guardName: this.#name,
|
|
163
|
+
user: this.user,
|
|
164
|
+
token,
|
|
165
|
+
});
|
|
166
|
+
return this.user;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* `authenticate()` sem lançar — só para falha de AUTENTICAÇÃO (mesma regra do
|
|
170
|
+
* `OidcRpGuard#check`): erro de infraestrutura (banco, rede) é relançado.
|
|
171
|
+
*/
|
|
172
|
+
async check() {
|
|
173
|
+
try {
|
|
174
|
+
await this.authenticate();
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
const Unauthorized = this.#unauthorized ?? cachedUnauthorizedAccessConstructor();
|
|
179
|
+
if (Unauthorized && error instanceof Unauthorized)
|
|
180
|
+
return false;
|
|
181
|
+
throw error;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Para testes (`client.get('/api').loginAs(user)`): emite um access token REAL
|
|
186
|
+
* no issuer embarcado e devolve o header `Authorization`. Só no modo in-process
|
|
187
|
+
* — um resource server remoto não emite tokens.
|
|
188
|
+
*/
|
|
189
|
+
async authenticateAsClient(user, options) {
|
|
190
|
+
if (!this.#verifier.issue) {
|
|
191
|
+
throw new RuntimeException('oidcBearerGuard: authenticateAsClient só funciona no modo in-process (o app hospeda o issuer).');
|
|
192
|
+
}
|
|
193
|
+
const guardUser = await this.#userProvider.createUserForGuard(user);
|
|
194
|
+
const token = await this.#verifier.issue(String(guardUser.getId()), {
|
|
195
|
+
...options,
|
|
196
|
+
scopes: options?.scopes ?? this.#policy.scopes,
|
|
197
|
+
});
|
|
198
|
+
return { headers: { authorization: `Bearer ${token}` } };
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Factory de config pro {@link OidcBearerGuard} — mesmo padrão do `oidcRpGuard()`
|
|
203
|
+
* e do `sessionGuard()` nativo.
|
|
204
|
+
*/
|
|
205
|
+
export function oidcBearerGuard(config) {
|
|
206
|
+
return {
|
|
207
|
+
async resolver(name, app) {
|
|
208
|
+
const emitter = await app.container.make('emitter');
|
|
209
|
+
const unauthorized = await loadUnauthorizedAccess('oidcBearerGuard');
|
|
210
|
+
let userProvider;
|
|
211
|
+
if (typeof config.provider.resolver === 'function') {
|
|
212
|
+
userProvider = await config.provider.resolver(app);
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
userProvider = config.provider;
|
|
216
|
+
}
|
|
217
|
+
// In-process: o `authkit.server` é resolvido na PRIMEIRA verificação (não
|
|
218
|
+
// aqui) — a config do auth pode ser resolvida antes do provider do authkit
|
|
219
|
+
// terminar o boot.
|
|
220
|
+
const verifier = config.verifier ??
|
|
221
|
+
(config.remote
|
|
222
|
+
? remoteAccessTokenVerifier(config.remote)
|
|
223
|
+
: inProcessAccessTokenVerifier(async () => (await app.container.make('authkit.server'))));
|
|
224
|
+
const policy = {
|
|
225
|
+
scopes: config.scopes,
|
|
226
|
+
audience: config.audience,
|
|
227
|
+
clientIds: config.clientIds,
|
|
228
|
+
};
|
|
229
|
+
return (ctx) => new OidcBearerGuard(name, ctx, emitter, userProvider, verifier, policy, unauthorized);
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
}
|
|
@@ -15,13 +15,15 @@ export type UnauthorizedAccessConstructor = new (message: string, options: {
|
|
|
15
15
|
guardDriverName: string;
|
|
16
16
|
redirectTo?: string;
|
|
17
17
|
}) => Error;
|
|
18
|
+
/** O construtor já resolvido por {@link loadUnauthorizedAccess}, se houver. */
|
|
19
|
+
export declare function cachedUnauthorizedAccessConstructor(): UnauthorizedAccessConstructor | undefined;
|
|
18
20
|
/**
|
|
19
21
|
* Captura o `E_UNAUTHORIZED_ACCESS` real do `@adonisjs/auth` sem import
|
|
20
22
|
* estático. Chamado no boot pelo {@link oidcRpGuard} (falha cedo e com uma
|
|
21
23
|
* mensagem útil se o peer não estiver instalado) e, como rede de segurança, na
|
|
22
24
|
* primeira `authenticate()` de um guard construído à mão.
|
|
23
25
|
*/
|
|
24
|
-
export declare function loadUnauthorizedAccess(): Promise<UnauthorizedAccessConstructor>;
|
|
26
|
+
export declare function loadUnauthorizedAccess(guardFactoryName?: string): Promise<UnauthorizedAccessConstructor>;
|
|
25
27
|
export type OidcRpGuardOptions<UserProvider extends SessionUserProviderContract<unknown>> = {
|
|
26
28
|
provider: UserProvider | ConfigProvider<UserProvider>;
|
|
27
29
|
sessionKey?: string;
|
|
@@ -2,13 +2,17 @@ import { RuntimeException } from '@adonisjs/core/exceptions';
|
|
|
2
2
|
import { ACCOUNT_SESSION_KEY } from './account_session_key.js';
|
|
3
3
|
/** Memo do construtor entre instâncias — o `import()` só paga o custo uma vez. */
|
|
4
4
|
let cachedUnauthorizedAccess;
|
|
5
|
+
/** O construtor já resolvido por {@link loadUnauthorizedAccess}, se houver. */
|
|
6
|
+
export function cachedUnauthorizedAccessConstructor() {
|
|
7
|
+
return cachedUnauthorizedAccess;
|
|
8
|
+
}
|
|
5
9
|
/**
|
|
6
10
|
* Captura o `E_UNAUTHORIZED_ACCESS` real do `@adonisjs/auth` sem import
|
|
7
11
|
* estático. Chamado no boot pelo {@link oidcRpGuard} (falha cedo e com uma
|
|
8
12
|
* mensagem útil se o peer não estiver instalado) e, como rede de segurança, na
|
|
9
13
|
* primeira `authenticate()` de um guard construído à mão.
|
|
10
14
|
*/
|
|
11
|
-
export async function loadUnauthorizedAccess() {
|
|
15
|
+
export async function loadUnauthorizedAccess(guardFactoryName = 'oidcRpGuard') {
|
|
12
16
|
if (cachedUnauthorizedAccess)
|
|
13
17
|
return cachedUnauthorizedAccess;
|
|
14
18
|
try {
|
|
@@ -17,7 +21,7 @@ export async function loadUnauthorizedAccess() {
|
|
|
17
21
|
return cachedUnauthorizedAccess;
|
|
18
22
|
}
|
|
19
23
|
catch (error) {
|
|
20
|
-
throw new RuntimeException(
|
|
24
|
+
throw new RuntimeException(`${guardFactoryName}() precisa de "@adonisjs/auth" instalado (é um peer opcional do ` +
|
|
21
25
|
'@adonis-agora/authkit-server, só necessário se você plugar este guard em config/auth.ts). ' +
|
|
22
26
|
'Rode `npm i @adonisjs/auth` (ou pnpm/yarn).', { cause: error });
|
|
23
27
|
}
|