@adonis-agora/authkit-client 0.14.0 → 0.16.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/build/commands/configure.js +5 -3
- package/build/index.d.ts +32 -28
- package/build/index.js +18 -15
- package/build/providers/authkit_client_provider.d.ts +66 -8
- package/build/providers/authkit_client_provider.js +130 -21
- package/build/services/main.d.ts +1 -1
- package/build/services/main.js +2 -2
- package/build/src/authenticator.d.ts +3 -3
- package/build/src/authenticator.js +4 -4
- package/build/src/define_config.d.ts +5 -5
- package/build/src/define_config.js +5 -5
- package/build/src/discovery.d.ts +1 -1
- package/build/src/discovery.js +5 -5
- package/build/src/dpop.js +1 -1
- package/build/src/host/authkit_client_guard.d.ts +100 -0
- package/build/src/host/authkit_client_guard.js +153 -0
- package/build/src/host/request_authenticator.d.ts +31 -0
- package/build/src/host/request_authenticator.js +24 -0
- package/build/src/middleware/authkit_context_middleware.d.ts +28 -0
- package/build/src/middleware/authkit_context_middleware.js +30 -0
- package/build/src/middleware/backchannel_revocation_middleware.js +3 -4
- package/build/src/observability/context_bridge.d.ts +1 -1
- package/build/src/observability/context_bridge.js +8 -8
- package/build/src/oidc_login.d.ts +2 -2
- package/build/src/oidc_login.js +29 -31
- package/build/src/register_oidc_client.d.ts +1 -1
- package/build/src/register_oidc_client.js +7 -4
- package/build/src/resolvers/factory.js +2 -2
- package/build/src/resolvers/jwt_resolver.d.ts +1 -1
- package/build/src/resolvers/jwt_resolver.js +12 -1
- package/build/src/resolvers/opaque_resolver.d.ts +1 -1
- package/build/src/resolvers/pat_resolver.d.ts +1 -1
- package/build/src/revocation/revocation_store.js +0 -0
- package/build/src/types.d.ts +9 -0
- package/build/types.d.ts +1 -1
- package/package.json +10 -3
- package/stubs/main.ts +2 -2
|
@@ -4,13 +4,15 @@ export async function configure(command) {
|
|
|
4
4
|
await codemods.makeUsingStub(stubsRoot, 'config/authkit_client.stub', {});
|
|
5
5
|
await codemods.makeUsingStub(stubsRoot, 'controllers/oidc_session_controller.stub', {});
|
|
6
6
|
await codemods.updateRcFile((rc) => rc.addProvider('@adonis-agora/authkit-client/authkit_client_provider'));
|
|
7
|
-
await codemods.registerMiddleware('router', [
|
|
7
|
+
await codemods.registerMiddleware('router', [
|
|
8
|
+
{ path: '@adonis-agora/authkit-client/authkit_middleware' },
|
|
9
|
+
]);
|
|
8
10
|
await codemods.defineEnvValidations({
|
|
9
11
|
leadingComment: 'Variáveis do @adonis-agora/authkit-client (OIDC client)',
|
|
10
12
|
variables: {
|
|
11
13
|
AUTHKIT_ISSUER: `Env.schema.string({ format: 'url', tld: false })`,
|
|
12
|
-
AUTHKIT_CLIENT_ID:
|
|
13
|
-
AUTHKIT_CLIENT_SECRET:
|
|
14
|
+
AUTHKIT_CLIENT_ID: 'Env.schema.string()',
|
|
15
|
+
AUTHKIT_CLIENT_SECRET: 'Env.schema.string.optional()',
|
|
14
16
|
AUTHKIT_REDIRECT_URI: `Env.schema.string({ format: 'url', tld: false })`,
|
|
15
17
|
},
|
|
16
18
|
});
|
package/build/index.d.ts
CHANGED
|
@@ -1,28 +1,32 @@
|
|
|
1
|
-
export { defineConfig, resolvers } from
|
|
2
|
-
export { Authenticator } from
|
|
3
|
-
export { identityToUser, createUserinfoResolver, } from
|
|
4
|
-
export type { ClaimsUser, ResolveUserContext, UserinfoResolverOptions, } from
|
|
5
|
-
export { buildAuthorizeUrl, buildEndSessionUrl, exchangeCode, exchangeToken, refreshTokens, generatePkce, } from
|
|
6
|
-
export type { EndSessionParams, RefreshParams } from
|
|
7
|
-
export { discoverEndpoints, conventionEndpoints } from
|
|
8
|
-
export type { OidcEndpoints, DiscoverOptions } from
|
|
9
|
-
export { resilientFetch } from
|
|
10
|
-
export type { ResiliencePolicy } from
|
|
11
|
-
export { AuthkitClientManager } from
|
|
12
|
-
export type { ResolvedClientConfig, ClientConfigInput, BackchannelLogoutCallback, BackchannelLogoutInput, } from
|
|
13
|
-
export { lucidRevocationStore, DEFAULT_REVOCATION_TABLE, } from
|
|
14
|
-
export type { RevocationStore, LucidRevocationStoreOptions, } from
|
|
15
|
-
export { default as BackchannelRevocationMiddleware } from
|
|
16
|
-
export {
|
|
17
|
-
export type {
|
|
18
|
-
export {
|
|
19
|
-
export
|
|
20
|
-
export {
|
|
21
|
-
export type {
|
|
22
|
-
export {
|
|
23
|
-
export type {
|
|
24
|
-
export {
|
|
25
|
-
export type {
|
|
26
|
-
export
|
|
27
|
-
export type {
|
|
28
|
-
export {
|
|
1
|
+
export { defineConfig, resolvers } from './src/define_config.js';
|
|
2
|
+
export { Authenticator } from './src/authenticator.js';
|
|
3
|
+
export { identityToUser, createUserinfoResolver, } from './src/user_resolvers.js';
|
|
4
|
+
export type { ClaimsUser, ResolveUserContext, UserinfoResolverOptions, } from './src/user_resolvers.js';
|
|
5
|
+
export { buildAuthorizeUrl, buildEndSessionUrl, exchangeCode, exchangeToken, refreshTokens, generatePkce, } from './src/oidc_login.js';
|
|
6
|
+
export type { EndSessionParams, RefreshParams } from './src/oidc_login.js';
|
|
7
|
+
export { discoverEndpoints, conventionEndpoints } from './src/discovery.js';
|
|
8
|
+
export type { OidcEndpoints, DiscoverOptions } from './src/discovery.js';
|
|
9
|
+
export { resilientFetch } from './src/http/resilient_fetch.js';
|
|
10
|
+
export type { ResiliencePolicy } from './src/http/resilient_fetch.js';
|
|
11
|
+
export { AuthkitClientManager } from './providers/authkit_client_provider.js';
|
|
12
|
+
export type { ResolvedClientConfig, ClientConfigInput, BackchannelLogoutCallback, BackchannelLogoutInput, } from './src/define_config.js';
|
|
13
|
+
export { lucidRevocationStore, DEFAULT_REVOCATION_TABLE, } from './src/revocation/revocation_store.js';
|
|
14
|
+
export type { RevocationStore, LucidRevocationStoreOptions, } from './src/revocation/revocation_store.js';
|
|
15
|
+
export { default as BackchannelRevocationMiddleware } from './src/middleware/backchannel_revocation_middleware.js';
|
|
16
|
+
export { authkitClientGuard, AuthkitClientGuard } from './src/host/authkit_client_guard.js';
|
|
17
|
+
export type { UnauthorizedAccessConstructor } from './src/host/authkit_client_guard.js';
|
|
18
|
+
export { getAuthkit } from './src/host/request_authenticator.js';
|
|
19
|
+
export { default as AuthkitContextMiddleware } from './src/middleware/authkit_context_middleware.js';
|
|
20
|
+
export { lucidMirror } from './src/lucid_mirror.js';
|
|
21
|
+
export type { LucidMirrorOptions } from './src/lucid_mirror.js';
|
|
22
|
+
export { registerOidcClient } from './src/register_oidc_client.js';
|
|
23
|
+
export type { RegisterOidcClientOptions, PostLoginRedirects, } from './src/register_oidc_client.js';
|
|
24
|
+
export { validateLogoutToken, InvalidLogoutTokenError, InMemorySessionIndex, BACKCHANNEL_LOGOUT_EVENT, } from './src/backchannel_logout.js';
|
|
25
|
+
export type { ValidateLogoutTokenOptions, ValidatedLogoutToken, SessionIndex, SessionIndexEntry, } from './src/backchannel_logout.js';
|
|
26
|
+
export { generateDpopKeyPair, createDpopProof, dpopJwkThumbprint, } from './src/dpop.js';
|
|
27
|
+
export type { DpopKeyPair, CreateDpopProofInput } from './src/dpop.js';
|
|
28
|
+
export { verifyJwtAccessToken, clearJwksCache, } from './src/verify_access_token.js';
|
|
29
|
+
export type { VerifyJwtAccessTokenOptions, JwtAccessTokenClaims, } from './src/verify_access_token.js';
|
|
30
|
+
export type { TokenSet } from './src/types.js';
|
|
31
|
+
export type { Identity } from '@adonis-agora/authkit-core';
|
|
32
|
+
export { configure } from './commands/configure.js';
|
package/build/index.js
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
|
-
export { defineConfig, resolvers } from
|
|
2
|
-
export { Authenticator } from
|
|
3
|
-
export { identityToUser, createUserinfoResolver, } from
|
|
4
|
-
export { buildAuthorizeUrl, buildEndSessionUrl, exchangeCode, exchangeToken, refreshTokens, generatePkce, } from
|
|
5
|
-
export { discoverEndpoints, conventionEndpoints } from
|
|
6
|
-
export { resilientFetch } from
|
|
7
|
-
export { AuthkitClientManager } from
|
|
8
|
-
export { lucidRevocationStore, DEFAULT_REVOCATION_TABLE, } from
|
|
9
|
-
export { default as BackchannelRevocationMiddleware } from
|
|
10
|
-
export {
|
|
11
|
-
export {
|
|
12
|
-
export {
|
|
13
|
-
export {
|
|
14
|
-
export {
|
|
15
|
-
export {
|
|
1
|
+
export { defineConfig, resolvers } from './src/define_config.js';
|
|
2
|
+
export { Authenticator } from './src/authenticator.js';
|
|
3
|
+
export { identityToUser, createUserinfoResolver, } from './src/user_resolvers.js';
|
|
4
|
+
export { buildAuthorizeUrl, buildEndSessionUrl, exchangeCode, exchangeToken, refreshTokens, generatePkce, } from './src/oidc_login.js';
|
|
5
|
+
export { discoverEndpoints, conventionEndpoints } from './src/discovery.js';
|
|
6
|
+
export { resilientFetch } from './src/http/resilient_fetch.js';
|
|
7
|
+
export { AuthkitClientManager } from './providers/authkit_client_provider.js';
|
|
8
|
+
export { lucidRevocationStore, DEFAULT_REVOCATION_TABLE, } from './src/revocation/revocation_store.js';
|
|
9
|
+
export { default as BackchannelRevocationMiddleware } from './src/middleware/backchannel_revocation_middleware.js';
|
|
10
|
+
export { authkitClientGuard, AuthkitClientGuard } from './src/host/authkit_client_guard.js';
|
|
11
|
+
export { getAuthkit } from './src/host/request_authenticator.js';
|
|
12
|
+
export { default as AuthkitContextMiddleware } from './src/middleware/authkit_context_middleware.js';
|
|
13
|
+
export { lucidMirror } from './src/lucid_mirror.js';
|
|
14
|
+
export { registerOidcClient } from './src/register_oidc_client.js';
|
|
15
|
+
export { validateLogoutToken, InvalidLogoutTokenError, InMemorySessionIndex, BACKCHANNEL_LOGOUT_EVENT, } from './src/backchannel_logout.js';
|
|
16
|
+
export { generateDpopKeyPair, createDpopProof, dpopJwkThumbprint, } from './src/dpop.js';
|
|
17
|
+
export { verifyJwtAccessToken, clearJwksCache, } from './src/verify_access_token.js';
|
|
18
|
+
export { configure } from './commands/configure.js';
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type { HttpContext } from
|
|
2
|
-
import type { ApplicationService } from
|
|
3
|
-
import { Authenticator } from
|
|
4
|
-
import type { ResolvedClientConfig } from
|
|
1
|
+
import type { HttpContext } from '@adonisjs/core/http';
|
|
2
|
+
import type { ApplicationService } from '@adonisjs/core/types';
|
|
3
|
+
import { Authenticator } from '../src/authenticator.js';
|
|
4
|
+
import type { ResolvedClientConfig } from '../src/define_config.js';
|
|
5
|
+
import type { TokenSet } from '../src/types.js';
|
|
5
6
|
/** Deps injetáveis (relógio/fetch) para testar `maybeRefresh` sem rede/tempo real. */
|
|
6
7
|
export interface RefreshDeps {
|
|
7
8
|
now?: () => number;
|
|
@@ -23,6 +24,9 @@ export declare class AuthkitClientManager {
|
|
|
23
24
|
* renovado — incluindo o refresh_token ROTACIONADO — de volta na sessão. Preserva
|
|
24
25
|
* o id_token / refresh_token anteriores quando o IdP não os reemite.
|
|
25
26
|
*
|
|
27
|
+
* Preserva TAMBÉM o `impersonationBinding`: renovar o token do alvo não muda a
|
|
28
|
+
* sessão, então não pode invalidar o vínculo e trancar o ator na impersonação.
|
|
29
|
+
*
|
|
26
30
|
* Best-effort: qualquer falha (token revogado, IdP fora) é silenciosa — o resolver
|
|
27
31
|
* lida com a sessão expirada no fluxo normal. Chamado pelo middleware por request.
|
|
28
32
|
*/
|
|
@@ -43,15 +47,69 @@ export declare class AuthkitClientManager {
|
|
|
43
47
|
* do chamador (ex.: Bouncer) ANTES de chamar este método.
|
|
44
48
|
*/
|
|
45
49
|
impersonate(ctx: HttpContext, requestedSubject: string): Promise<void>;
|
|
46
|
-
/**
|
|
50
|
+
/**
|
|
51
|
+
* Encerra a impersonação restaurando o token set do ator — SE a sessão que pede
|
|
52
|
+
* ainda for aquela para a qual a credencial foi guardada.
|
|
53
|
+
*
|
|
54
|
+
* A credencial parqueada é um refresh token vivo; sem essa conferência o restore
|
|
55
|
+
* a entrega a quem estiver de posse do cookie jar depois (logout do ator + login
|
|
56
|
+
* de outra identidade). A prova é o `impersonationBinding`: um nonce escrito ao
|
|
57
|
+
* mesmo tempo dentro do token set ATIVO e do registro parqueado. Só `impersonate()`
|
|
58
|
+
* o escreve e só o `maybeRefresh` o propaga — qualquer troca de identidade
|
|
59
|
+
* reescreve o slot da sessão e o vínculo some.
|
|
60
|
+
*
|
|
61
|
+
* As conferências ÓBVIAS não servem, e é por isso que o vínculo existe:
|
|
62
|
+
* - `sub === sub do ator`: durante uma impersonação legítima a sessão é a do
|
|
63
|
+
* ALVO, então isso nunca é verdade — trancaria o ator dentro da impersonação;
|
|
64
|
+
* - "o token parqueado é de um admin?": sempre verdade; o atacante não alega
|
|
65
|
+
* ser o ator, ele pede a credencial do ator;
|
|
66
|
+
* - gate por papel: o ator impersonando carrega os papéis do ALVO nesse momento.
|
|
67
|
+
*
|
|
68
|
+
* Recusa é FAIL-CLOSED: a credencial órfã é DESCARTADA (nunca devolvida) e o
|
|
69
|
+
* token set da sessão também cai — uma recusa não é uma tentativa a repetir, e
|
|
70
|
+
* ninguém fica preso impersonando.
|
|
71
|
+
*
|
|
72
|
+
* LIMITE HONESTO: quem controla o cookie jar DURANTE uma impersonação ativa é
|
|
73
|
+
* indistinguível do ator — é literalmente a mesma sessão, e o vínculo viaja nela.
|
|
74
|
+
* Isso é o modelo de sessão, não esta conferência. O que fecha é a janela em que
|
|
75
|
+
* a credencial sobrevive à sessão que a guardou.
|
|
76
|
+
*
|
|
77
|
+
* @returns `true` só quando o ator foi restaurado.
|
|
78
|
+
*/
|
|
47
79
|
stopImpersonating(ctx: HttpContext): Promise<boolean>;
|
|
48
|
-
/**
|
|
80
|
+
/**
|
|
81
|
+
* Indica se a request corrente está impersonando (p/ exibir banner na UI). Exige o
|
|
82
|
+
* mesmo vínculo do `stopImpersonating` — uma credencial órfã de outra sessão não
|
|
83
|
+
* faz o banner aparecer para quem não está impersonando ninguém.
|
|
84
|
+
*/
|
|
49
85
|
isImpersonating(ctx: HttpContext): boolean;
|
|
86
|
+
/**
|
|
87
|
+
* Encerra a sessão do AuthKit: remove o token set E qualquer credencial de
|
|
88
|
+
* impersonação parqueada. Todo caminho da lib que derruba a sessão passa por aqui
|
|
89
|
+
* — logout RP-initiated (`registerOidcClient`) e revogação back-channel.
|
|
90
|
+
*
|
|
91
|
+
* Existe porque um `session.forget(sessionKey)` solto deixava o refresh token do
|
|
92
|
+
* ATOR parqueado no browser depois de uma revogação central — o mecanismo que o
|
|
93
|
+
* operador usa justamente para matar a sessão.
|
|
94
|
+
*/
|
|
95
|
+
endSession(ctx: HttpContext): void;
|
|
96
|
+
/**
|
|
97
|
+
* Instala o token set de um login recém-concluído, descartando o que sobrou da
|
|
98
|
+
* sessão anterior no mesmo cookie jar.
|
|
99
|
+
*
|
|
100
|
+
* O `stopImpersonating` já recusa uma credencial órfã — o token set novo nasce sem
|
|
101
|
+
* `impersonationBinding`, então o vínculo não fecha. Mas recusar é só não DEVOLVER:
|
|
102
|
+
* sem esta limpeza o refresh token do ator continua serializado no cookie de sessão
|
|
103
|
+
* de outra identidade até a sessão expirar. Não devolver e não guardar são coisas
|
|
104
|
+
* diferentes, e a segunda é a que mantém a invariante — a credencial parqueada nunca
|
|
105
|
+
* sobrevive à sessão que a parqueou.
|
|
106
|
+
*/
|
|
107
|
+
startSession(ctx: HttpContext, tokenSet: TokenSet): void;
|
|
50
108
|
createAuthenticator(ctx: HttpContext): Promise<Authenticator>;
|
|
51
109
|
}
|
|
52
|
-
declare module
|
|
110
|
+
declare module '@adonisjs/core/types' {
|
|
53
111
|
interface ContainerBindings {
|
|
54
|
-
|
|
112
|
+
'authkit.client': AuthkitClientManager;
|
|
55
113
|
}
|
|
56
114
|
}
|
|
57
115
|
export default class AuthkitClientProvider {
|
|
@@ -1,10 +1,22 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { configProvider } from '@adonisjs/core';
|
|
3
|
+
import { RuntimeException } from '@adonisjs/core/exceptions';
|
|
4
|
+
import { Authenticator } from '../src/authenticator.js';
|
|
5
|
+
import { validateLogoutToken } from '../src/backchannel_logout.js';
|
|
6
|
+
import { exchangeToken, refreshTokens } from '../src/oidc_login.js';
|
|
6
7
|
/** Margem (ms) antes do `expiresAt` em que o access token é renovado proativamente. */
|
|
7
8
|
const REFRESH_SKEW_MS = 60_000;
|
|
9
|
+
/** Aceita só o formato COM vínculo; o formato legado (TokenSet cru) vira `null`. */
|
|
10
|
+
function readParked(value) {
|
|
11
|
+
if (!value || typeof value !== 'object')
|
|
12
|
+
return null;
|
|
13
|
+
const candidate = value;
|
|
14
|
+
if (typeof candidate.binding !== 'string' || !candidate.binding)
|
|
15
|
+
return null;
|
|
16
|
+
if (!candidate.tokenSet || typeof candidate.tokenSet !== 'object')
|
|
17
|
+
return null;
|
|
18
|
+
return candidate;
|
|
19
|
+
}
|
|
8
20
|
export class AuthkitClientManager {
|
|
9
21
|
config;
|
|
10
22
|
#resolverCache;
|
|
@@ -40,6 +52,9 @@ export class AuthkitClientManager {
|
|
|
40
52
|
* renovado — incluindo o refresh_token ROTACIONADO — de volta na sessão. Preserva
|
|
41
53
|
* o id_token / refresh_token anteriores quando o IdP não os reemite.
|
|
42
54
|
*
|
|
55
|
+
* Preserva TAMBÉM o `impersonationBinding`: renovar o token do alvo não muda a
|
|
56
|
+
* sessão, então não pode invalidar o vínculo e trancar o ator na impersonação.
|
|
57
|
+
*
|
|
43
58
|
* Best-effort: qualquer falha (token revogado, IdP fora) é silenciosa — o resolver
|
|
44
59
|
* lida com a sessão expirada no fluxo normal. Chamado pelo middleware por request.
|
|
45
60
|
*/
|
|
@@ -68,6 +83,7 @@ export class AuthkitClientManager {
|
|
|
68
83
|
accessToken: next.accessToken,
|
|
69
84
|
refreshToken: next.refreshToken ?? tokenSet.refreshToken,
|
|
70
85
|
expiresAt: next.expiresAt,
|
|
86
|
+
impersonationBinding: tokenSet.impersonationBinding,
|
|
71
87
|
});
|
|
72
88
|
}
|
|
73
89
|
catch {
|
|
@@ -84,10 +100,10 @@ export class AuthkitClientManager {
|
|
|
84
100
|
*/
|
|
85
101
|
async handleBackchannelLogout(ctx) {
|
|
86
102
|
const { request, response } = ctx;
|
|
87
|
-
response.header(
|
|
88
|
-
const token = request.input(
|
|
89
|
-
if (!token || typeof token !==
|
|
90
|
-
return response.badRequest({ error:
|
|
103
|
+
response.header('Cache-Control', 'no-store');
|
|
104
|
+
const token = request.input('logout_token');
|
|
105
|
+
if (!token || typeof token !== 'string') {
|
|
106
|
+
return response.badRequest({ error: 'invalid_request' });
|
|
91
107
|
}
|
|
92
108
|
let validated;
|
|
93
109
|
try {
|
|
@@ -97,7 +113,7 @@ export class AuthkitClientManager {
|
|
|
97
113
|
});
|
|
98
114
|
}
|
|
99
115
|
catch {
|
|
100
|
-
return response.badRequest({ error:
|
|
116
|
+
return response.badRequest({ error: 'invalid_request' });
|
|
101
117
|
}
|
|
102
118
|
// Atualiza o índice local (se configurado) — revoga as sessões mapeadas.
|
|
103
119
|
const index = this.config.sessionIndex;
|
|
@@ -126,9 +142,11 @@ export class AuthkitClientManager {
|
|
|
126
142
|
*/
|
|
127
143
|
async impersonate(ctx, requestedSubject) {
|
|
128
144
|
const session = ctx.session;
|
|
129
|
-
|
|
145
|
+
if (!session)
|
|
146
|
+
throw new Error('Sem sessão ativa para impersonar a partir dela');
|
|
147
|
+
const current = session.get(this.config.sessionKey);
|
|
130
148
|
if (!current?.accessToken) {
|
|
131
|
-
throw new Error(
|
|
149
|
+
throw new Error('Sem sessão ativa para impersonar a partir dela');
|
|
132
150
|
}
|
|
133
151
|
const impersonated = await exchangeToken({
|
|
134
152
|
issuer: this.config.issuer,
|
|
@@ -138,22 +156,113 @@ export class AuthkitClientManager {
|
|
|
138
156
|
requestedSubject,
|
|
139
157
|
resilience: this.config.resilience,
|
|
140
158
|
});
|
|
141
|
-
|
|
142
|
-
|
|
159
|
+
// Vínculo: o mesmo nonce nos dois lados. O lado ATIVO mora dentro do token set
|
|
160
|
+
// da sessão, o único slot que todo caminho de fim-de-sessão já limpa.
|
|
161
|
+
const binding = randomUUID();
|
|
162
|
+
session.put(this.#impersonatorKey, {
|
|
163
|
+
tokenSet: current,
|
|
164
|
+
binding,
|
|
165
|
+
});
|
|
166
|
+
session.put(this.config.sessionKey, { ...impersonated, impersonationBinding: binding });
|
|
143
167
|
}
|
|
144
|
-
/**
|
|
168
|
+
/**
|
|
169
|
+
* Encerra a impersonação restaurando o token set do ator — SE a sessão que pede
|
|
170
|
+
* ainda for aquela para a qual a credencial foi guardada.
|
|
171
|
+
*
|
|
172
|
+
* A credencial parqueada é um refresh token vivo; sem essa conferência o restore
|
|
173
|
+
* a entrega a quem estiver de posse do cookie jar depois (logout do ator + login
|
|
174
|
+
* de outra identidade). A prova é o `impersonationBinding`: um nonce escrito ao
|
|
175
|
+
* mesmo tempo dentro do token set ATIVO e do registro parqueado. Só `impersonate()`
|
|
176
|
+
* o escreve e só o `maybeRefresh` o propaga — qualquer troca de identidade
|
|
177
|
+
* reescreve o slot da sessão e o vínculo some.
|
|
178
|
+
*
|
|
179
|
+
* As conferências ÓBVIAS não servem, e é por isso que o vínculo existe:
|
|
180
|
+
* - `sub === sub do ator`: durante uma impersonação legítima a sessão é a do
|
|
181
|
+
* ALVO, então isso nunca é verdade — trancaria o ator dentro da impersonação;
|
|
182
|
+
* - "o token parqueado é de um admin?": sempre verdade; o atacante não alega
|
|
183
|
+
* ser o ator, ele pede a credencial do ator;
|
|
184
|
+
* - gate por papel: o ator impersonando carrega os papéis do ALVO nesse momento.
|
|
185
|
+
*
|
|
186
|
+
* Recusa é FAIL-CLOSED: a credencial órfã é DESCARTADA (nunca devolvida) e o
|
|
187
|
+
* token set da sessão também cai — uma recusa não é uma tentativa a repetir, e
|
|
188
|
+
* ninguém fica preso impersonando.
|
|
189
|
+
*
|
|
190
|
+
* LIMITE HONESTO: quem controla o cookie jar DURANTE uma impersonação ativa é
|
|
191
|
+
* indistinguível do ator — é literalmente a mesma sessão, e o vínculo viaja nela.
|
|
192
|
+
* Isso é o modelo de sessão, não esta conferência. O que fecha é a janela em que
|
|
193
|
+
* a credencial sobrevive à sessão que a guardou.
|
|
194
|
+
*
|
|
195
|
+
* @returns `true` só quando o ator foi restaurado.
|
|
196
|
+
*/
|
|
145
197
|
async stopImpersonating(ctx) {
|
|
146
198
|
const session = ctx.session;
|
|
147
|
-
|
|
148
|
-
|
|
199
|
+
if (!session)
|
|
200
|
+
return false;
|
|
201
|
+
const raw = session.get(this.#impersonatorKey);
|
|
202
|
+
if (raw === undefined || raw === null)
|
|
149
203
|
return false;
|
|
204
|
+
const parked = readParked(raw);
|
|
205
|
+
const live = session.get(this.config.sessionKey);
|
|
206
|
+
// `parked === null` cobre o formato pré-0.15 (TokenSet cru, sem vínculo nenhum):
|
|
207
|
+
// é justamente a credencial que não dá pra atribuir. Recusada como qualquer outra.
|
|
208
|
+
if (!parked || !live?.impersonationBinding || live.impersonationBinding !== parked.binding) {
|
|
209
|
+
session.forget(this.#impersonatorKey);
|
|
210
|
+
session.forget(this.config.sessionKey);
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
const { impersonationBinding: _binding, ...original } = parked.tokenSet;
|
|
150
214
|
session.put(this.config.sessionKey, original);
|
|
151
215
|
session.forget(this.#impersonatorKey);
|
|
152
216
|
return true;
|
|
153
217
|
}
|
|
154
|
-
/**
|
|
218
|
+
/**
|
|
219
|
+
* Indica se a request corrente está impersonando (p/ exibir banner na UI). Exige o
|
|
220
|
+
* mesmo vínculo do `stopImpersonating` — uma credencial órfã de outra sessão não
|
|
221
|
+
* faz o banner aparecer para quem não está impersonando ninguém.
|
|
222
|
+
*/
|
|
155
223
|
isImpersonating(ctx) {
|
|
156
|
-
|
|
224
|
+
const session = ctx.session;
|
|
225
|
+
if (!session)
|
|
226
|
+
return false;
|
|
227
|
+
const parked = readParked(session.get(this.#impersonatorKey));
|
|
228
|
+
if (!parked)
|
|
229
|
+
return false;
|
|
230
|
+
const live = session.get(this.config.sessionKey);
|
|
231
|
+
return live?.impersonationBinding === parked.binding;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Encerra a sessão do AuthKit: remove o token set E qualquer credencial de
|
|
235
|
+
* impersonação parqueada. Todo caminho da lib que derruba a sessão passa por aqui
|
|
236
|
+
* — logout RP-initiated (`registerOidcClient`) e revogação back-channel.
|
|
237
|
+
*
|
|
238
|
+
* Existe porque um `session.forget(sessionKey)` solto deixava o refresh token do
|
|
239
|
+
* ATOR parqueado no browser depois de uma revogação central — o mecanismo que o
|
|
240
|
+
* operador usa justamente para matar a sessão.
|
|
241
|
+
*/
|
|
242
|
+
endSession(ctx) {
|
|
243
|
+
const session = ctx.session;
|
|
244
|
+
if (!session)
|
|
245
|
+
return;
|
|
246
|
+
session.forget(this.config.sessionKey);
|
|
247
|
+
session.forget(this.#impersonatorKey);
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Instala o token set de um login recém-concluído, descartando o que sobrou da
|
|
251
|
+
* sessão anterior no mesmo cookie jar.
|
|
252
|
+
*
|
|
253
|
+
* O `stopImpersonating` já recusa uma credencial órfã — o token set novo nasce sem
|
|
254
|
+
* `impersonationBinding`, então o vínculo não fecha. Mas recusar é só não DEVOLVER:
|
|
255
|
+
* sem esta limpeza o refresh token do ator continua serializado no cookie de sessão
|
|
256
|
+
* de outra identidade até a sessão expirar. Não devolver e não guardar são coisas
|
|
257
|
+
* diferentes, e a segunda é a que mantém a invariante — a credencial parqueada nunca
|
|
258
|
+
* sobrevive à sessão que a parqueou.
|
|
259
|
+
*/
|
|
260
|
+
startSession(ctx, tokenSet) {
|
|
261
|
+
const session = ctx.session;
|
|
262
|
+
if (!session)
|
|
263
|
+
return;
|
|
264
|
+
session.forget(this.#impersonatorKey);
|
|
265
|
+
session.put(this.config.sessionKey, tokenSet);
|
|
157
266
|
}
|
|
158
267
|
async createAuthenticator(ctx) {
|
|
159
268
|
const resolver = await this.#getResolver();
|
|
@@ -174,8 +283,8 @@ export default class AuthkitClientProvider {
|
|
|
174
283
|
this.app = app;
|
|
175
284
|
}
|
|
176
285
|
register() {
|
|
177
|
-
this.app.container.singleton(
|
|
178
|
-
const value = this.app.config.get(
|
|
286
|
+
this.app.container.singleton('authkit.client', async () => {
|
|
287
|
+
const value = this.app.config.get('authkit_client');
|
|
179
288
|
const config = (await configProvider.resolve(this.app, value));
|
|
180
289
|
if (!config) {
|
|
181
290
|
throw new RuntimeException('Config inválido em "config/authkit_client.ts". Use defineConfig de @adonis-agora/authkit-client.');
|
package/build/services/main.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AuthkitClientManager } from
|
|
1
|
+
import type { AuthkitClientManager } from '../providers/authkit_client_provider.js';
|
|
2
2
|
/**
|
|
3
3
|
* Singleton accessor for the {@link AuthkitClientManager}, following the Adonis `services/main`
|
|
4
4
|
* convention (like `db`, `mail`, `drive`). Lets an app write
|
package/build/services/main.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import app from
|
|
1
|
+
import app from '@adonisjs/core/services/app';
|
|
2
2
|
/**
|
|
3
3
|
* Singleton accessor for the {@link AuthkitClientManager}, following the Adonis `services/main`
|
|
4
4
|
* convention (like `db`, `mail`, `drive`). Lets an app write
|
|
@@ -8,6 +8,6 @@ import app from "@adonisjs/core/services/app";
|
|
|
8
8
|
*/
|
|
9
9
|
let manager;
|
|
10
10
|
await app.booted(async () => {
|
|
11
|
-
manager = await app.container.make(
|
|
11
|
+
manager = await app.container.make('authkit.client');
|
|
12
12
|
});
|
|
13
13
|
export { manager as default };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { Identity, SessionResolver } from
|
|
2
|
-
import { type MetricsRecorder } from
|
|
3
|
-
import type { HttpContext } from
|
|
1
|
+
import type { Identity, SessionResolver } from '@adonis-agora/authkit-core';
|
|
2
|
+
import { type MetricsRecorder } from '@adonis-agora/authkit-core';
|
|
3
|
+
import type { HttpContext } from '@adonisjs/core/http';
|
|
4
4
|
/** Contexto opcional repassado a `resolveUser` (extensão backward-compatible). */
|
|
5
5
|
export interface ResolveUserContext {
|
|
6
6
|
accessToken?: string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { AUTHKIT_METRICS, NoopRecorder
|
|
2
|
-
import { populateContext } from
|
|
1
|
+
import { AUTHKIT_METRICS, NoopRecorder } from '@adonis-agora/authkit-core';
|
|
2
|
+
import { populateContext } from './observability/context_bridge.js';
|
|
3
3
|
/**
|
|
4
4
|
* Autenticador do request. O parâmetro `TUser` é o tipo do usuário do app que `resolveUser`
|
|
5
5
|
* devolve — o app o fixa via augmentation de `HttpContext.auth` (`auth: Authenticator<AppUser>`),
|
|
@@ -47,7 +47,7 @@ export class Authenticator {
|
|
|
47
47
|
async authenticate() {
|
|
48
48
|
const identity = await this.getIdentity();
|
|
49
49
|
if (!identity)
|
|
50
|
-
throw new Error(
|
|
50
|
+
throw new Error('Not authenticated');
|
|
51
51
|
return identity;
|
|
52
52
|
}
|
|
53
53
|
async check() {
|
|
@@ -84,7 +84,7 @@ export class Authenticator {
|
|
|
84
84
|
async getUserOrFail() {
|
|
85
85
|
const user = await this.getUser();
|
|
86
86
|
if (user === null) {
|
|
87
|
-
throw new Error(
|
|
87
|
+
throw new Error('Not authenticated: no user for the current session');
|
|
88
88
|
}
|
|
89
89
|
return user;
|
|
90
90
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type { Identity } from
|
|
2
|
-
import type { SessionIndex } from
|
|
3
|
-
import type { ResiliencePolicy } from
|
|
4
|
-
import { type ResolverFactory, resolvers } from
|
|
5
|
-
import type { RevocationStore } from
|
|
1
|
+
import type { Identity } from '@adonis-agora/authkit-core';
|
|
2
|
+
import type { SessionIndex } from './backchannel_logout.js';
|
|
3
|
+
import type { ResiliencePolicy } from './http/resilient_fetch.js';
|
|
4
|
+
import { type ResolverFactory, resolvers } from './resolvers/factory.js';
|
|
5
|
+
import type { RevocationStore } from './revocation/revocation_store.js';
|
|
6
6
|
export { resolvers };
|
|
7
7
|
/** Callback invocado quando um logout_token válido chega via Back-Channel Logout. */
|
|
8
8
|
export type BackchannelLogoutCallback = (event: {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { configProvider } from
|
|
2
|
-
import { resolvers } from
|
|
1
|
+
import { configProvider } from '@adonisjs/core';
|
|
2
|
+
import { resolvers } from './resolvers/factory.js';
|
|
3
3
|
export { resolvers };
|
|
4
4
|
export function defineConfig(config) {
|
|
5
5
|
return configProvider.create(async () => {
|
|
@@ -22,9 +22,9 @@ export function defineConfig(config) {
|
|
|
22
22
|
redirectUri: config.redirectUri,
|
|
23
23
|
resolverFactory: config.resolver,
|
|
24
24
|
resolveUser: config.resolveUser,
|
|
25
|
-
sessionKey: config.sessionKey ??
|
|
26
|
-
scopes: config.scopes ?? [
|
|
27
|
-
globalRolesClaim: config.globalRolesClaim ??
|
|
25
|
+
sessionKey: config.sessionKey ?? 'authkit',
|
|
26
|
+
scopes: config.scopes ?? ['openid', 'profile', 'email', 'offline_access', 'roles'],
|
|
27
|
+
globalRolesClaim: config.globalRolesClaim ?? 'roles',
|
|
28
28
|
onBackchannelLogout,
|
|
29
29
|
sessionIndex: config.sessionIndex,
|
|
30
30
|
backchannelStore: store,
|
package/build/src/discovery.d.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* const tokens = await exchangeCode({ issuer, tokenEndpoint: endpoints.tokenEndpoint, ... })
|
|
17
17
|
* ```
|
|
18
18
|
*/
|
|
19
|
-
import { type ResiliencePolicy } from
|
|
19
|
+
import { type ResiliencePolicy } from './http/resilient_fetch.js';
|
|
20
20
|
export interface OidcEndpoints {
|
|
21
21
|
authorizationEndpoint: string;
|
|
22
22
|
tokenEndpoint: string;
|
package/build/src/discovery.js
CHANGED
|
@@ -16,13 +16,13 @@
|
|
|
16
16
|
* const tokens = await exchangeCode({ issuer, tokenEndpoint: endpoints.tokenEndpoint, ... })
|
|
17
17
|
* ```
|
|
18
18
|
*/
|
|
19
|
-
import { resilientFetch
|
|
19
|
+
import { resilientFetch } from './http/resilient_fetch.js';
|
|
20
20
|
/**
|
|
21
21
|
* Convenção do oidc-provider (authkit-server) — usada como fallback quando o
|
|
22
22
|
* documento de discovery não está acessível.
|
|
23
23
|
*/
|
|
24
24
|
export function conventionEndpoints(issuer) {
|
|
25
|
-
const base = issuer.replace(/\/$/,
|
|
25
|
+
const base = issuer.replace(/\/$/, '');
|
|
26
26
|
return {
|
|
27
27
|
authorizationEndpoint: `${base}/auth`,
|
|
28
28
|
tokenEndpoint: `${base}/token`,
|
|
@@ -43,7 +43,7 @@ const cache = new Map();
|
|
|
43
43
|
* - `overrides` sempre vencem, campo a campo.
|
|
44
44
|
*/
|
|
45
45
|
export async function discoverEndpoints(issuer, options = {}) {
|
|
46
|
-
const base = issuer.replace(/\/$/,
|
|
46
|
+
const base = issuer.replace(/\/$/, '');
|
|
47
47
|
const ttl = options.cacheTtlMs ?? DEFAULT_TTL_MS;
|
|
48
48
|
const cached = cache.get(base);
|
|
49
49
|
let endpoints;
|
|
@@ -66,11 +66,11 @@ export async function discoverEndpoints(issuer, options = {}) {
|
|
|
66
66
|
async function fetchDiscovery(base, fetchImpl, resilience) {
|
|
67
67
|
const fallback = conventionEndpoints(base);
|
|
68
68
|
try {
|
|
69
|
-
const res = await resilientFetch(`${base}/.well-known/openid-configuration`, { headers: { accept:
|
|
69
|
+
const res = await resilientFetch(`${base}/.well-known/openid-configuration`, { headers: { accept: 'application/json' } }, resilience, fetchImpl);
|
|
70
70
|
if (!res.ok)
|
|
71
71
|
return fallback;
|
|
72
72
|
const doc = (await res.json());
|
|
73
|
-
const str = (v) => typeof v ===
|
|
73
|
+
const str = (v) => (typeof v === 'string' && v ? v : undefined);
|
|
74
74
|
return {
|
|
75
75
|
authorizationEndpoint: str(doc.authorization_endpoint) ?? fallback.authorizationEndpoint,
|
|
76
76
|
tokenEndpoint: str(doc.token_endpoint) ?? fallback.tokenEndpoint,
|
package/build/src/dpop.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { SignJWT, exportJWK, generateKeyPair, calculateJwkThumbprint, base64url, } from 'jose';
|
|
2
1
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { SignJWT, base64url, calculateJwkThumbprint, exportJWK, generateKeyPair, } from 'jose';
|
|
3
3
|
/**
|
|
4
4
|
* Gera um par de chaves DPoP ES256 (P-256), exportável como JWK. ES256 é o alg
|
|
5
5
|
* recomendado pelo RFC 9449 e o que o oidc-provider do authkit-server aceita.
|