@adonis-agora/authkit-client 0.17.4 → 0.18.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.
package/build/index.d.ts CHANGED
@@ -19,6 +19,8 @@ export { getAuthkit } from './src/host/request_authenticator.js';
19
19
  export { isAuthkitAdmin, authkitDashboardAuthorize, authkitDashboardMiddleware, } from './src/host/dashboard_guard.js';
20
20
  export type { DashboardGuardOptions } from './src/host/dashboard_guard.js';
21
21
  export { default as AuthkitContextMiddleware } from './src/middleware/authkit_context_middleware.js';
22
+ export { default as RequireOrgMiddleware } from './src/middleware/require_org_middleware.js';
23
+ export type { RequireOrgMiddlewareOptions } from './src/middleware/require_org_middleware.js';
22
24
  export { lucidMirror } from './src/lucid_mirror.js';
23
25
  export type { LucidMirrorOptions } from './src/lucid_mirror.js';
24
26
  export { registerOidcClient } from './src/register_oidc_client.js';
package/build/index.js CHANGED
@@ -11,6 +11,7 @@ export { authkitClientGuard, AuthkitClientGuard } from './src/host/authkit_clien
11
11
  export { getAuthkit } from './src/host/request_authenticator.js';
12
12
  export { isAuthkitAdmin, authkitDashboardAuthorize, authkitDashboardMiddleware, } from './src/host/dashboard_guard.js';
13
13
  export { default as AuthkitContextMiddleware } from './src/middleware/authkit_context_middleware.js';
14
+ export { default as RequireOrgMiddleware } from './src/middleware/require_org_middleware.js';
14
15
  export { lucidMirror } from './src/lucid_mirror.js';
15
16
  export { registerOidcClient } from './src/register_oidc_client.js';
16
17
  export { validateLogoutToken, InvalidLogoutTokenError, InMemorySessionIndex, BACKCHANNEL_LOGOUT_EVENT, } from './src/backchannel_logout.js';
@@ -0,0 +1,47 @@
1
+ import type { HttpContext } from '@adonisjs/core/http';
2
+ import type { NextFn } from '@adonisjs/core/types/http';
3
+ export interface RequireOrgMiddlewareOptions {
4
+ /**
5
+ * Para onde mandar quem não tem org ativa. Default: `/account/orgs` — a tela
6
+ * de seleção que o host-kit do authkit-server já serve.
7
+ */
8
+ redirectTo?: string;
9
+ /**
10
+ * `web` (default) redireciona para {@link redirectTo}; `api` responde 403 com
11
+ * um corpo JSON — rotas de API não têm para onde redirecionar.
12
+ */
13
+ mode?: 'web' | 'api';
14
+ /**
15
+ * Restringe às orgs listadas. Útil para rotas que só existem dentro de um
16
+ * conjunto conhecido de tenants. Ausente → qualquer org ativa serve.
17
+ */
18
+ oneOf?: string[];
19
+ }
20
+ /**
21
+ * Exige que a sessão tenha uma **organização ativa** (claim `org_id`, exposta
22
+ * como `identity.orgId`).
23
+ *
24
+ * Existe porque num app multi-tenant "estar autenticado" e "estar dentro de um
25
+ * tenant" são coisas diferentes: sem esta barreira, uma request de um usuário
26
+ * logado mas sem org ativa chega no controller com `tenantId` indefinido, e daí
27
+ * o escopo por tenant do `@adonis-agora/authz` cai no escopo GLOBAL — que é o
28
+ * pior lugar para se cair por acidente. Fail-closed: sem sessão ou sem org, não
29
+ * passa.
30
+ *
31
+ * Registre como named middleware e encadeie DEPOIS do de auth:
32
+ *
33
+ * ```ts title="start/kernel.ts"
34
+ * export const middleware = router.named({
35
+ * auth: () => import('@adonis-agora/authkit-client/auth_middleware'),
36
+ * requireOrg: () => import('@adonis-agora/authkit-client/require_org_middleware'),
37
+ * })
38
+ * ```
39
+ *
40
+ * ```ts title="start/routes.ts"
41
+ * router.group(() => { … }).use([middleware.auth(), middleware.requireOrg()])
42
+ * router.group(() => { … }).use([middleware.auth(), middleware.requireOrg({ mode: 'api' })])
43
+ * ```
44
+ */
45
+ export default class RequireOrgMiddleware {
46
+ handle(ctx: HttpContext, next: NextFn, options?: RequireOrgMiddlewareOptions): Promise<any>;
47
+ }
@@ -0,0 +1,39 @@
1
+ import { ctxAuth } from '../host/ctx_auth.js';
2
+ /**
3
+ * Exige que a sessão tenha uma **organização ativa** (claim `org_id`, exposta
4
+ * como `identity.orgId`).
5
+ *
6
+ * Existe porque num app multi-tenant "estar autenticado" e "estar dentro de um
7
+ * tenant" são coisas diferentes: sem esta barreira, uma request de um usuário
8
+ * logado mas sem org ativa chega no controller com `tenantId` indefinido, e daí
9
+ * o escopo por tenant do `@adonis-agora/authz` cai no escopo GLOBAL — que é o
10
+ * pior lugar para se cair por acidente. Fail-closed: sem sessão ou sem org, não
11
+ * passa.
12
+ *
13
+ * Registre como named middleware e encadeie DEPOIS do de auth:
14
+ *
15
+ * ```ts title="start/kernel.ts"
16
+ * export const middleware = router.named({
17
+ * auth: () => import('@adonis-agora/authkit-client/auth_middleware'),
18
+ * requireOrg: () => import('@adonis-agora/authkit-client/require_org_middleware'),
19
+ * })
20
+ * ```
21
+ *
22
+ * ```ts title="start/routes.ts"
23
+ * router.group(() => { … }).use([middleware.auth(), middleware.requireOrg()])
24
+ * router.group(() => { … }).use([middleware.auth(), middleware.requireOrg({ mode: 'api' })])
25
+ * ```
26
+ */
27
+ export default class RequireOrgMiddleware {
28
+ async handle(ctx, next, options = {}) {
29
+ const identity = await ctxAuth(ctx).getIdentity();
30
+ const orgId = identity?.orgId ?? null;
31
+ const allowed = orgId !== null && (!options.oneOf || options.oneOf.includes(orgId));
32
+ if (allowed)
33
+ return next();
34
+ if (options.mode === 'api') {
35
+ return ctx.response.abort({ error: 'no_active_organization', message: 'This route requires an active organization.' }, 403);
36
+ }
37
+ return ctx.response.redirect(options.redirectTo ?? '/account/orgs');
38
+ }
39
+ }
@@ -1,3 +1,4 @@
1
+ import { deriveOrgId } from '@adonis-agora/authkit-core';
1
2
  /**
2
3
  * Ponte estrutural (best-effort) entre a sessão resolvida do authkit e o
3
4
  * contexto do Agora. Lê o slot global `@agora/context:set` de forma ESTRUTURAL —
@@ -6,26 +7,14 @@
6
7
  */
7
8
  const SET_SLOT = Symbol.for('@agora/context:set');
8
9
  /**
9
- * Claims OIDC candidatas a "organização/tenant ativo". A primeira presente (e
10
- * string não vazia) vira o `tenantId`. Não falha se nenhuma existir.
10
+ * Deriva o tenant ativo da identidade. Prefere o `orgId` já resolvido (o caminho
11
+ * normal — `buildIdentityFromClaims` o preenche); cai para as claims cruas
12
+ * quando a `Identity` veio de um `SessionResolver` customizado que não o
13
+ * populou. A tabela de claims aceitas vive em `@adonis-agora/authkit-core`
14
+ * (`ORG_ID_CLAIMS`) para não divergir entre a identidade e esta ponte.
11
15
  */
12
- const TENANT_CLAIMS = [
13
- 'active_organization_id',
14
- 'org_id',
15
- 'organization_id',
16
- 'tenant_id',
17
- 'tid',
18
- ];
19
- /** Deriva o tenant ativo das claims cruas, ou `undefined` quando ausente. */
20
- function deriveTenant(raw) {
21
- if (!raw)
22
- return undefined;
23
- for (const claim of TENANT_CLAIMS) {
24
- const value = raw[claim];
25
- if (typeof value === 'string' && value.length > 0)
26
- return value;
27
- }
28
- return undefined;
16
+ function deriveTenant(identity) {
17
+ return identity.orgId ?? deriveOrgId(identity.raw) ?? undefined;
29
18
  }
30
19
  /**
31
20
  * Popula o contexto do Agora a partir de uma identidade recém-resolvida:
@@ -38,7 +27,7 @@ export function populateContext(identity) {
38
27
  const set = globalThis[SET_SLOT];
39
28
  if (!set)
40
29
  return;
41
- const tenant = deriveTenant(identity.raw);
30
+ const tenant = deriveTenant(identity);
42
31
  set({
43
32
  userRef: { type: 'user', id: identity.userId },
44
33
  globalRoles: identity.globalRoles,
@@ -8,7 +8,9 @@ type FetchImpl = (url: string, init: any) => Promise<{
8
8
  * resolvers `jwt`, `pat` e `opaque`. Espelha exatamente o que o `JwtResolver`
9
9
  * produzia (a referência canônica): `sub`→userId, `email`, papéis na
10
10
  * `globalRolesClaim`, `name`/`picture`→profile, `sid`→sessionId, `iat`/`exp`
11
- * (segundos→ms, fallback 0) e as claims cruas em `raw`.
11
+ * (segundos→ms, fallback 0), a org/tenant ativo (`orgId`/`orgSlug`/`orgRole`,
12
+ * ver `deriveOrgId` — vale para o authkit E para IdPs de terceiros) e as claims
13
+ * cruas em `raw`.
12
14
  */
13
15
  export declare function buildIdentityFromClaims(claims: Record<string, unknown>, globalRolesClaim: string): Identity;
14
16
  export interface IntrospectAuth {
@@ -1,9 +1,12 @@
1
+ import { deriveOrgId, deriveOrgRole, deriveOrgSlug } from '@adonis-agora/authkit-core';
1
2
  /**
2
3
  * Mapeamento canônico de claims OIDC validadas → Identity, compartilhado pelos
3
4
  * resolvers `jwt`, `pat` e `opaque`. Espelha exatamente o que o `JwtResolver`
4
5
  * produzia (a referência canônica): `sub`→userId, `email`, papéis na
5
6
  * `globalRolesClaim`, `name`/`picture`→profile, `sid`→sessionId, `iat`/`exp`
6
- * (segundos→ms, fallback 0) e as claims cruas em `raw`.
7
+ * (segundos→ms, fallback 0), a org/tenant ativo (`orgId`/`orgSlug`/`orgRole`,
8
+ * ver `deriveOrgId` — vale para o authkit E para IdPs de terceiros) e as claims
9
+ * cruas em `raw`.
7
10
  */
8
11
  export function buildIdentityFromClaims(claims, globalRolesClaim) {
9
12
  const roles = claims[globalRolesClaim];
@@ -16,6 +19,9 @@ export function buildIdentityFromClaims(claims, globalRolesClaim) {
16
19
  avatarUrl: typeof claims.picture === 'string' ? claims.picture : undefined,
17
20
  },
18
21
  sessionId: typeof claims.sid === 'string' ? claims.sid : undefined,
22
+ orgId: deriveOrgId(claims),
23
+ orgSlug: deriveOrgSlug(claims),
24
+ orgRole: deriveOrgRole(claims),
19
25
  issuedAt: typeof claims.iat === 'number' ? claims.iat * 1000 : 0,
20
26
  expiresAt: typeof claims.exp === 'number' ? claims.exp * 1000 : 0,
21
27
  raw: claims,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adonis-agora/authkit-client",
3
- "version": "0.17.4",
3
+ "version": "0.18.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",
@@ -21,7 +21,8 @@
21
21
  "authentication",
22
22
  "authkit",
23
23
  "relying-party",
24
- "jwt"
24
+ "jwt",
25
+ "tanstack-intent"
25
26
  ],
26
27
  "publishConfig": {
27
28
  "access": "public"
@@ -31,7 +32,8 @@
31
32
  "types": "./build/index.d.ts",
32
33
  "files": [
33
34
  "build",
34
- "stubs"
35
+ "stubs",
36
+ "skills"
35
37
  ],
36
38
  "exports": {
37
39
  ".": "./build/index.js",
@@ -45,6 +47,7 @@
45
47
  "./authkit_context_middleware": "./build/src/middleware/authkit_context_middleware.js",
46
48
  "./backchannel_revocation_middleware": "./build/src/middleware/backchannel_revocation_middleware.js",
47
49
  "./auth_middleware": "./build/src/middleware/auth_middleware.js",
50
+ "./require_org_middleware": "./build/src/middleware/require_org_middleware.js",
48
51
  "./silent_auth_middleware": "./build/src/middleware/silent_auth_middleware.js",
49
52
  "./commands": "./build/commands/configure.js"
50
53
  },
@@ -77,7 +80,7 @@
77
80
  },
78
81
  "dependencies": {
79
82
  "jose": "5.10.0",
80
- "@adonis-agora/authkit-core": "0.7.1"
83
+ "@adonis-agora/authkit-core": "0.8.0"
81
84
  },
82
85
  "devDependencies": {
83
86
  "@adonisjs/auth": "10.1.0",
@@ -89,10 +92,11 @@
89
92
  "@japa/assert": "4.2.0",
90
93
  "@japa/runner": "5.3.0",
91
94
  "@poppinss/ts-exec": "1.4.4",
95
+ "@tanstack/intent": "^0.3.2",
92
96
  "ioredis-mock": "8.13.1",
93
97
  "@types/node": "25.9.1",
94
98
  "typescript": "6.0.3",
95
- "@adonis-agora/authkit-server": "0.58.2"
99
+ "@adonis-agora/authkit-server": "0.61.0"
96
100
  },
97
101
  "scripts": {
98
102
  "build": "tsc && node -e \"require('node:fs').cpSync('stubs','build/stubs',{recursive:true,filter:(s)=>!s.endsWith('.ts')})\"",
@@ -0,0 +1,242 @@
1
+ ---
2
+ name: authkit-rp-client
3
+ description: >-
4
+ Integrate an AdonisJS app as an OpenID Connect relying party with
5
+ @adonis-agora/authkit-client. Covers defineConfig({ issuer, clientId, redirectUri,
6
+ resolver }), resolvers.jwt({ tokenSource }) / resolvers.pat / resolvers.opaque,
7
+ resolveUser topologies (identityToUser, createUserinfoResolver, local Lucid model),
8
+ the authkit_middleware + @adonisjs/session ordering rule, ctx.auth
9
+ (getIdentity/getUser/hasGlobalRole), registerOidcClient(router, { prefix, redirects,
10
+ afterLogin, loginMiddleware }), backchannelLogout: { store } +
11
+ BackchannelRevocationMiddleware, AUTHKIT_REDIRECT_URI, and why app-role gating
12
+ belongs to @adonis-agora/authz instead. Use when wiring a client app to the IdP,
13
+ fixing null identity, choosing a user-resolution topology, or adding back-channel
14
+ logout.
15
+ license: MIT
16
+ metadata:
17
+ type: core
18
+ library: "@adonis-agora/authkit-client"
19
+ library_version: "0.18.0"
20
+ framework: adonisjs
21
+ sources:
22
+ - "DavideCarvalho/adonis-authkit:packages/authkit-client/README.md"
23
+ - "DavideCarvalho/adonis-authkit:packages/authkit-client/src/define_config.ts"
24
+ - "DavideCarvalho/adonis-authkit:packages/authkit-client/src/resolvers/factory.ts"
25
+ - "DavideCarvalho/adonis-authkit:packages/authkit-client/src/register_oidc_client.ts"
26
+ ---
27
+
28
+ # OIDC relying-party (client) integration
29
+
30
+ `@adonis-agora/authkit-client` makes an AdonisJS app consume an OIDC provider (the
31
+ AuthKit IdP or any other) with session-based auth: PKCE authorization-code login,
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.
35
+
36
+ ## Setup
37
+
38
+ ```bash
39
+ node ace add @adonis-agora/authkit-client
40
+ ```
41
+
42
+ ```ts
43
+ // config/authkit_client.ts
44
+ import env from '#start/env'
45
+ import { defineConfig, resolvers } from '@adonis-agora/authkit-client'
46
+
47
+ const authkitClientConfig = defineConfig({
48
+ issuer: env.get('AUTHKIT_ISSUER'),
49
+ clientId: env.get('AUTHKIT_CLIENT_ID'),
50
+ clientSecret: env.get('AUTHKIT_CLIENT_SECRET'),
51
+ redirectUri: env.get('AUTHKIT_REDIRECT_URI'), // must point at the callback route
52
+ resolver: resolvers.jwt({ tokenSource: 'session' }),
53
+ })
54
+
55
+ export default authkitClientConfig
56
+ ```
57
+
58
+ ```ts
59
+ // start/routes.ts
60
+ import router from '@adonisjs/core/services/router'
61
+ import OidcSessionController from '#controllers/oidc_session_controller'
62
+
63
+ router.get('/auth/login', [OidcSessionController, 'login'])
64
+ router.get('/auth/callback', [OidcSessionController, 'callback'])
65
+ router.post('/auth/logout', [OidcSessionController, 'logout'])
66
+ ```
67
+
68
+ Source: `packages/authkit-client/README.md`, `stubs/config/authkit_client.stub`.
69
+
70
+ ## Core patterns
71
+
72
+ ### Pattern 1 — `ctx.auth` per request
73
+
74
+ The `authkit_middleware` populates `ctx.auth`; identity comes from the validated
75
+ token, the user from your `resolveUser`:
76
+
77
+ ```ts
78
+ router.get('/me', async ({ auth }) => {
79
+ const identity = await auth.getIdentity() // validated OIDC claims, or null
80
+ const user = await auth.getUser() // app model via resolveUser
81
+ const isAdmin = auth.hasGlobalRole('ADMIN') // sync — reads token claims
82
+ return { identity, user, isAdmin }
83
+ })
84
+ ```
85
+
86
+ `getIdentity()` resolves once per request and memoizes; `authenticate()` throws when
87
+ anonymous; `check()` returns a boolean. `identity` (getter) is only populated after
88
+ the first async call — prefer `getIdentity()`.
89
+
90
+ Source: `packages/authkit-client/README.md` § Uso por request,
91
+ `src/authenticator.ts` (`Authenticator`).
92
+
93
+ ### Pattern 2 — `resolveUser` per database topology
94
+
95
+ Identity always comes from the token; what varies is how the app model is fetched:
96
+
97
+ ```ts
98
+ // Same DB, cross-schema FK: map to a local Lucid model
99
+ resolveUser: async (identity) => {
100
+ return AppUser.query().where('authUserId', identity.userId).firstOrFail()
101
+ }
102
+
103
+ // Separate DBs: the IdP emits a fat ID token — claims-only, zero lookups
104
+ import { identityToUser } from '@adonis-agora/authkit-client'
105
+ resolveUser: identityToUser // { id, email, name?, avatarUrl?, globalRoles }
106
+
107
+ // Separate DBs + data beyond the claims: userinfo resolver against ${issuer}/me
108
+ import { createUserinfoResolver } from '@adonis-agora/authkit-client'
109
+ resolveUser: createUserinfoResolver({ issuer: env.get('AUTHKIT_ISSUER') })
110
+ ```
111
+
112
+ Source: `packages/authkit-client/README.md` § Topologias de banco,
113
+ `src/user_resolvers.ts`.
114
+
115
+ ### Pattern 3 — one-liner routes with `registerOidcClient`
116
+
117
+ Instead of hand-writing the controller trio, register PKCE + state, code exchange,
118
+ RP-initiated logout and role-based redirects in one call:
119
+
120
+ ```ts
121
+ import router from '@adonisjs/core/services/router'
122
+ import middleware from '#start/kernel'
123
+ import { registerOidcClient } from '@adonis-agora/authkit-client'
124
+
125
+ registerOidcClient(router, {
126
+ loginMiddleware: middleware.guest(),
127
+ redirects: { byGlobalRole: { ADMIN: '/admin' }, default: '/' },
128
+ // prefix defaults to /auth → /auth/login, /auth/callback, /auth/logout
129
+ // backchannelLogout defaults to true → also registers POST /auth/backchannel-logout
130
+ })
131
+ ```
132
+
133
+ Source: `src/register_oidc_client.ts` (`RegisterOidcClientOptions` + doc example).
134
+
135
+ ### Pattern 4 — back-channel logout for cookie sessions
136
+
137
+ Pass a store and the kit derives the callback + enforcement middleware:
138
+
139
+ ```ts
140
+ import { defineConfig, resolvers, lucidRevocationStore } from '@adonis-agora/authkit-client'
141
+
142
+ defineConfig({
143
+ issuer: env.get('AUTHKIT_ISSUER'),
144
+ clientId: env.get('AUTHKIT_CLIENT_ID'),
145
+ redirectUri: env.get('AUTHKIT_REDIRECT_URI'),
146
+ resolver: resolvers.jwt(),
147
+ backchannelLogout: { store: lucidRevocationStore() },
148
+ })
149
+ ```
150
+
151
+ Then apply `BackchannelRevocationMiddleware` (from
152
+ `@adonis-agora/authkit-client/backchannel_revocation_middleware`) so revoked
153
+ sessions are rejected per request. Custom needs: `onBackchannelLogout` receives the
154
+ validated `{ sid, sub }` (store runs first, then the hook).
155
+
156
+ Source: `src/define_config.ts` (`BackchannelLogoutInput`), README.
157
+
158
+ ## Common mistakes
159
+
160
+ ### HIGH — Session middleware running after `authkit_middleware`
161
+
162
+ ```ts
163
+ // Wrong — resolver reads an empty session; every request looks anonymous
164
+ router.use([middleware.authkit(), middleware.session()])
165
+ ```
166
+
167
+ ```ts
168
+ // Correct — @adonisjs/session MUST run BEFORE the authkit middleware
169
+ router.use([middleware.session(), middleware.authkit()])
170
+ ```
171
+
172
+ With `resolvers.jwt({ tokenSource: 'session' })` (the default) the resolver reads
173
+ the token set from the `@adonisjs/session` store; if session middleware has not run
174
+ yet the token is never found and `getIdentity()` silently returns `null` — no error,
175
+ just a logged-out app.
176
+
177
+ Source: `packages/authkit-client/README.md` § Ordem de middleware (importante).
178
+
179
+ ### HIGH — Putting app-role authorization in the client kit
180
+
181
+ ```ts
182
+ // Wrong — expecting authkit to resolve app roles/permissions
183
+ if (auth.hasGlobalRole('ADMIN')) { /* app-role check? */ }
184
+ const can = await authkit.checkPermission(user, 'posts.edit') // does not exist
185
+ ```
186
+
187
+ ```ts
188
+ // Correct — AuthKit authenticates + exposes token globalRoles; authz decides access
189
+ if (auth.hasGlobalRole('ADMIN')) { /* global role from the token claims */ }
190
+ // App roles/permissions: configure resolveRoles in @adonis-agora/authz
191
+ // and enforce with authz/Bouncer route guards, not authkit_middleware.
192
+ ```
193
+
194
+ AuthKit only authenticates and knows the `globalRoles` claim; app roles (domain
195
+ tables) and permissions were deliberately moved out to `@adonis-agora/authz`.
196
+ The `authkit_middleware` only enforces login.
197
+
198
+ Source: `packages/authkit-client/README.md` callout ("Autorização por app-role saiu
199
+ do AuthKit"), `stubs/config/authkit_client.stub` comment.
200
+
201
+ ### MEDIUM — `resolvers.opaque` without a confidential client
202
+
203
+ ```ts
204
+ // Wrong — public client: throws at resolver creation
205
+ resolver: resolvers.opaque({ introspectionUrl: '...' })
206
+ ```
207
+
208
+ ```ts
209
+ // Correct — opaque introspection requires the client secret
210
+ resolver: resolvers.opaque({
211
+ tokenSource: 'bearer',
212
+ cacheTtlMs: 30_000, // trade immediacy for fewer round-trips
213
+ })
214
+ ```
215
+
216
+ The opaque resolver introspects with `clientId`/`clientSecret` and the factory
217
+ throws `resolvers.opaque requer um client confidencial (clientSecret)` when the
218
+ config lacks a secret — a public client cannot authenticate introspection.
219
+
220
+ Source: `src/resolvers/factory.ts` (`resolvers.opaque` throw), README.
221
+
222
+ ### MEDIUM — Bearer resolver in a browser session app (or vice versa)
223
+
224
+ ```ts
225
+ // Wrong — SPA/API app still using the default session source
226
+ resolver: resolvers.jwt() // tokenSource defaults to 'session'
227
+ ```
228
+
229
+ ```ts
230
+ // Correct — pick the source matching the transport
231
+ resolver: resolvers.jwt({ tokenSource: 'bearer' }) // reads Authorization header
232
+ ```
233
+
234
+ `tokenSource` decides where the ID token is read from: `'session'` (default, cookie
235
+ apps) vs `'bearer'` (`Authorization` header for SPA/API callers). The mismatched
236
+ source yields a silent `null` identity, not an error.
237
+
238
+ Source: `packages/authkit-client/README.md` § Resolução de sessão,
239
+ `src/resolvers/factory.ts` (`JwtResolverFactoryConfig`).
240
+
241
+ See also: `authkit-idp-setup/SKILL.md` — the IdP side that issues these tokens, and
242
+ `authkit-react-auth/SKILL.md` — how the identity reaches the frontend.