@adonis-agora/authkit-client 0.19.0 → 0.19.2

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.
@@ -16,4 +16,10 @@ export async function configure(command) {
16
16
  AUTHKIT_REDIRECT_URI: `Env.schema.string({ format: 'url', tld: false })`,
17
17
  },
18
18
  });
19
+ // As rotas não são registradas pelo configure. O caminho é o `registerOidcClient`
20
+ // (startSession/endSession, RP-initiated logout, back-channel); o controller
21
+ // ejetado acima existe só para quem precisa customizar o fluxo.
22
+ command.logger.info('Registre as rotas de sessão em start/routes.ts com registerOidcClient(router) ' +
23
+ "(import { registerOidcClient } from '@adonis-agora/authkit-client'). " +
24
+ 'O app/controllers/oidc_session_controller.ts publicado é só para customizar o fluxo.');
19
25
  }
@@ -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}`);
@@ -7,7 +7,7 @@ import { AuthkitClientManager, OidcCallbackError } from '@adonis-agora/authkit-c
7
7
 
8
8
  /**
9
9
  * Login, callback and logout written out by hand. If you do not need to customise
10
- * them, `registerOidcClient(router)` registers the same three routes for you.
10
+ * them, registerOidcClient(router) registers the same three routes for you.
11
11
  */
12
12
  @inject()
13
13
  export default class OidcSessionController {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adonis-agora/authkit-client",
3
- "version": "0.19.0",
3
+ "version": "0.19.2",
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",
@@ -83,7 +83,7 @@
83
83
  "jose": "6.2.12"
84
84
  },
85
85
  "devDependencies": {
86
- "@adonis-agora/authkit-server": "0.80.1",
86
+ "@adonis-agora/authkit-server": "0.80.2",
87
87
  "@adonis-agora/resilience": "0.3.7",
88
88
  "@adonisjs/auth": "10.1.0",
89
89
  "@adonisjs/core": "7.5.0",
@@ -30,8 +30,9 @@ sources:
30
30
  `@adonis-agora/authkit-client` makes an AdonisJS app consume an OIDC provider (the
31
31
  AuthKit IdP or any other) with session-based auth: PKCE authorization-code login,
32
32
  locally-validated ID tokens (JWKS), typed `ctx.auth` per request. `configure`
33
- publishes `config/authkit_client.ts`, `OidcSessionController` (login/callback/logout)
34
- and registers the provider + middleware.
33
+ publishes `config/authkit_client.ts` and registers the provider + middleware; the
34
+ login/callback/logout routes come from `registerOidcClient(router)`. (`configure` also
35
+ publishes an `OidcSessionController` stub, only for apps that must customise the flow.)
35
36
 
36
37
  ## Setup
37
38
 
@@ -58,13 +59,17 @@ export default authkitClientConfig
58
59
  ```ts
59
60
  // start/routes.ts
60
61
  import router from '@adonisjs/core/services/router'
61
- import OidcSessionController from '#controllers/oidc_session_controller'
62
+ import { registerOidcClient } from '@adonis-agora/authkit-client'
62
63
 
63
- router.get('/auth/login', [OidcSessionController, 'login'])
64
- router.get('/auth/callback', [OidcSessionController, 'callback'])
65
- router.post('/auth/logout', [OidcSessionController, 'logout'])
64
+ // /auth/login, /auth/callback, /auth/logout and /auth/backchannel-logout
65
+ registerOidcClient(router)
66
66
  ```
67
67
 
68
+ If you wire the scaffolded `OidcSessionController` instead, keep it on the manager:
69
+ `startSession(ctx, tokenSet)` in the callback and `endSession(ctx)` in the logout —
70
+ never a raw `session.put(sessionKey)` / `session.forget(sessionKey)`, which leaves a
71
+ parked impersonation credential alive.
72
+
68
73
  Source: `packages/authkit-client/README.md`, `stubs/config/authkit_client.stub`.
69
74
 
70
75
  ## Core patterns
@@ -7,7 +7,7 @@ import { AuthkitClientManager, OidcCallbackError } from '@adonis-agora/authkit-c
7
7
 
8
8
  /**
9
9
  * Login, callback and logout written out by hand. If you do not need to customise
10
- * them, `registerOidcClient(router)` registers the same three routes for you.
10
+ * them, registerOidcClient(router) registers the same three routes for you.
11
11
  */
12
12
  @inject()
13
13
  export default class OidcSessionController {