@adonis-agora/authkit-client 0.20.5 → 1.0.0-rc.1

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.
Files changed (52) hide show
  1. package/CHANGELOG.md +963 -0
  2. package/README.md +47 -12
  3. package/build/auth.d.ts +2 -0
  4. package/build/auth.js +1 -0
  5. package/build/authkit_context_middleware.d.ts +1 -0
  6. package/build/authkit_context_middleware.js +2 -0
  7. package/build/commands/configure.d.ts +10 -0
  8. package/build/commands/configure.js +57 -2
  9. package/build/index.d.ts +5 -4
  10. package/build/index.js +2 -2
  11. package/build/providers/authkit_client_provider.d.ts +8 -1
  12. package/build/providers/authkit_client_provider.js +28 -12
  13. package/build/src/authenticator.d.ts +4 -3
  14. package/build/src/authenticator.js +6 -3
  15. package/build/src/host/authkit_client_guard.d.ts +51 -36
  16. package/build/src/host/authkit_client_guard.js +97 -46
  17. package/build/src/host/dashboard_guard.d.ts +24 -31
  18. package/build/src/host/dashboard_guard.js +16 -40
  19. package/build/src/host/request_authenticator.d.ts +28 -10
  20. package/build/src/host/request_authenticator.js +5 -3
  21. package/build/src/middleware/authkit_context_middleware.d.ts +3 -4
  22. package/build/src/middleware/authkit_context_middleware.js +5 -4
  23. package/build/src/middleware/backchannel_revocation_middleware.d.ts +1 -1
  24. package/build/src/middleware/backchannel_revocation_middleware.js +1 -1
  25. package/build/src/middleware/require_org_middleware.d.ts +3 -2
  26. package/build/src/middleware/require_org_middleware.js +5 -4
  27. package/build/src/resolvers/dpop_proof.d.ts +34 -0
  28. package/build/src/resolvers/dpop_proof.js +92 -0
  29. package/build/src/resolvers/factory.d.ts +5 -0
  30. package/build/src/resolvers/factory.js +1 -0
  31. package/build/src/resolvers/jwt_resolver.d.ts +9 -0
  32. package/build/src/resolvers/jwt_resolver.js +47 -3
  33. package/build/stubs/config/auth.stub +30 -0
  34. package/build/stubs/config/authkit_client.stub +2 -1
  35. package/build/stubs/middleware/auth_middleware.stub +25 -0
  36. package/build/stubs/middleware/silent_auth_middleware.stub +16 -0
  37. package/build/types.d.ts +5 -32
  38. package/build/types.js +1 -1
  39. package/package.json +11 -16
  40. package/skills/authkit-rp-client/SKILL.md +102 -47
  41. package/stubs/config/auth.stub +30 -0
  42. package/stubs/config/authkit_client.stub +2 -1
  43. package/stubs/middleware/auth_middleware.stub +25 -0
  44. package/stubs/middleware/silent_auth_middleware.stub +16 -0
  45. package/build/src/host/ctx_auth.d.ts +0 -6
  46. package/build/src/host/ctx_auth.js +0 -8
  47. package/build/src/middleware/auth_middleware.d.ts +0 -13
  48. package/build/src/middleware/auth_middleware.js +0 -13
  49. package/build/src/middleware/authkit_middleware.d.ts +0 -6
  50. package/build/src/middleware/authkit_middleware.js +0 -24
  51. package/build/src/middleware/silent_auth_middleware.d.ts +0 -13
  52. package/build/src/middleware/silent_auth_middleware.js +0 -15
@@ -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;
@@ -34,6 +34,7 @@ export const resolvers = {
34
34
  audience: rc.clientId,
35
35
  globalRolesClaim: rc.globalRolesClaim,
36
36
  getToken: (httpCtx) => getTokenFromSource(httpCtx, tokenSource, rc.sessionKey),
37
+ authorizationHeader: tokenSource === 'bearer',
37
38
  });
38
39
  },
39
40
  };
@@ -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). */
@@ -1,13 +1,15 @@
1
1
  import { jwtVerify } from 'jose';
2
2
  import { DEFAULT_ALGS, getJwks } from '../jwks.js';
3
+ import { DpopReplayCache, verifyDpopProof } from './dpop_proof.js';
3
4
  import { buildIdentityFromClaims } from './identity.js';
4
5
  export class JwtResolver {
5
6
  config;
7
+ #replay = new DpopReplayCache();
6
8
  constructor(config) {
7
9
  this.config = config;
8
10
  }
9
- /** Valida um JWT cru e monta a Identity (ou null se inválido). */
10
- async resolveToken(token) {
11
+ /** The verified claims of a raw JWT, or `null`. */
12
+ async #verify(token) {
11
13
  const endpoints = await this.config.endpoints();
12
14
  if (!endpoints)
13
15
  return null;
@@ -21,16 +23,58 @@ export class JwtResolver {
21
23
  // uma identidade válida exige um subject (`sub`)
22
24
  if (!payload.sub)
23
25
  return null;
24
- return buildIdentityFromClaims(payload, this.config.globalRolesClaim);
26
+ return payload;
25
27
  }
26
28
  catch {
27
29
  return null;
28
30
  }
29
31
  }
32
+ #identity(claims) {
33
+ return claims ? buildIdentityFromClaims(claims, this.config.globalRolesClaim) : null;
34
+ }
35
+ /** Valida um JWT cru e monta a Identity (ou null se inválido). */
36
+ async resolveToken(token) {
37
+ return this.#identity(await this.#verify(token));
38
+ }
30
39
  async resolve(ctx) {
40
+ if (this.config.authorizationHeader)
41
+ return this.#resolveAuthorization(ctx);
31
42
  const token = this.config.getToken?.(ctx) ?? null;
32
43
  if (!token)
33
44
  return null;
34
45
  return this.resolveToken(token);
35
46
  }
47
+ async #resolveAuthorization(ctx) {
48
+ const header = ctx.request.header('authorization');
49
+ if (!header)
50
+ return null;
51
+ const [scheme, token, ...rest] = header.trim().split(/\s+/);
52
+ if (!token || rest.length > 0)
53
+ return null;
54
+ const claims = await this.#verify(token);
55
+ if (!claims)
56
+ return null;
57
+ const jkt = claims.cnf?.jkt;
58
+ switch (scheme?.toLowerCase()) {
59
+ case 'bearer':
60
+ // RFC 9449 §7.1: a DPoP-bound token is not a bearer token.
61
+ return typeof jkt === 'string' ? null : this.#identity(claims);
62
+ case 'dpop': {
63
+ if (typeof jkt !== 'string')
64
+ return null;
65
+ const valid = await verifyDpopProof({
66
+ proof: ctx.request.header('dpop'),
67
+ method: ctx.request.method(),
68
+ url: ctx.request.completeUrl(false),
69
+ accessToken: token,
70
+ expectedJkt: jkt,
71
+ maxAge: this.config.dpopMaxAge ?? 300,
72
+ replay: this.#replay,
73
+ });
74
+ return valid ? this.#identity(claims) : null;
75
+ }
76
+ default:
77
+ return null;
78
+ }
79
+ }
36
80
  }
@@ -0,0 +1,30 @@
1
+ {{{
2
+ exports({ to: app.configPath('auth.ts') })
3
+ }}}
4
+ import { defineConfig } from '@adonisjs/auth'
5
+ import { sessionUserProvider } from '@adonisjs/auth/session'
6
+ import type { InferAuthenticators } from '@adonisjs/auth/types'
7
+ import { authkitClientGuard } from '@adonis-agora/authkit-client/auth'
8
+
9
+ /**
10
+ * ctx.auth is backed by the AuthKit session. The guard takes the user from
11
+ * resolveUser in config/authkit_client.ts when you set one (e.g. lucidMirror,
12
+ * which creates the local row on first login), and falls back to this provider
13
+ * otherwise. The model's primary key must be the IdP's sub.
14
+ */
15
+ const authConfig = defineConfig({
16
+ default: 'web',
17
+ guards: {
18
+ web: authkitClientGuard({
19
+ provider: sessionUserProvider({
20
+ model: () => import('{{ modelImport }}'),
21
+ }),
22
+ }),
23
+ },
24
+ })
25
+
26
+ export default authConfig
27
+
28
+ declare module '@adonisjs/auth/types' {
29
+ export interface Authenticators extends InferAuthenticators<typeof authConfig> {}
30
+ }
@@ -10,7 +10,8 @@ const authkitClientConfig = defineConfig({
10
10
  clientSecret: env.get('AUTHKIT_CLIENT_SECRET'),
11
11
  redirectUri: env.get('AUTHKIT_REDIRECT_URI'),
12
12
  resolver: resolvers.jwt({ tokenSource: 'session' }),
13
- // resolveUser: async (identity) => User.find(identity.userId),
13
+ // Creates/updates the local user on each login; ctx.auth.user is this user.
14
+ // resolveUser: lucidMirror(User, { sync: (identity) => ({ email: identity.email }) }),
14
15
  // Roles do app? Configure resolveRoles no @adonis-agora/authz (não aqui — o AuthKit só autentica).
15
16
  })
16
17
 
@@ -0,0 +1,25 @@
1
+ {{{
2
+ exports({ to: app.middlewarePath('auth_middleware.ts') })
3
+ }}}
4
+ import type { HttpContext } from '@adonisjs/core/http'
5
+ import type { NextFn } from '@adonisjs/core/types/http'
6
+ import type { Authenticators } from '@adonisjs/auth/types'
7
+
8
+ /**
9
+ * Denies access to unauthenticated requests and sends them to the AuthKit
10
+ * login route registered by registerOidcClient(router).
11
+ */
12
+ export default class AuthMiddleware {
13
+ redirectTo = '/auth/login'
14
+
15
+ async handle(
16
+ ctx: HttpContext,
17
+ next: NextFn,
18
+ options: {
19
+ guards?: (keyof Authenticators)[]
20
+ } = {}
21
+ ) {
22
+ await ctx.auth.authenticateUsing(options.guards, { loginRoute: this.redirectTo })
23
+ return next()
24
+ }
25
+ }
@@ -0,0 +1,16 @@
1
+ {{{
2
+ exports({ to: app.middlewarePath('silent_auth_middleware.ts') })
3
+ }}}
4
+ import type { HttpContext } from '@adonisjs/core/http'
5
+ import type { NextFn } from '@adonisjs/core/types/http'
6
+
7
+ /**
8
+ * Checks the AuthKit session without requiring one, so ctx.auth.user is set
9
+ * on routes that also serve guests.
10
+ */
11
+ export default class SilentAuthMiddleware {
12
+ async handle(ctx: HttpContext, next: NextFn) {
13
+ await ctx.auth.check()
14
+ return next()
15
+ }
16
+ }
package/build/types.d.ts CHANGED
@@ -1,43 +1,16 @@
1
- import './container_bindings.js';
1
+ import './src/host/request_authenticator.js';
2
2
  import type { AuthkitClientManager } from './providers/authkit_client_provider.js';
3
3
  import type { Authenticator } from './src/authenticator.js';
4
4
  /**
5
- * OPT-IN typing for `ctx.auth`.
5
+ * Types of `@adonis-agora/authkit-client`.
6
6
  *
7
- * Reference this module from your app when you want `ctx.auth` typed as
8
- * AuthKit's `Authenticator` and you are NOT declaring the property yourself:
7
+ * Importing this module brings the container bindings (`authkit.client`) and the
8
+ * `ctx.authkit` declaration into your program. `ctx.auth` belongs to
9
+ * `@adonisjs/auth`; use `authkitClientGuard` to back it with the AuthKit session.
9
10
  *
10
11
  * ```ts
11
12
  * // any file in your app, e.g. app/types.ts
12
13
  * import '@adonis-agora/authkit-client/types'
13
14
  * ```
14
- *
15
- * NOTHING in this package imports this module on your behalf, and that is the
16
- * point. `Authenticator` is generic (`Authenticator<TUser>`), and most apps
17
- * declare their own augmentation so `auth.getUser()` returns their user model:
18
- *
19
- * ```ts
20
- * declare module '@adonisjs/core/http' {
21
- * interface HttpContext {
22
- * auth: Authenticator<AppUser>
23
- * }
24
- * }
25
- * ```
26
- *
27
- * Two declarations of the same property with different types do not merge —
28
- * they conflict, and because `skipLibCheck` is on in every AdonisJS app the
29
- * conflict is never reported: one simply wins by include order. When the
30
- * library's won, `auth.getUser()` collapsed to `unknown` across the whole app.
31
- * So the library ships the declaration and lets you decide whether to load it,
32
- * rather than forcing it through a middleware import.
33
- *
34
- * The container bindings come along with it (see `container_bindings.ts`); they
35
- * are also available on their own, which is what the provider and the middleware
36
- * use.
37
15
  */
38
- declare module '@adonisjs/core/http' {
39
- interface HttpContext {
40
- auth: Authenticator;
41
- }
42
- }
43
16
  export type { Authenticator, AuthkitClientManager };
package/build/types.js CHANGED
@@ -1 +1 @@
1
- import './container_bindings.js';
1
+ import './src/host/request_authenticator.js';
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@adonis-agora/authkit-client",
3
- "version": "0.20.5",
3
+ "version": "1.0.0-rc.1",
4
4
  "description": "AdonisJS OIDC/OAuth2 relying-party (client) toolkit: session-based authentication against an OpenID Connect identity provider, with JWT/PAT user resolvers and OpenTelemetry metrics.",
5
5
  "license": "MIT",
6
6
  "author": "dudousxd",
7
7
  "repository": {
8
8
  "type": "git",
9
- "url": "https://github.com/DavideCarvalho/adonis-agora-authkit.git",
9
+ "url": "git+https://github.com/DavideCarvalho/adonis-agora-authkit.git",
10
10
  "directory": "packages/authkit-client"
11
11
  },
12
12
  "homepage": "https://davidecarvalho.github.io/agora/docs/authkit",
@@ -33,7 +33,8 @@
33
33
  "files": [
34
34
  "build",
35
35
  "stubs",
36
- "skills"
36
+ "skills",
37
+ "CHANGELOG.md"
37
38
  ],
38
39
  "exports": {
39
40
  ".": "./build/index.js",
@@ -41,14 +42,11 @@
41
42
  "./container_bindings": "./build/container_bindings.js",
42
43
  "./services/main": "./build/services/main.js",
43
44
  "./authkit_client_provider": "./build/providers/authkit_client_provider.js",
44
- "./auth": "./build/src/host/authkit_client_guard.js",
45
+ "./auth": "./build/auth.js",
45
46
  "./dashboard_guard": "./build/src/host/dashboard_guard.js",
46
- "./authkit_middleware": "./build/src/middleware/authkit_middleware.js",
47
- "./authkit_context_middleware": "./build/src/middleware/authkit_context_middleware.js",
47
+ "./authkit_context_middleware": "./build/authkit_context_middleware.js",
48
48
  "./backchannel_revocation_middleware": "./build/src/middleware/backchannel_revocation_middleware.js",
49
- "./auth_middleware": "./build/src/middleware/auth_middleware.js",
50
49
  "./require_org_middleware": "./build/src/middleware/require_org_middleware.js",
51
- "./silent_auth_middleware": "./build/src/middleware/silent_auth_middleware.js",
52
50
  "./commands": "./build/commands/configure.js"
53
51
  },
54
52
  "adonisjs": {
@@ -64,16 +62,13 @@
64
62
  "node": ">=24"
65
63
  },
66
64
  "peerDependencies": {
67
- "@adonis-agora/resilience": ">=0.1.0 <1.0.0",
65
+ "@adonis-agora/resilience": ">=0.1.0 <2.0.0 || >=1.0.0-rc.0 <2.0.0",
68
66
  "@adonisjs/auth": "^10.1.0",
69
67
  "@adonisjs/core": "^7.5.0",
70
68
  "@adonisjs/lucid": "^22.4.2",
71
69
  "@adonisjs/session": "^8.1.0"
72
70
  },
73
71
  "peerDependenciesMeta": {
74
- "@adonisjs/auth": {
75
- "optional": true
76
- },
77
72
  "@adonisjs/lucid": {
78
73
  "optional": true
79
74
  },
@@ -82,11 +77,11 @@
82
77
  }
83
78
  },
84
79
  "dependencies": {
85
- "@adonis-agora/authkit-core": "0.10.1",
80
+ "@adonis-agora/authkit-core": "1.0.0-rc.0",
86
81
  "jose": "6.2.15"
87
82
  },
88
83
  "devDependencies": {
89
- "@adonis-agora/authkit-server": "0.90.1",
84
+ "@adonis-agora/authkit-server": "1.0.0-rc.1",
90
85
  "@adonis-agora/resilience": "0.4.3",
91
86
  "@adonisjs/auth": "10.1.0",
92
87
  "@adonisjs/core": "7.6.0",
@@ -95,9 +90,9 @@
95
90
  "@adonisjs/tsconfig": "2.0.0",
96
91
  "@japa/assert": "4.2.0",
97
92
  "@japa/runner": "5.4.0",
98
- "@poppinss/ts-exec": "1.5.1",
93
+ "@poppinss/ts-exec": "1.5.2",
99
94
  "@tanstack/intent": "0.5.6",
100
- "@types/node": "26.6.5",
95
+ "@types/node": "26.6.6",
101
96
  "ioredis-mock": "8.13.1",
102
97
  "tempura": "0.4.1",
103
98
  "testcontainers": "12.2.1",