@adonis-agora/authkit-client 0.19.1 → 0.20.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.
@@ -6,7 +6,7 @@ import { configProvider } from '@adonisjs/core';
6
6
  import { RuntimeException } from '@adonisjs/core/exceptions';
7
7
  import { decodeJwt } from 'jose';
8
8
  import { Authenticator } from '../src/authenticator.js';
9
- import { completeAuthorization, createAuthorizationRequest, } from '../src/authorization.js';
9
+ import { assertCallbackBelongs, completeAuthorization, createAuthorizationRequest, } from '../src/authorization.js';
10
10
  import { validateLogoutToken } from '../src/backchannel_logout.js';
11
11
  import { conventionEndpoints, discoverEndpoints, mergeEndpoints, } from '../src/discovery.js';
12
12
  import { validateIdToken, } from '../src/id_token.js';
@@ -111,6 +111,8 @@ export class AuthkitClientManager {
111
111
  * Throws `OidcCallbackError`; `error.restartable` means "send them back to login".
112
112
  */
113
113
  async completeAuthorization(params) {
114
+ // Before discovery: a stale or forged callback is restartable even while the IdP is down.
115
+ assertCallbackBelongs(params.pending, params.callbackParams);
114
116
  const { endpoints, keys } = await this.#idp();
115
117
  return completeAuthorization({
116
118
  ...params,
@@ -463,7 +465,7 @@ export default class AuthkitClientProvider {
463
465
  }
464
466
  register() {
465
467
  // Bound by class, like the first-party packages do, so `@inject()` works with
466
- // `AuthkitClientManager` as a constructor type. The string key stays as an alias.
468
+ // `AuthkitClientManager` as a constructor type.
467
469
  this.app.container.singleton(AuthkitClientManager, async (resolver) => {
468
470
  const value = this.app.config.get('authkit_client');
469
471
  const config = (await configProvider.resolve(this.app, value));
@@ -473,7 +475,12 @@ export default class AuthkitClientProvider {
473
475
  const logger = await resolver.make('logger');
474
476
  return new AuthkitClientManager(config, { logger });
475
477
  });
476
- this.app.container.alias('authkit.client', AuthkitClientManager);
478
+ // The string key resolves the class binding on every lookup — the same singleton — rather
479
+ // than being an `alias`: the container follows an alias before it checks bound values, so
480
+ // `bindValue('authkit.client', fake)`, how apps replaced the manager in tests before it was
481
+ // bound by class, would silently get the real one. Not cached here, so a `swap` of the class
482
+ // also reaches the string key, even after its first use.
483
+ this.app.container.bind('authkit.client', (resolver) => resolver.make(AuthkitClientManager));
477
484
  }
478
485
  }
479
486
  /** `sub` of a stored id_token, or undefined when there is none / it is unreadable. */
@@ -115,6 +115,13 @@ export interface CompletedAuthorization {
115
115
  /** Validated id_token claims. */
116
116
  claims: IdTokenClaims;
117
117
  }
118
+ /**
119
+ * The checks that say whether a callback belongs to this browser's pending authorization at all
120
+ * (`missing_pending`, `state_mismatch`). They need nothing from the IdP, so a caller that has to
121
+ * discover the IdP's endpoints first runs them before that: a stale or forged callback is
122
+ * restartable even while the IdP is unreachable, rather than surfacing as a discovery failure.
123
+ */
124
+ export declare function assertCallbackBelongs(pending: PendingAuthorization | null | undefined, callbackParams: Record<string, unknown>): asserts pending is PendingAuthorization;
118
125
  /**
119
126
  * Finishes the authorization-code flow: checks `state` and `iss`, exchanges the
120
127
  * code (with the PKCE verifier) and validates the id_token, `nonce` included.
@@ -71,6 +71,21 @@ export class OidcCallbackError extends Error {
71
71
  this.code === 'missing_code');
72
72
  }
73
73
  }
74
+ const str = (v) => (typeof v === 'string' && v ? v : undefined);
75
+ /**
76
+ * The checks that say whether a callback belongs to this browser's pending authorization at all
77
+ * (`missing_pending`, `state_mismatch`). They need nothing from the IdP, so a caller that has to
78
+ * discover the IdP's endpoints first runs them before that: a stale or forged callback is
79
+ * restartable even while the IdP is unreachable, rather than surfacing as a discovery failure.
80
+ */
81
+ export function assertCallbackBelongs(pending, callbackParams) {
82
+ if (!pending?.state || !pending.nonce || !pending.codeVerifier) {
83
+ throw new OidcCallbackError('missing_pending', 'No pending authorization for this callback');
84
+ }
85
+ if (str(callbackParams.state) !== pending.state) {
86
+ throw new OidcCallbackError('state_mismatch', 'Callback state does not match');
87
+ }
88
+ }
74
89
  /**
75
90
  * Finishes the authorization-code flow: checks `state` and `iss`, exchanges the
76
91
  * code (with the PKCE verifier) and validates the id_token, `nonce` included.
@@ -80,15 +95,9 @@ export class OidcCallbackError extends Error {
80
95
  */
81
96
  export async function completeAuthorization(p) {
82
97
  const q = p.callbackParams;
83
- const str = (v) => (typeof v === 'string' && v ? v : undefined);
84
- const { issuer, tokenEndpoint, jwksUri } = p.endpoints ?? conventionEndpoints(p.issuer);
85
98
  const pending = p.pending;
86
- if (!pending?.state || !pending.nonce || !pending.codeVerifier) {
87
- throw new OidcCallbackError('missing_pending', 'No pending authorization for this callback');
88
- }
89
- if (str(q.state) !== pending.state) {
90
- throw new OidcCallbackError('state_mismatch', 'Callback state does not match');
91
- }
99
+ assertCallbackBelongs(pending, q);
100
+ const { issuer, tokenEndpoint, jwksUri } = p.endpoints ?? conventionEndpoints(p.issuer);
92
101
  const iss = str(q.iss);
93
102
  if (iss !== undefined && iss !== issuer) {
94
103
  throw new OidcCallbackError('issuer_mismatch', `Callback iss ${iss} is not ${issuer}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adonis-agora/authkit-client",
3
- "version": "0.19.1",
3
+ "version": "0.20.0",
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",
@@ -60,6 +60,9 @@
60
60
  "@adonis-agora/authkit-client/commands"
61
61
  ]
62
62
  },
63
+ "engines": {
64
+ "node": ">=24"
65
+ },
63
66
  "peerDependencies": {
64
67
  "@adonis-agora/resilience": ">=0.1.0 <1.0.0",
65
68
  "@adonisjs/auth": "^10.1.0",
@@ -79,11 +82,11 @@
79
82
  }
80
83
  },
81
84
  "dependencies": {
82
- "@adonis-agora/authkit-core": "0.9.0",
85
+ "@adonis-agora/authkit-core": "0.10.0",
83
86
  "jose": "6.2.12"
84
87
  },
85
88
  "devDependencies": {
86
- "@adonis-agora/authkit-server": "0.80.2",
89
+ "@adonis-agora/authkit-server": "0.81.0",
87
90
  "@adonis-agora/resilience": "0.3.7",
88
91
  "@adonisjs/auth": "10.1.0",
89
92
  "@adonisjs/core": "7.5.0",