@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 +2 -0
- package/build/index.js +1 -0
- package/build/src/middleware/require_org_middleware.d.ts +47 -0
- package/build/src/middleware/require_org_middleware.js +39 -0
- package/build/src/observability/context_bridge.js +9 -20
- package/build/src/resolvers/identity.d.ts +3 -1
- package/build/src/resolvers/identity.js +7 -1
- package/package.json +9 -5
- package/skills/authkit-rp-client/SKILL.md +242 -0
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
|
-
*
|
|
10
|
-
*
|
|
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
|
-
|
|
13
|
-
|
|
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
|
|
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)
|
|
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)
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|