@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.
- package/build/commands/configure.js +6 -0
- package/build/providers/authkit_client_provider.js +10 -3
- package/build/src/authorization.d.ts +7 -0
- package/build/src/authorization.js +17 -8
- package/build/stubs/controllers/oidc_session_controller.stub +1 -1
- package/package.json +2 -2
- package/skills/authkit-rp-client/SKILL.md +11 -6
- package/stubs/controllers/oidc_session_controller.stub +1 -1
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
87
|
-
|
|
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,
|
|
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.
|
|
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.
|
|
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
|
|
34
|
-
|
|
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
|
|
62
|
+
import { registerOidcClient } from '@adonis-agora/authkit-client'
|
|
62
63
|
|
|
63
|
-
|
|
64
|
-
router
|
|
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,
|
|
10
|
+
* them, registerOidcClient(router) registers the same three routes for you.
|
|
11
11
|
*/
|
|
12
12
|
@inject()
|
|
13
13
|
export default class OidcSessionController {
|