@adonis-agora/authkit-client 0.20.5 → 1.0.0-rc.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/CHANGELOG.md +957 -0
- package/README.md +47 -12
- package/build/auth.d.ts +2 -0
- package/build/auth.js +1 -0
- package/build/authkit_context_middleware.d.ts +1 -0
- package/build/authkit_context_middleware.js +2 -0
- package/build/commands/configure.d.ts +10 -0
- package/build/commands/configure.js +57 -2
- package/build/index.d.ts +3 -2
- package/build/index.js +1 -1
- package/build/providers/authkit_client_provider.d.ts +8 -1
- package/build/providers/authkit_client_provider.js +28 -12
- package/build/src/authenticator.d.ts +4 -3
- package/build/src/authenticator.js +6 -3
- package/build/src/host/authkit_client_guard.d.ts +51 -36
- package/build/src/host/authkit_client_guard.js +97 -46
- package/build/src/host/dashboard_guard.d.ts +3 -7
- package/build/src/host/dashboard_guard.js +3 -7
- package/build/src/host/request_authenticator.d.ts +28 -10
- package/build/src/host/request_authenticator.js +5 -3
- package/build/src/middleware/authkit_context_middleware.d.ts +3 -4
- package/build/src/middleware/authkit_context_middleware.js +5 -4
- package/build/src/middleware/backchannel_revocation_middleware.d.ts +1 -1
- package/build/src/middleware/backchannel_revocation_middleware.js +1 -1
- package/build/src/middleware/require_org_middleware.d.ts +3 -2
- package/build/src/middleware/require_org_middleware.js +5 -4
- package/build/src/resolvers/dpop_proof.d.ts +34 -0
- package/build/src/resolvers/dpop_proof.js +92 -0
- package/build/src/resolvers/factory.d.ts +5 -0
- package/build/src/resolvers/factory.js +1 -0
- package/build/src/resolvers/jwt_resolver.d.ts +9 -0
- package/build/src/resolvers/jwt_resolver.js +47 -3
- package/build/stubs/config/auth.stub +30 -0
- package/build/stubs/config/authkit_client.stub +2 -1
- package/build/stubs/middleware/auth_middleware.stub +25 -0
- package/build/stubs/middleware/silent_auth_middleware.stub +16 -0
- package/build/types.d.ts +5 -32
- package/build/types.js +1 -1
- package/package.json +10 -15
- package/skills/authkit-rp-client/SKILL.md +102 -47
- package/stubs/config/auth.stub +30 -0
- package/stubs/config/authkit_client.stub +2 -1
- package/stubs/middleware/auth_middleware.stub +25 -0
- package/stubs/middleware/silent_auth_middleware.stub +16 -0
- package/build/src/host/ctx_auth.d.ts +0 -6
- package/build/src/host/ctx_auth.js +0 -8
- package/build/src/middleware/auth_middleware.d.ts +0 -13
- package/build/src/middleware/auth_middleware.js +0 -13
- package/build/src/middleware/authkit_middleware.d.ts +0 -6
- package/build/src/middleware/authkit_middleware.js +0 -24
- package/build/src/middleware/silent_auth_middleware.d.ts +0 -13
- package/build/src/middleware/silent_auth_middleware.js +0 -15
|
@@ -1,14 +1,21 @@
|
|
|
1
|
+
import { errors } from '@adonisjs/auth';
|
|
1
2
|
import { RuntimeException } from '@adonisjs/core/exceptions';
|
|
2
3
|
import { getAuthkit } from './request_authenticator.js';
|
|
4
|
+
function canBuildFromIdentity(provider) {
|
|
5
|
+
return (typeof provider.fromIdentity === 'function');
|
|
6
|
+
}
|
|
3
7
|
/**
|
|
4
|
-
* Guard de `@adonisjs/auth` apoiado na sessão
|
|
8
|
+
* Guard de `@adonisjs/auth` apoiado na sessão do `@adonis-agora/authkit-client`.
|
|
5
9
|
*
|
|
6
10
|
* A fonte da verdade de "quem está logado" é o `SessionResolver` do authkit
|
|
7
|
-
* (JWT
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
11
|
+
* (sessão OIDC, bearer JWT, opaco/introspecção ou PAT, conforme
|
|
12
|
+
* `config/authkit_client.ts` ou a opção `resolver` do guard). O guard não lê a
|
|
13
|
+
* sessão por conta própria.
|
|
14
|
+
*
|
|
15
|
+
* O usuário vem, nesta ordem: do `resolveUser` de `config/authkit_client.ts`
|
|
16
|
+
* (ex.: `lucidMirror`, que cria a linha local no primeiro login; é a MESMA
|
|
17
|
+
* instância de `ctx.authkit.getUser()`), do provider montando o usuário da
|
|
18
|
+
* `Identity` ({@link authkitIdentityProvider}), ou de `provider.findById(sub)`.
|
|
12
19
|
*
|
|
13
20
|
* Consequência: `middleware.auth()`, `ctx.auth.user`, `auth.check()` e o Bouncer
|
|
14
21
|
* passam a funcionar sobre uma sessão do authkit.
|
|
@@ -16,19 +23,19 @@ import { getAuthkit } from './request_authenticator.js';
|
|
|
16
23
|
export class AuthkitClientGuard {
|
|
17
24
|
#ctx;
|
|
18
25
|
#userProvider;
|
|
19
|
-
#
|
|
26
|
+
#resolver;
|
|
20
27
|
#authkit;
|
|
21
28
|
driverName = 'authkit_client';
|
|
22
29
|
authenticationAttempted = false;
|
|
23
30
|
isAuthenticated = false;
|
|
24
31
|
user;
|
|
25
|
-
constructor(_name, ctx, userProvider,
|
|
32
|
+
constructor(_name, ctx, userProvider, options = {}) {
|
|
26
33
|
this.#ctx = ctx;
|
|
27
34
|
this.#userProvider = userProvider;
|
|
28
|
-
this.#
|
|
35
|
+
this.#resolver = options.resolver;
|
|
29
36
|
}
|
|
30
37
|
/**
|
|
31
|
-
* O {@link Authenticator} do authkit
|
|
38
|
+
* O {@link Authenticator} do authkit que este guard usou (a `Identity` crua,
|
|
32
39
|
* `hasGlobalRole`, `toSharedProps`). Só existe depois de uma tentativa de
|
|
33
40
|
* autenticação; use `ctx.authkit` (via `authkit_context_middleware`) quando
|
|
34
41
|
* precisar dele em rota pública.
|
|
@@ -39,32 +46,60 @@ export class AuthkitClientGuard {
|
|
|
39
46
|
#failed() {
|
|
40
47
|
this.isAuthenticated = false;
|
|
41
48
|
this.user = undefined;
|
|
42
|
-
return new
|
|
49
|
+
return new errors.E_UNAUTHORIZED_ACCESS('Invalid or expired user session', {
|
|
43
50
|
guardDriverName: this.driverName,
|
|
44
51
|
});
|
|
45
52
|
}
|
|
46
53
|
/**
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
|
|
51
|
-
|
|
54
|
+
* The authenticator for this request. Without a `resolver` option it is the
|
|
55
|
+
* request's shared `ctx.authkit`. With one, it is a separate authenticator over
|
|
56
|
+
* that resolver; it becomes `ctx.authkit` only when nothing set it yet.
|
|
57
|
+
*/
|
|
58
|
+
async #authenticator() {
|
|
59
|
+
if (!this.#resolver)
|
|
60
|
+
return getAuthkit(this.#ctx);
|
|
61
|
+
const manager = await this.#ctx.containerResolver.make('authkit.client');
|
|
62
|
+
const authenticator = (await manager.createAuthenticator(this.#ctx, {
|
|
63
|
+
resolver: this.#resolver,
|
|
64
|
+
}));
|
|
65
|
+
const ctx = this.#ctx;
|
|
66
|
+
if (!ctx.authkit)
|
|
67
|
+
ctx.authkit = authenticator;
|
|
68
|
+
return authenticator;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Autentica a request. Lança `E_UNAUTHORIZED_ACCESS` (o MESMO erro dos guards
|
|
72
|
+
* nativos, então o `middleware.auth()` e o handler de exceção do host tratam
|
|
73
|
+
* igual) quando não há sessão OU quando a sessão é válida mas nenhuma fonte
|
|
74
|
+
* devolve usuário (fail-closed).
|
|
52
75
|
*/
|
|
53
76
|
async authenticate() {
|
|
54
77
|
if (this.authenticationAttempted)
|
|
55
78
|
return this.getUserOrFail();
|
|
56
79
|
this.authenticationAttempted = true;
|
|
57
|
-
const authkit = await
|
|
80
|
+
const authkit = await this.#authenticator();
|
|
58
81
|
this.#authkit = authkit;
|
|
59
82
|
const identity = await authkit.getIdentity();
|
|
60
83
|
if (!identity)
|
|
61
84
|
throw this.#failed();
|
|
62
|
-
// `
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
-
|
|
85
|
+
// `resolveUser` (ex.: `lucidMirror`) provisiona a linha local no primeiro
|
|
86
|
+
// login e devolve o usuário enriquecido; reaproveitá-lo mantém UMA instância
|
|
87
|
+
// por request, compartilhada com `ctx.authkit.getUser()`.
|
|
88
|
+
let user = null;
|
|
89
|
+
if (authkit.resolvesUser)
|
|
90
|
+
user = await authkit.getUser();
|
|
91
|
+
if (user === null || user === undefined) {
|
|
92
|
+
const provider = this.#userProvider;
|
|
93
|
+
// `identity.userId` É a claim `sub` — o único id estável por conta que a
|
|
94
|
+
// `Identity` tem. (Ela NÃO tem `id`.)
|
|
95
|
+
const guardUser = canBuildFromIdentity(provider)
|
|
96
|
+
? await provider.fromIdentity(identity)
|
|
97
|
+
: await provider.findById(identity.userId);
|
|
98
|
+
user = guardUser?.getOriginal() ?? null;
|
|
99
|
+
}
|
|
100
|
+
if (user === null || user === undefined)
|
|
66
101
|
throw this.#failed();
|
|
67
|
-
this.user =
|
|
102
|
+
this.user = user;
|
|
68
103
|
this.isAuthenticated = true;
|
|
69
104
|
return this.user;
|
|
70
105
|
}
|
|
@@ -75,7 +110,7 @@ export class AuthkitClientGuard {
|
|
|
75
110
|
return true;
|
|
76
111
|
}
|
|
77
112
|
catch (error) {
|
|
78
|
-
if (error instanceof
|
|
113
|
+
if (error instanceof errors.E_UNAUTHORIZED_ACCESS)
|
|
79
114
|
return false;
|
|
80
115
|
throw error;
|
|
81
116
|
}
|
|
@@ -83,7 +118,7 @@ export class AuthkitClientGuard {
|
|
|
83
118
|
/** O usuário autenticado, ou `E_UNAUTHORIZED_ACCESS`. */
|
|
84
119
|
getUserOrFail() {
|
|
85
120
|
if (!this.user) {
|
|
86
|
-
throw new
|
|
121
|
+
throw new errors.E_UNAUTHORIZED_ACCESS('Invalid or expired user session', {
|
|
87
122
|
guardDriverName: this.driverName,
|
|
88
123
|
});
|
|
89
124
|
}
|
|
@@ -108,7 +143,8 @@ export class AuthkitClientGuard {
|
|
|
108
143
|
* ```ts
|
|
109
144
|
* import { defineConfig } from '@adonisjs/auth'
|
|
110
145
|
* import { sessionUserProvider } from '@adonisjs/auth/session'
|
|
111
|
-
* import { authkitClientGuard } from '@adonis-agora/authkit-client'
|
|
146
|
+
* import { authkitClientGuard, authkitIdentityProvider } from '@adonis-agora/authkit-client/auth'
|
|
147
|
+
* import { resolvers } from '@adonis-agora/authkit-client'
|
|
112
148
|
*
|
|
113
149
|
* export default defineConfig({
|
|
114
150
|
* default: 'web',
|
|
@@ -116,38 +152,53 @@ export class AuthkitClientGuard {
|
|
|
116
152
|
* web: authkitClientGuard({
|
|
117
153
|
* provider: sessionUserProvider({ model: () => import('#models/user') }),
|
|
118
154
|
* }),
|
|
155
|
+
* // API routes authenticated by a bearer access token.
|
|
156
|
+
* api: authkitClientGuard({
|
|
157
|
+
* provider: authkitIdentityProvider(),
|
|
158
|
+
* resolver: resolvers.jwt({ tokenSource: 'bearer' }),
|
|
159
|
+
* }),
|
|
119
160
|
* },
|
|
120
161
|
* })
|
|
121
162
|
* ```
|
|
122
163
|
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
* Precedência com `resolveUser` de `config/authkit_client.ts`: são superfícies
|
|
129
|
-
* SEPARADAS e ambas continuam valendo. `ctx.auth.user` (framework) vem SEMPRE do
|
|
130
|
-
* `provider`; `ctx.authkit.getUser()` (authkit) continua vindo do `resolveUser`.
|
|
131
|
-
* O guard nunca chama `resolveUser`. Com o guard configurado, o normal é apagar
|
|
132
|
-
* o `resolveUser` e deixar só o `provider`.
|
|
164
|
+
* - `provider`: any session user provider. Its `findById` receives `identity.userId`
|
|
165
|
+
* (the `sub` claim). With `resolveUser` in `config/authkit_client.ts`, the guard
|
|
166
|
+
* uses that user first and the provider is the fallback.
|
|
167
|
+
* - `resolver`: replaces the resolver of `config/authkit_client.ts` for this guard
|
|
168
|
+
* only (bearer JWT, opaque, PAT). Without it the guard uses the configured one.
|
|
133
169
|
*/
|
|
134
170
|
export function authkitClientGuard(config) {
|
|
135
171
|
return {
|
|
136
172
|
async resolver(name, app) {
|
|
137
|
-
let unauthorized;
|
|
138
|
-
try {
|
|
139
|
-
const auth = (await import('@adonisjs/auth'));
|
|
140
|
-
unauthorized = auth.errors.E_UNAUTHORIZED_ACCESS;
|
|
141
|
-
}
|
|
142
|
-
catch (error) {
|
|
143
|
-
throw new RuntimeException('authkitClientGuard() precisa de "@adonisjs/auth" instalado (é um peer opcional do ' +
|
|
144
|
-
'@adonis-agora/authkit-client, só necessário se você plugar este guard em config/auth.ts). ' +
|
|
145
|
-
'Rode `npm i @adonisjs/auth` (ou pnpm/yarn).', { cause: error });
|
|
146
|
-
}
|
|
147
173
|
const provider = 'resolver' in config.provider
|
|
148
174
|
? (await config.provider.resolver(app))
|
|
149
175
|
: config.provider;
|
|
150
|
-
return (ctx) => new AuthkitClientGuard(name, ctx, provider,
|
|
176
|
+
return (ctx) => new AuthkitClientGuard(name, ctx, provider, { resolver: config.resolver });
|
|
151
177
|
},
|
|
152
178
|
};
|
|
153
179
|
}
|
|
180
|
+
/**
|
|
181
|
+
* User provider with no database: `ctx.auth.user` is the AuthKit {@link Identity}
|
|
182
|
+
* of the request (`userId`, `email`, `globalRoles`, `profile`, the raw claims).
|
|
183
|
+
* Use it when the app keeps no local user table, or for API guards that only need
|
|
184
|
+
* the token's subject. When `config/authkit_client.ts` sets `resolveUser`, the
|
|
185
|
+
* guard uses that user instead.
|
|
186
|
+
*/
|
|
187
|
+
export function authkitIdentityProvider() {
|
|
188
|
+
return new AuthkitIdentityProvider();
|
|
189
|
+
}
|
|
190
|
+
class AuthkitIdentityProvider {
|
|
191
|
+
#wrap(identity) {
|
|
192
|
+
return { getId: () => identity.userId, getOriginal: () => identity };
|
|
193
|
+
}
|
|
194
|
+
async fromIdentity(identity) {
|
|
195
|
+
return this.#wrap(identity);
|
|
196
|
+
}
|
|
197
|
+
async createUserForGuard(identity) {
|
|
198
|
+
return this.#wrap(identity);
|
|
199
|
+
}
|
|
200
|
+
/** There is no store to look an id up in: only the request's identity exists. */
|
|
201
|
+
async findById() {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
@@ -6,15 +6,11 @@ export interface DashboardGuardOptions {
|
|
|
6
6
|
/**
|
|
7
7
|
* Core predicate behind {@link authkitDashboardAuthorize} and
|
|
8
8
|
* {@link authkitDashboardMiddleware}: authenticated (a resolvable authkit `Identity`) AND holding
|
|
9
|
-
* the configured global role — `ctx.
|
|
9
|
+
* the configured global role — `ctx.authkit.hasGlobalRole('ADMIN')`, the pattern hosts already
|
|
10
10
|
* hand-roll per embedded admin dashboard (durable/media/agent/telescope).
|
|
11
11
|
*
|
|
12
|
-
* Resolves the session via {@link getAuthkit}
|
|
13
|
-
*
|
|
14
|
-
* `@adonisjs/auth`'s guard), so this module — shipped from the library, with no visibility into
|
|
15
|
-
* that augmentation — can't assume its shape. `getAuthkit` is the one session-resolution path that
|
|
16
|
-
* works either way (legacy `authkit_middleware`'s `ctx.auth` or the newer `authkitClientGuard`'s
|
|
17
|
-
* `ctx.authkit`), memoizing so a second call in the same request is free.
|
|
12
|
+
* Resolves the session via {@link getAuthkit}, which memoizes on `ctx.authkit`, so a second call
|
|
13
|
+
* in the same request is free.
|
|
18
14
|
*/
|
|
19
15
|
export declare function isAuthkitAdmin(ctx: HttpContext, options?: DashboardGuardOptions): Promise<boolean>;
|
|
20
16
|
/**
|
|
@@ -6,15 +6,11 @@ const DEFAULT_ADMIN_ROLE = 'ADMIN';
|
|
|
6
6
|
/**
|
|
7
7
|
* Core predicate behind {@link authkitDashboardAuthorize} and
|
|
8
8
|
* {@link authkitDashboardMiddleware}: authenticated (a resolvable authkit `Identity`) AND holding
|
|
9
|
-
* the configured global role — `ctx.
|
|
9
|
+
* the configured global role — `ctx.authkit.hasGlobalRole('ADMIN')`, the pattern hosts already
|
|
10
10
|
* hand-roll per embedded admin dashboard (durable/media/agent/telescope).
|
|
11
11
|
*
|
|
12
|
-
* Resolves the session via {@link getAuthkit}
|
|
13
|
-
*
|
|
14
|
-
* `@adonisjs/auth`'s guard), so this module — shipped from the library, with no visibility into
|
|
15
|
-
* that augmentation — can't assume its shape. `getAuthkit` is the one session-resolution path that
|
|
16
|
-
* works either way (legacy `authkit_middleware`'s `ctx.auth` or the newer `authkitClientGuard`'s
|
|
17
|
-
* `ctx.authkit`), memoizing so a second call in the same request is free.
|
|
12
|
+
* Resolves the session via {@link getAuthkit}, which memoizes on `ctx.authkit`, so a second call
|
|
13
|
+
* in the same request is free.
|
|
18
14
|
*/
|
|
19
15
|
export async function isAuthkitAdmin(ctx, options = {}) {
|
|
20
16
|
const authkit = await getAuthkit(ctx);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import '../../container_bindings.js';
|
|
1
2
|
import type { HttpContext } from '@adonisjs/core/http';
|
|
2
3
|
import type { Authenticator } from '../authenticator.js';
|
|
3
4
|
/**
|
|
@@ -7,25 +8,42 @@ import type { Authenticator } from '../authenticator.js';
|
|
|
7
8
|
* É o ÚNICO caminho de resolução de sessão da lib: passa pelo
|
|
8
9
|
* `AuthkitClientManager`, então o refresh proativo do token (`maybeRefresh`), o
|
|
9
10
|
* `SessionResolver` configurado, as métricas e a ponte do `@agora/context`
|
|
10
|
-
* continuam valendo —
|
|
11
|
-
* para quem usa o `authkitClientGuard` de `@adonisjs/auth`.
|
|
11
|
+
* continuam valendo — com ou sem o `authkitClientGuard` de `@adonisjs/auth`.
|
|
12
12
|
*
|
|
13
13
|
* Memoiza em `ctx.authkit` de propósito: quando o host registra o
|
|
14
14
|
* `authkit_context_middleware` E o guard, os dois compartilham a MESMA
|
|
15
15
|
* instância, e a sessão é resolvida (e o token renovado) uma vez só.
|
|
16
16
|
*/
|
|
17
|
-
export declare function getAuthkit(ctx: HttpContext): Promise<Authenticator
|
|
17
|
+
export declare function getAuthkit(ctx: HttpContext): Promise<Authenticator<AuthkitUser>>;
|
|
18
|
+
/**
|
|
19
|
+
* Augment this interface to type the user that `ctx.authkit.getUser()` returns
|
|
20
|
+
* (whatever your `resolveUser` produces):
|
|
21
|
+
*
|
|
22
|
+
* ```ts
|
|
23
|
+
* // types/authkit.ts
|
|
24
|
+
* import type AppUser from '#models/app_user'
|
|
25
|
+
*
|
|
26
|
+
* declare module '@adonis-agora/authkit-client' {
|
|
27
|
+
* interface AuthkitUserType {
|
|
28
|
+
* user: AppUser
|
|
29
|
+
* }
|
|
30
|
+
* }
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export interface AuthkitUserType {
|
|
34
|
+
}
|
|
35
|
+
/** The user type the app declared on {@link AuthkitUserType}, or `unknown`. */
|
|
36
|
+
export type AuthkitUser = AuthkitUserType extends {
|
|
37
|
+
user: infer TUser;
|
|
38
|
+
} ? TUser : unknown;
|
|
18
39
|
declare module '@adonisjs/core/http' {
|
|
19
40
|
interface HttpContext {
|
|
20
41
|
/**
|
|
21
42
|
* Superfície específica do authkit (`getIdentity`, `hasGlobalRole`, a
|
|
22
|
-
* `Identity` crua). Populada pelo `authkit_context_middleware` e,
|
|
23
|
-
* caminho do guard, pelo próprio `authkitClientGuard` ao autenticar.
|
|
24
|
-
*
|
|
25
|
-
* Existe para que o `@adonisjs/auth` possa ser o dono de `ctx.auth` sem que
|
|
26
|
-
* o host perca o que só o authkit sabe. Quem NÃO usa o guard continua com o
|
|
27
|
-
* `Authenticator` do authkit direto em `ctx.auth` (ver `types.ts`).
|
|
43
|
+
* `Identity` crua, `getUser`). Populada pelo `authkit_context_middleware` e,
|
|
44
|
+
* no caminho do guard, pelo próprio `authkitClientGuard` ao autenticar.
|
|
45
|
+
* `ctx.auth` fica livre para o `@adonisjs/auth` (via `authkitClientGuard`).
|
|
28
46
|
*/
|
|
29
|
-
authkit: Authenticator
|
|
47
|
+
authkit: Authenticator<AuthkitUser>;
|
|
30
48
|
}
|
|
31
49
|
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// Side-effect import: carries the `authkit.client` container binding into the emitted
|
|
2
|
+
// `.d.ts`, so `containerResolver.make('authkit.client')` is typed in a consumer's program.
|
|
3
|
+
import '../../container_bindings.js';
|
|
1
4
|
/**
|
|
2
5
|
* Resolve (uma vez por request) o {@link Authenticator} do authkit e o memoiza
|
|
3
6
|
* em `ctx.authkit`.
|
|
@@ -5,8 +8,7 @@
|
|
|
5
8
|
* É o ÚNICO caminho de resolução de sessão da lib: passa pelo
|
|
6
9
|
* `AuthkitClientManager`, então o refresh proativo do token (`maybeRefresh`), o
|
|
7
10
|
* `SessionResolver` configurado, as métricas e a ponte do `@agora/context`
|
|
8
|
-
* continuam valendo —
|
|
9
|
-
* para quem usa o `authkitClientGuard` de `@adonisjs/auth`.
|
|
11
|
+
* continuam valendo — com ou sem o `authkitClientGuard` de `@adonisjs/auth`.
|
|
10
12
|
*
|
|
11
13
|
* Memoiza em `ctx.authkit` de propósito: quando o host registra o
|
|
12
14
|
* `authkit_context_middleware` E o guard, os dois compartilham a MESMA
|
|
@@ -18,7 +20,7 @@ export async function getAuthkit(ctx) {
|
|
|
18
20
|
return existing;
|
|
19
21
|
const manager = await ctx.containerResolver.make('authkit.client');
|
|
20
22
|
await manager.maybeRefresh(ctx);
|
|
21
|
-
const authenticator = await manager.createAuthenticator(ctx);
|
|
23
|
+
const authenticator = (await manager.createAuthenticator(ctx));
|
|
22
24
|
ctx.authkit = authenticator;
|
|
23
25
|
return authenticator;
|
|
24
26
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import '../../container_bindings.js';
|
|
1
2
|
import type { HttpContext } from '@adonisjs/core/http';
|
|
2
3
|
import type { NextFn } from '@adonisjs/core/types/http';
|
|
3
4
|
/**
|
|
@@ -18,10 +19,8 @@ import type { NextFn } from '@adonisjs/core/types/http';
|
|
|
18
19
|
* ])
|
|
19
20
|
* ```
|
|
20
21
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* `ctx.auth` como sempre. Registrar os dois não faz sentido (e resolveria a
|
|
24
|
-
* sessão duas vezes em objetos distintos).
|
|
22
|
+
* Sem `@adonisjs/auth`, é ele sozinho que dá a sessão ao app: `ctx.authkit`
|
|
23
|
+
* em qualquer rota, e `auth_middleware` para exigir login.
|
|
25
24
|
*/
|
|
26
25
|
export default class AuthkitContextMiddleware {
|
|
27
26
|
handle(ctx: HttpContext, next: NextFn): Promise<any>;
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// Side-effect import: carries the `authkit.client` container binding into the emitted
|
|
2
|
+
// `.d.ts` of the middleware an app registers in start/kernel.ts.
|
|
3
|
+
import '../../container_bindings.js';
|
|
1
4
|
import { getAuthkit } from '../host/request_authenticator.js';
|
|
2
5
|
/**
|
|
3
6
|
* Popula `ctx.authkit` (o `Authenticator` do authkit) e renova o token
|
|
@@ -17,10 +20,8 @@ import { getAuthkit } from '../host/request_authenticator.js';
|
|
|
17
20
|
* ])
|
|
18
21
|
* ```
|
|
19
22
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* `ctx.auth` como sempre. Registrar os dois não faz sentido (e resolveria a
|
|
23
|
-
* sessão duas vezes em objetos distintos).
|
|
23
|
+
* Sem `@adonisjs/auth`, é ele sozinho que dá a sessão ao app: `ctx.authkit`
|
|
24
|
+
* em qualquer rota, e `auth_middleware` para exigir login.
|
|
24
25
|
*/
|
|
25
26
|
export default class AuthkitContextMiddleware {
|
|
26
27
|
async handle(ctx, next) {
|
|
@@ -10,7 +10,7 @@ import type { NextFn } from '@adonisjs/core/types/http';
|
|
|
10
10
|
*
|
|
11
11
|
* No-op quando `backchannelLogout.store` não está configurado em `config/authkit_client.ts`.
|
|
12
12
|
*
|
|
13
|
-
* Ordem no kernel: DEPOIS do `
|
|
13
|
+
* Ordem no kernel: DEPOIS do `authkit_context_middleware` (depende do `authkit.client`
|
|
14
14
|
* resolvido), antes dos named middlewares de auth.
|
|
15
15
|
*/
|
|
16
16
|
export default class BackchannelRevocationMiddleware {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* No-op quando `backchannelLogout.store` não está configurado em `config/authkit_client.ts`.
|
|
10
10
|
*
|
|
11
|
-
* Ordem no kernel: DEPOIS do `
|
|
11
|
+
* Ordem no kernel: DEPOIS do `authkit_context_middleware` (depende do `authkit.client`
|
|
12
12
|
* resolvido), antes dos named middlewares de auth.
|
|
13
13
|
*/
|
|
14
14
|
export default class BackchannelRevocationMiddleware {
|
|
@@ -28,11 +28,12 @@ export interface RequireOrgMiddlewareOptions {
|
|
|
28
28
|
* pior lugar para se cair por acidente. Fail-closed: sem sessão ou sem org, não
|
|
29
29
|
* passa.
|
|
30
30
|
*
|
|
31
|
-
* Registre como named middleware e encadeie DEPOIS do
|
|
31
|
+
* Registre como named middleware e encadeie DEPOIS do `auth` (o
|
|
32
|
+
* `#middleware/auth_middleware` que o `node ace configure` publica):
|
|
32
33
|
*
|
|
33
34
|
* ```ts title="start/kernel.ts"
|
|
34
35
|
* export const middleware = router.named({
|
|
35
|
-
* auth: () => import('
|
|
36
|
+
* auth: () => import('#middleware/auth_middleware'),
|
|
36
37
|
* requireOrg: () => import('@adonis-agora/authkit-client/require_org_middleware'),
|
|
37
38
|
* })
|
|
38
39
|
* ```
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getAuthkit } from '../host/request_authenticator.js';
|
|
2
2
|
/**
|
|
3
3
|
* Exige que a sessão tenha uma **organização ativa** (claim `org_id`, exposta
|
|
4
4
|
* como `identity.orgId`).
|
|
@@ -10,11 +10,12 @@ import { ctxAuth } from '../host/ctx_auth.js';
|
|
|
10
10
|
* pior lugar para se cair por acidente. Fail-closed: sem sessão ou sem org, não
|
|
11
11
|
* passa.
|
|
12
12
|
*
|
|
13
|
-
* Registre como named middleware e encadeie DEPOIS do
|
|
13
|
+
* Registre como named middleware e encadeie DEPOIS do `auth` (o
|
|
14
|
+
* `#middleware/auth_middleware` que o `node ace configure` publica):
|
|
14
15
|
*
|
|
15
16
|
* ```ts title="start/kernel.ts"
|
|
16
17
|
* export const middleware = router.named({
|
|
17
|
-
* auth: () => import('
|
|
18
|
+
* auth: () => import('#middleware/auth_middleware'),
|
|
18
19
|
* requireOrg: () => import('@adonis-agora/authkit-client/require_org_middleware'),
|
|
19
20
|
* })
|
|
20
21
|
* ```
|
|
@@ -26,7 +27,7 @@ import { ctxAuth } from '../host/ctx_auth.js';
|
|
|
26
27
|
*/
|
|
27
28
|
export default class RequireOrgMiddleware {
|
|
28
29
|
async handle(ctx, next, options = {}) {
|
|
29
|
-
const identity = await
|
|
30
|
+
const identity = await (await getAuthkit(ctx)).getIdentity();
|
|
30
31
|
const orgId = identity?.orgId ?? null;
|
|
31
32
|
const allowed = orgId !== null && (!options.oneOf || options.oneOf.includes(orgId));
|
|
32
33
|
if (allowed)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Asymmetric algorithms accepted for DPoP proofs (RFC 9449 forbids symmetric ones). */
|
|
2
|
+
export declare const DPOP_ALGORITHMS: string[];
|
|
3
|
+
export interface VerifyDpopProofInput {
|
|
4
|
+
/** The `DPoP` request header. */
|
|
5
|
+
proof: string | string[] | undefined;
|
|
6
|
+
/** The request method. */
|
|
7
|
+
method: string;
|
|
8
|
+
/** The request URL; the query and fragment are ignored. */
|
|
9
|
+
url: string;
|
|
10
|
+
/** The access token sent with the proof, bound through `ath`. */
|
|
11
|
+
accessToken: string;
|
|
12
|
+
/** The `cnf.jkt` of the access token. */
|
|
13
|
+
expectedJkt: string;
|
|
14
|
+
/** Acceptance window for `iat`, in seconds. */
|
|
15
|
+
maxAge: number;
|
|
16
|
+
/** Seen `jti`s, to refuse a replayed proof. */
|
|
17
|
+
replay: DpopReplayCache;
|
|
18
|
+
now?: () => number;
|
|
19
|
+
}
|
|
20
|
+
/** Bounded in-memory set of `jti`s seen within the proof window. */
|
|
21
|
+
export declare class DpopReplayCache {
|
|
22
|
+
#private;
|
|
23
|
+
private readonly max;
|
|
24
|
+
constructor(max?: number);
|
|
25
|
+
/** `true` the first time a `jti` is seen before `expiresAt` (ms). */
|
|
26
|
+
remember(jti: string, expiresAt: number, now: number): boolean;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Verifies a DPoP proof sent to a resource server (RFC 9449 §4.3, §7): one
|
|
30
|
+
* `dpop+jwt` proof signed by the public key in its own header, matching the
|
|
31
|
+
* request method and URL, fresh, not replayed, bound to the access token
|
|
32
|
+
* (`ath`) and to the key the token is bound to (`cnf.jkt`).
|
|
33
|
+
*/
|
|
34
|
+
export declare function verifyDpopProof(input: VerifyDpopProofInput): Promise<boolean>;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { calculateJwkThumbprint, decodeProtectedHeader, EmbeddedJWK, jwtVerify, } from 'jose';
|
|
3
|
+
/** Asymmetric algorithms accepted for DPoP proofs (RFC 9449 forbids symmetric ones). */
|
|
4
|
+
export const DPOP_ALGORITHMS = ['ES256', 'ES384', 'ES512', 'EdDSA', 'PS256', 'RS256'];
|
|
5
|
+
function htu(value) {
|
|
6
|
+
try {
|
|
7
|
+
const url = new URL(value);
|
|
8
|
+
if (url.protocol !== 'https:' && url.protocol !== 'http:')
|
|
9
|
+
return null;
|
|
10
|
+
url.hash = '';
|
|
11
|
+
url.search = '';
|
|
12
|
+
return url.toString();
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/** Bounded in-memory set of `jti`s seen within the proof window. */
|
|
19
|
+
export class DpopReplayCache {
|
|
20
|
+
max;
|
|
21
|
+
#seen = new Map();
|
|
22
|
+
constructor(max = 10_000) {
|
|
23
|
+
this.max = max;
|
|
24
|
+
}
|
|
25
|
+
/** `true` the first time a `jti` is seen before `expiresAt` (ms). */
|
|
26
|
+
remember(jti, expiresAt, now) {
|
|
27
|
+
for (const [key, exp] of this.#seen) {
|
|
28
|
+
if (exp > now && this.#seen.size < this.max)
|
|
29
|
+
break;
|
|
30
|
+
this.#seen.delete(key);
|
|
31
|
+
}
|
|
32
|
+
const existing = this.#seen.get(jti);
|
|
33
|
+
if (existing !== undefined && existing > now)
|
|
34
|
+
return false;
|
|
35
|
+
this.#seen.set(jti, expiresAt);
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Verifies a DPoP proof sent to a resource server (RFC 9449 §4.3, §7): one
|
|
41
|
+
* `dpop+jwt` proof signed by the public key in its own header, matching the
|
|
42
|
+
* request method and URL, fresh, not replayed, bound to the access token
|
|
43
|
+
* (`ath`) and to the key the token is bound to (`cnf.jkt`).
|
|
44
|
+
*/
|
|
45
|
+
export async function verifyDpopProof(input) {
|
|
46
|
+
const raw = input.proof;
|
|
47
|
+
if (!raw || (Array.isArray(raw) ? raw.length !== 1 : raw.includes(',')))
|
|
48
|
+
return false;
|
|
49
|
+
const proof = Array.isArray(raw) ? raw[0] : raw;
|
|
50
|
+
let jwk;
|
|
51
|
+
try {
|
|
52
|
+
const header = decodeProtectedHeader(proof);
|
|
53
|
+
if (header.typ !== 'dpop+jwt' || !header.alg || !DPOP_ALGORITHMS.includes(header.alg)) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
jwk = header.jwk;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
if (!jwk || typeof jwk !== 'object' || 'd' in jwk || jwk.kty === 'oct')
|
|
62
|
+
return false;
|
|
63
|
+
const nowMs = (input.now ?? Date.now)();
|
|
64
|
+
let payload;
|
|
65
|
+
try {
|
|
66
|
+
({ payload } = await jwtVerify(proof, EmbeddedJWK, {
|
|
67
|
+
algorithms: DPOP_ALGORITHMS,
|
|
68
|
+
typ: 'dpop+jwt',
|
|
69
|
+
currentDate: new Date(nowMs),
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
const { jti, htm, htu: proofHtu, iat, ath } = payload;
|
|
76
|
+
if (typeof jti !== 'string' || jti.length < 8 || jti.length > 256)
|
|
77
|
+
return false;
|
|
78
|
+
if (typeof htm !== 'string' || htm !== input.method.toUpperCase())
|
|
79
|
+
return false;
|
|
80
|
+
const expectedHtu = htu(input.url);
|
|
81
|
+
if (typeof proofHtu !== 'string' || !expectedHtu || htu(proofHtu) !== expectedHtu)
|
|
82
|
+
return false;
|
|
83
|
+
const nowS = Math.floor(nowMs / 1000);
|
|
84
|
+
if (typeof iat !== 'number' || iat > nowS + 5 || iat < nowS - input.maxAge)
|
|
85
|
+
return false;
|
|
86
|
+
const tokenHash = createHash('sha256').update(input.accessToken).digest('base64url');
|
|
87
|
+
if (ath !== tokenHash)
|
|
88
|
+
return false;
|
|
89
|
+
if ((await calculateJwkThumbprint(jwk, 'sha256')) !== input.expectedJkt)
|
|
90
|
+
return false;
|
|
91
|
+
return input.replay.remember(jti, (iat + input.maxAge + 5) * 1000, nowMs);
|
|
92
|
+
}
|
|
@@ -2,6 +2,11 @@ import type { SessionResolver } from '@adonis-agora/authkit-core';
|
|
|
2
2
|
import { type OidcEndpoints } from '../discovery.js';
|
|
3
3
|
import { type TokenSource } from '../token_source.js';
|
|
4
4
|
export interface JwtResolverFactoryConfig {
|
|
5
|
+
/**
|
|
6
|
+
* `session` (default) validates the ID token of the OIDC session. `bearer` reads
|
|
7
|
+
* the `Authorization` header (API routes): `Bearer <jwt>`, or `DPoP <jwt>` with a
|
|
8
|
+
* `DPoP` proof for sender-constrained tokens (`cnf.jkt`).
|
|
9
|
+
*/
|
|
5
10
|
tokenSource?: TokenSource;
|
|
6
11
|
/** Explicit jwks_uri. Default: the discovered `jwks_uri` (`${issuer}/jwks` with discovery off). */
|
|
7
12
|
jwksUri?: string;
|
|
@@ -14,8 +14,17 @@ export interface JwtResolverConfig {
|
|
|
14
14
|
algorithms?: string[];
|
|
15
15
|
/** de onde extrair o token cru no request; injetado pelo factory */
|
|
16
16
|
getToken?: (ctx: HttpContext) => string | null;
|
|
17
|
+
/**
|
|
18
|
+
* Read the token from the `Authorization` header (API routes) instead of
|
|
19
|
+
* `getToken`. `Bearer <token>` and `DPoP <token>` (with its `DPoP` proof header)
|
|
20
|
+
* are accepted; a DPoP-bound token (`cnf.jkt`) sent as `Bearer` is refused.
|
|
21
|
+
*/
|
|
22
|
+
authorizationHeader?: boolean;
|
|
23
|
+
/** Acceptance window of DPoP proofs, in seconds. Default: 300. */
|
|
24
|
+
dpopMaxAge?: number;
|
|
17
25
|
}
|
|
18
26
|
export declare class JwtResolver implements SessionResolver {
|
|
27
|
+
#private;
|
|
19
28
|
private config;
|
|
20
29
|
constructor(config: JwtResolverConfig);
|
|
21
30
|
/** Valida um JWT cru e monta a Identity (ou null se inválido). */
|