@adonis-agora/authkit-client 0.20.5 → 1.0.0-rc.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.
- package/CHANGELOG.md +957 -0
- package/README.md +47 -12
- package/build/auth.d.ts +2 -0
- package/build/auth.js +1 -0
- package/build/authkit_context_middleware.d.ts +1 -0
- package/build/authkit_context_middleware.js +2 -0
- package/build/commands/configure.d.ts +10 -0
- package/build/commands/configure.js +57 -2
- package/build/index.d.ts +3 -2
- package/build/index.js +1 -1
- package/build/providers/authkit_client_provider.d.ts +8 -1
- package/build/providers/authkit_client_provider.js +28 -12
- package/build/src/authenticator.d.ts +4 -3
- package/build/src/authenticator.js +6 -3
- package/build/src/host/authkit_client_guard.d.ts +51 -36
- package/build/src/host/authkit_client_guard.js +97 -46
- package/build/src/host/dashboard_guard.d.ts +3 -7
- package/build/src/host/dashboard_guard.js +3 -7
- package/build/src/host/request_authenticator.d.ts +28 -10
- package/build/src/host/request_authenticator.js +5 -3
- package/build/src/middleware/authkit_context_middleware.d.ts +3 -4
- package/build/src/middleware/authkit_context_middleware.js +5 -4
- package/build/src/middleware/backchannel_revocation_middleware.d.ts +1 -1
- package/build/src/middleware/backchannel_revocation_middleware.js +1 -1
- package/build/src/middleware/require_org_middleware.d.ts +3 -2
- package/build/src/middleware/require_org_middleware.js +5 -4
- package/build/src/resolvers/dpop_proof.d.ts +34 -0
- package/build/src/resolvers/dpop_proof.js +92 -0
- package/build/src/resolvers/factory.d.ts +5 -0
- package/build/src/resolvers/factory.js +1 -0
- package/build/src/resolvers/jwt_resolver.d.ts +9 -0
- package/build/src/resolvers/jwt_resolver.js +47 -3
- package/build/stubs/config/auth.stub +30 -0
- package/build/stubs/config/authkit_client.stub +2 -1
- package/build/stubs/middleware/auth_middleware.stub +25 -0
- package/build/stubs/middleware/silent_auth_middleware.stub +16 -0
- package/build/types.d.ts +5 -32
- package/build/types.js +1 -1
- package/package.json +10 -15
- package/skills/authkit-rp-client/SKILL.md +102 -47
- package/stubs/config/auth.stub +30 -0
- package/stubs/config/authkit_client.stub +2 -1
- package/stubs/middleware/auth_middleware.stub +25 -0
- package/stubs/middleware/silent_auth_middleware.stub +16 -0
- package/build/src/host/ctx_auth.d.ts +0 -6
- package/build/src/host/ctx_auth.js +0 -8
- package/build/src/middleware/auth_middleware.d.ts +0 -13
- package/build/src/middleware/auth_middleware.js +0 -13
- package/build/src/middleware/authkit_middleware.d.ts +0 -6
- package/build/src/middleware/authkit_middleware.js +0 -24
- package/build/src/middleware/silent_auth_middleware.d.ts +0 -13
- package/build/src/middleware/silent_auth_middleware.js +0 -15
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { jwtVerify } from 'jose';
|
|
2
2
|
import { DEFAULT_ALGS, getJwks } from '../jwks.js';
|
|
3
|
+
import { DpopReplayCache, verifyDpopProof } from './dpop_proof.js';
|
|
3
4
|
import { buildIdentityFromClaims } from './identity.js';
|
|
4
5
|
export class JwtResolver {
|
|
5
6
|
config;
|
|
7
|
+
#replay = new DpopReplayCache();
|
|
6
8
|
constructor(config) {
|
|
7
9
|
this.config = config;
|
|
8
10
|
}
|
|
9
|
-
/**
|
|
10
|
-
async
|
|
11
|
+
/** The verified claims of a raw JWT, or `null`. */
|
|
12
|
+
async #verify(token) {
|
|
11
13
|
const endpoints = await this.config.endpoints();
|
|
12
14
|
if (!endpoints)
|
|
13
15
|
return null;
|
|
@@ -21,16 +23,58 @@ export class JwtResolver {
|
|
|
21
23
|
// uma identidade válida exige um subject (`sub`)
|
|
22
24
|
if (!payload.sub)
|
|
23
25
|
return null;
|
|
24
|
-
return
|
|
26
|
+
return payload;
|
|
25
27
|
}
|
|
26
28
|
catch {
|
|
27
29
|
return null;
|
|
28
30
|
}
|
|
29
31
|
}
|
|
32
|
+
#identity(claims) {
|
|
33
|
+
return claims ? buildIdentityFromClaims(claims, this.config.globalRolesClaim) : null;
|
|
34
|
+
}
|
|
35
|
+
/** Valida um JWT cru e monta a Identity (ou null se inválido). */
|
|
36
|
+
async resolveToken(token) {
|
|
37
|
+
return this.#identity(await this.#verify(token));
|
|
38
|
+
}
|
|
30
39
|
async resolve(ctx) {
|
|
40
|
+
if (this.config.authorizationHeader)
|
|
41
|
+
return this.#resolveAuthorization(ctx);
|
|
31
42
|
const token = this.config.getToken?.(ctx) ?? null;
|
|
32
43
|
if (!token)
|
|
33
44
|
return null;
|
|
34
45
|
return this.resolveToken(token);
|
|
35
46
|
}
|
|
47
|
+
async #resolveAuthorization(ctx) {
|
|
48
|
+
const header = ctx.request.header('authorization');
|
|
49
|
+
if (!header)
|
|
50
|
+
return null;
|
|
51
|
+
const [scheme, token, ...rest] = header.trim().split(/\s+/);
|
|
52
|
+
if (!token || rest.length > 0)
|
|
53
|
+
return null;
|
|
54
|
+
const claims = await this.#verify(token);
|
|
55
|
+
if (!claims)
|
|
56
|
+
return null;
|
|
57
|
+
const jkt = claims.cnf?.jkt;
|
|
58
|
+
switch (scheme?.toLowerCase()) {
|
|
59
|
+
case 'bearer':
|
|
60
|
+
// RFC 9449 §7.1: a DPoP-bound token is not a bearer token.
|
|
61
|
+
return typeof jkt === 'string' ? null : this.#identity(claims);
|
|
62
|
+
case 'dpop': {
|
|
63
|
+
if (typeof jkt !== 'string')
|
|
64
|
+
return null;
|
|
65
|
+
const valid = await verifyDpopProof({
|
|
66
|
+
proof: ctx.request.header('dpop'),
|
|
67
|
+
method: ctx.request.method(),
|
|
68
|
+
url: ctx.request.completeUrl(false),
|
|
69
|
+
accessToken: token,
|
|
70
|
+
expectedJkt: jkt,
|
|
71
|
+
maxAge: this.config.dpopMaxAge ?? 300,
|
|
72
|
+
replay: this.#replay,
|
|
73
|
+
});
|
|
74
|
+
return valid ? this.#identity(claims) : null;
|
|
75
|
+
}
|
|
76
|
+
default:
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
36
80
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{{{
|
|
2
|
+
exports({ to: app.configPath('auth.ts') })
|
|
3
|
+
}}}
|
|
4
|
+
import { defineConfig } from '@adonisjs/auth'
|
|
5
|
+
import { sessionUserProvider } from '@adonisjs/auth/session'
|
|
6
|
+
import type { InferAuthenticators } from '@adonisjs/auth/types'
|
|
7
|
+
import { authkitClientGuard } from '@adonis-agora/authkit-client/auth'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* ctx.auth is backed by the AuthKit session. The guard takes the user from
|
|
11
|
+
* resolveUser in config/authkit_client.ts when you set one (e.g. lucidMirror,
|
|
12
|
+
* which creates the local row on first login), and falls back to this provider
|
|
13
|
+
* otherwise. The model's primary key must be the IdP's sub.
|
|
14
|
+
*/
|
|
15
|
+
const authConfig = defineConfig({
|
|
16
|
+
default: 'web',
|
|
17
|
+
guards: {
|
|
18
|
+
web: authkitClientGuard({
|
|
19
|
+
provider: sessionUserProvider({
|
|
20
|
+
model: () => import('{{ modelImport }}'),
|
|
21
|
+
}),
|
|
22
|
+
}),
|
|
23
|
+
},
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
export default authConfig
|
|
27
|
+
|
|
28
|
+
declare module '@adonisjs/auth/types' {
|
|
29
|
+
export interface Authenticators extends InferAuthenticators<typeof authConfig> {}
|
|
30
|
+
}
|
|
@@ -10,7 +10,8 @@ const authkitClientConfig = defineConfig({
|
|
|
10
10
|
clientSecret: env.get('AUTHKIT_CLIENT_SECRET'),
|
|
11
11
|
redirectUri: env.get('AUTHKIT_REDIRECT_URI'),
|
|
12
12
|
resolver: resolvers.jwt({ tokenSource: 'session' }),
|
|
13
|
-
//
|
|
13
|
+
// Creates/updates the local user on each login; ctx.auth.user is this user.
|
|
14
|
+
// resolveUser: lucidMirror(User, { sync: (identity) => ({ email: identity.email }) }),
|
|
14
15
|
// Roles do app? Configure resolveRoles no @adonis-agora/authz (não aqui — o AuthKit só autentica).
|
|
15
16
|
})
|
|
16
17
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{{{
|
|
2
|
+
exports({ to: app.middlewarePath('auth_middleware.ts') })
|
|
3
|
+
}}}
|
|
4
|
+
import type { HttpContext } from '@adonisjs/core/http'
|
|
5
|
+
import type { NextFn } from '@adonisjs/core/types/http'
|
|
6
|
+
import type { Authenticators } from '@adonisjs/auth/types'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Denies access to unauthenticated requests and sends them to the AuthKit
|
|
10
|
+
* login route registered by registerOidcClient(router).
|
|
11
|
+
*/
|
|
12
|
+
export default class AuthMiddleware {
|
|
13
|
+
redirectTo = '/auth/login'
|
|
14
|
+
|
|
15
|
+
async handle(
|
|
16
|
+
ctx: HttpContext,
|
|
17
|
+
next: NextFn,
|
|
18
|
+
options: {
|
|
19
|
+
guards?: (keyof Authenticators)[]
|
|
20
|
+
} = {}
|
|
21
|
+
) {
|
|
22
|
+
await ctx.auth.authenticateUsing(options.guards, { loginRoute: this.redirectTo })
|
|
23
|
+
return next()
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{{{
|
|
2
|
+
exports({ to: app.middlewarePath('silent_auth_middleware.ts') })
|
|
3
|
+
}}}
|
|
4
|
+
import type { HttpContext } from '@adonisjs/core/http'
|
|
5
|
+
import type { NextFn } from '@adonisjs/core/types/http'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Checks the AuthKit session without requiring one, so ctx.auth.user is set
|
|
9
|
+
* on routes that also serve guests.
|
|
10
|
+
*/
|
|
11
|
+
export default class SilentAuthMiddleware {
|
|
12
|
+
async handle(ctx: HttpContext, next: NextFn) {
|
|
13
|
+
await ctx.auth.check()
|
|
14
|
+
return next()
|
|
15
|
+
}
|
|
16
|
+
}
|
package/build/types.d.ts
CHANGED
|
@@ -1,43 +1,16 @@
|
|
|
1
|
-
import './
|
|
1
|
+
import './src/host/request_authenticator.js';
|
|
2
2
|
import type { AuthkitClientManager } from './providers/authkit_client_provider.js';
|
|
3
3
|
import type { Authenticator } from './src/authenticator.js';
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* Types of `@adonis-agora/authkit-client`.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* Importing this module brings the container bindings (`authkit.client`) and the
|
|
8
|
+
* `ctx.authkit` declaration into your program. `ctx.auth` belongs to
|
|
9
|
+
* `@adonisjs/auth`; use `authkitClientGuard` to back it with the AuthKit session.
|
|
9
10
|
*
|
|
10
11
|
* ```ts
|
|
11
12
|
* // any file in your app, e.g. app/types.ts
|
|
12
13
|
* import '@adonis-agora/authkit-client/types'
|
|
13
14
|
* ```
|
|
14
|
-
*
|
|
15
|
-
* NOTHING in this package imports this module on your behalf, and that is the
|
|
16
|
-
* point. `Authenticator` is generic (`Authenticator<TUser>`), and most apps
|
|
17
|
-
* declare their own augmentation so `auth.getUser()` returns their user model:
|
|
18
|
-
*
|
|
19
|
-
* ```ts
|
|
20
|
-
* declare module '@adonisjs/core/http' {
|
|
21
|
-
* interface HttpContext {
|
|
22
|
-
* auth: Authenticator<AppUser>
|
|
23
|
-
* }
|
|
24
|
-
* }
|
|
25
|
-
* ```
|
|
26
|
-
*
|
|
27
|
-
* Two declarations of the same property with different types do not merge —
|
|
28
|
-
* they conflict, and because `skipLibCheck` is on in every AdonisJS app the
|
|
29
|
-
* conflict is never reported: one simply wins by include order. When the
|
|
30
|
-
* library's won, `auth.getUser()` collapsed to `unknown` across the whole app.
|
|
31
|
-
* So the library ships the declaration and lets you decide whether to load it,
|
|
32
|
-
* rather than forcing it through a middleware import.
|
|
33
|
-
*
|
|
34
|
-
* The container bindings come along with it (see `container_bindings.ts`); they
|
|
35
|
-
* are also available on their own, which is what the provider and the middleware
|
|
36
|
-
* use.
|
|
37
15
|
*/
|
|
38
|
-
declare module '@adonisjs/core/http' {
|
|
39
|
-
interface HttpContext {
|
|
40
|
-
auth: Authenticator;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
16
|
export type { Authenticator, AuthkitClientManager };
|
package/build/types.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import './
|
|
1
|
+
import './src/host/request_authenticator.js';
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adonis-agora/authkit-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0-rc.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",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "https://github.com/DavideCarvalho/adonis-agora-authkit.git",
|
|
9
|
+
"url": "git+https://github.com/DavideCarvalho/adonis-agora-authkit.git",
|
|
10
10
|
"directory": "packages/authkit-client"
|
|
11
11
|
},
|
|
12
12
|
"homepage": "https://davidecarvalho.github.io/agora/docs/authkit",
|
|
@@ -33,7 +33,8 @@
|
|
|
33
33
|
"files": [
|
|
34
34
|
"build",
|
|
35
35
|
"stubs",
|
|
36
|
-
"skills"
|
|
36
|
+
"skills",
|
|
37
|
+
"CHANGELOG.md"
|
|
37
38
|
],
|
|
38
39
|
"exports": {
|
|
39
40
|
".": "./build/index.js",
|
|
@@ -41,14 +42,11 @@
|
|
|
41
42
|
"./container_bindings": "./build/container_bindings.js",
|
|
42
43
|
"./services/main": "./build/services/main.js",
|
|
43
44
|
"./authkit_client_provider": "./build/providers/authkit_client_provider.js",
|
|
44
|
-
"./auth": "./build/
|
|
45
|
+
"./auth": "./build/auth.js",
|
|
45
46
|
"./dashboard_guard": "./build/src/host/dashboard_guard.js",
|
|
46
|
-
"./
|
|
47
|
-
"./authkit_context_middleware": "./build/src/middleware/authkit_context_middleware.js",
|
|
47
|
+
"./authkit_context_middleware": "./build/authkit_context_middleware.js",
|
|
48
48
|
"./backchannel_revocation_middleware": "./build/src/middleware/backchannel_revocation_middleware.js",
|
|
49
|
-
"./auth_middleware": "./build/src/middleware/auth_middleware.js",
|
|
50
49
|
"./require_org_middleware": "./build/src/middleware/require_org_middleware.js",
|
|
51
|
-
"./silent_auth_middleware": "./build/src/middleware/silent_auth_middleware.js",
|
|
52
50
|
"./commands": "./build/commands/configure.js"
|
|
53
51
|
},
|
|
54
52
|
"adonisjs": {
|
|
@@ -64,16 +62,13 @@
|
|
|
64
62
|
"node": ">=24"
|
|
65
63
|
},
|
|
66
64
|
"peerDependencies": {
|
|
67
|
-
"@adonis-agora/resilience": ">=0.1.0 <1.0.0",
|
|
65
|
+
"@adonis-agora/resilience": ">=0.1.0 <2.0.0 || >=1.0.0-rc.0 <2.0.0",
|
|
68
66
|
"@adonisjs/auth": "^10.1.0",
|
|
69
67
|
"@adonisjs/core": "^7.5.0",
|
|
70
68
|
"@adonisjs/lucid": "^22.4.2",
|
|
71
69
|
"@adonisjs/session": "^8.1.0"
|
|
72
70
|
},
|
|
73
71
|
"peerDependenciesMeta": {
|
|
74
|
-
"@adonisjs/auth": {
|
|
75
|
-
"optional": true
|
|
76
|
-
},
|
|
77
72
|
"@adonisjs/lucid": {
|
|
78
73
|
"optional": true
|
|
79
74
|
},
|
|
@@ -82,11 +77,11 @@
|
|
|
82
77
|
}
|
|
83
78
|
},
|
|
84
79
|
"dependencies": {
|
|
85
|
-
"@adonis-agora/authkit-core": "0.
|
|
80
|
+
"@adonis-agora/authkit-core": "1.0.0-rc.0",
|
|
86
81
|
"jose": "6.2.15"
|
|
87
82
|
},
|
|
88
83
|
"devDependencies": {
|
|
89
|
-
"@adonis-agora/authkit-server": "0.
|
|
84
|
+
"@adonis-agora/authkit-server": "1.0.0-rc.0",
|
|
90
85
|
"@adonis-agora/resilience": "0.4.3",
|
|
91
86
|
"@adonisjs/auth": "10.1.0",
|
|
92
87
|
"@adonisjs/core": "7.6.0",
|
|
@@ -95,7 +90,7 @@
|
|
|
95
90
|
"@adonisjs/tsconfig": "2.0.0",
|
|
96
91
|
"@japa/assert": "4.2.0",
|
|
97
92
|
"@japa/runner": "5.4.0",
|
|
98
|
-
"@poppinss/ts-exec": "1.5.
|
|
93
|
+
"@poppinss/ts-exec": "1.5.2",
|
|
99
94
|
"@tanstack/intent": "0.5.6",
|
|
100
95
|
"@types/node": "26.6.5",
|
|
101
96
|
"ioredis-mock": "8.13.1",
|
|
@@ -2,16 +2,16 @@
|
|
|
2
2
|
name: authkit-rp-client
|
|
3
3
|
description: >-
|
|
4
4
|
Integrate an AdonisJS app as an OpenID Connect relying party with
|
|
5
|
-
@adonis-agora/authkit-client. Covers defineConfig
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
(
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
belongs to @adonis-agora/authz
|
|
13
|
-
fixing null identity
|
|
14
|
-
logout.
|
|
5
|
+
@adonis-agora/authkit-client. Covers defineConfig, resolvers.jwt / pat / opaque,
|
|
6
|
+
resolveUser (lucidMirror, identityToUser, createUserinfoResolver), ctx.auth via the
|
|
7
|
+
required @adonisjs/auth peer (authkitClientGuard with sessionUserProvider or
|
|
8
|
+
authkitIdentityProvider, middleware.auth(), auth.user), API guards with
|
|
9
|
+
authkitClientGuard({ resolver }) for bearer JWT, DPoP, opaque and PAT, the session ->
|
|
10
|
+
initialize_auth -> authkit_context_middleware ordering rule, ctx.authkit
|
|
11
|
+
(getIdentity/hasGlobalRole/toSharedProps), registerOidcClient, backchannelLogout:
|
|
12
|
+
{ store }, and why app-role gating belongs to @adonis-agora/authz. Use when wiring a
|
|
13
|
+
client app to the IdP, fixing a null identity or a first-login loop, choosing a user
|
|
14
|
+
provider, protecting API routes, or adding back-channel logout.
|
|
15
15
|
license: MIT
|
|
16
16
|
metadata:
|
|
17
17
|
type: core
|
|
@@ -29,10 +29,15 @@ sources:
|
|
|
29
29
|
|
|
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
|
-
locally-validated ID tokens (JWKS)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
locally-validated ID tokens (JWKS). `ctx.auth` is `@adonisjs/auth` (a required peer)
|
|
33
|
+
backed by the AuthKit session through `authkitClientGuard`; `ctx.authkit` carries the
|
|
34
|
+
AuthKit-only data. `configure` installs `@adonisjs/auth` if missing, publishes
|
|
35
|
+
`config/authkit_client.ts`, `config/auth.ts` (guard + `sessionUserProvider`, `--model`,
|
|
36
|
+
default `#models/user`; an existing file is left alone) and the named `auth` /
|
|
37
|
+
`silentAuth` middleware, and registers the providers plus `initialize_auth_middleware` and
|
|
38
|
+
`authkit_context_middleware`; the login/callback/logout routes come from
|
|
39
|
+
`registerOidcClient(router)`. (`configure` also publishes an `OidcSessionController` stub,
|
|
40
|
+
only for apps that must customise the flow.)
|
|
36
41
|
|
|
37
42
|
## Setup
|
|
38
43
|
|
|
@@ -43,7 +48,8 @@ node ace add @adonis-agora/authkit-client
|
|
|
43
48
|
```ts
|
|
44
49
|
// config/authkit_client.ts
|
|
45
50
|
import env from '#start/env'
|
|
46
|
-
import { defineConfig, resolvers } from '@adonis-agora/authkit-client'
|
|
51
|
+
import { defineConfig, lucidMirror, resolvers } from '@adonis-agora/authkit-client'
|
|
52
|
+
import AppUser from '#models/app_user'
|
|
47
53
|
|
|
48
54
|
const authkitClientConfig = defineConfig({
|
|
49
55
|
issuer: env.get('AUTHKIT_ISSUER'),
|
|
@@ -51,11 +57,29 @@ const authkitClientConfig = defineConfig({
|
|
|
51
57
|
clientSecret: env.get('AUTHKIT_CLIENT_SECRET'),
|
|
52
58
|
redirectUri: env.get('AUTHKIT_REDIRECT_URI'), // must point at the callback route
|
|
53
59
|
resolver: resolvers.jwt({ tokenSource: 'session' }),
|
|
60
|
+
// Creates the local row on first login; ctx.auth.user is this user.
|
|
61
|
+
resolveUser: lucidMirror(AppUser, { sync: (i) => ({ email: i.email }) }),
|
|
54
62
|
})
|
|
55
63
|
|
|
56
64
|
export default authkitClientConfig
|
|
57
65
|
```
|
|
58
66
|
|
|
67
|
+
```ts
|
|
68
|
+
// config/auth.ts (written by configure)
|
|
69
|
+
import { defineConfig } from '@adonisjs/auth'
|
|
70
|
+
import { sessionUserProvider } from '@adonisjs/auth/session'
|
|
71
|
+
import { authkitClientGuard } from '@adonis-agora/authkit-client/auth'
|
|
72
|
+
|
|
73
|
+
export default defineConfig({
|
|
74
|
+
default: 'web',
|
|
75
|
+
guards: {
|
|
76
|
+
web: authkitClientGuard({
|
|
77
|
+
provider: sessionUserProvider({ model: () => import('#models/app_user') }),
|
|
78
|
+
}),
|
|
79
|
+
},
|
|
80
|
+
})
|
|
81
|
+
```
|
|
82
|
+
|
|
59
83
|
```ts
|
|
60
84
|
// start/routes.ts
|
|
61
85
|
import router from '@adonisjs/core/services/router'
|
|
@@ -74,26 +98,48 @@ Source: `packages/authkit-client/README.md`, `stubs/config/authkit_client.stub`.
|
|
|
74
98
|
|
|
75
99
|
## Core patterns
|
|
76
100
|
|
|
77
|
-
### Pattern 1 — `ctx.auth` per request
|
|
78
|
-
|
|
79
|
-
The `authkit_middleware` populates `ctx.auth`; identity comes from the validated
|
|
80
|
-
token, the user from your `resolveUser`:
|
|
101
|
+
### Pattern 1 — `ctx.auth` per request, `ctx.authkit` for AuthKit-only data
|
|
81
102
|
|
|
82
103
|
```ts
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
104
|
+
// app/controllers/me_controller.ts
|
|
105
|
+
export default class MeController {
|
|
106
|
+
async handle({ auth, authkit }: HttpContext) {
|
|
107
|
+
const user = auth.getUserOrFail() // the resolveUser user (sync, behind auth)
|
|
108
|
+
const identity = await authkit.getIdentity() // validated OIDC claims
|
|
109
|
+
const isAdmin = authkit.hasGlobalRole('ADMIN') // sync — reads token claims
|
|
110
|
+
return { user, identity, isAdmin }
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// start/routes.ts
|
|
115
|
+
router.get('/me', [MeController]).use(middleware.auth())
|
|
89
116
|
```
|
|
90
117
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
118
|
+
The guard's user comes from `resolveUser` first (so `lucidMirror` provisions on first
|
|
119
|
+
login; `ctx.auth.user` is the same object as `await ctx.authkit.getUser()`), then from the
|
|
120
|
+
provider. No local user table: `provider: authkitIdentityProvider()` makes `ctx.auth.user`
|
|
121
|
+
the `Identity`. `auth.getUserOrFail()` is synchronous and throws unless
|
|
122
|
+
`middleware.auth()`/`middleware.silentAuth()`/`auth.check()` ran.
|
|
94
123
|
|
|
95
124
|
Source: `packages/authkit-client/README.md` § Uso por request,
|
|
96
|
-
`src/
|
|
125
|
+
`src/host/authkit_client_guard.ts`.
|
|
126
|
+
|
|
127
|
+
### Pattern 1b — API routes (bearer JWT, DPoP, opaque, PAT)
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
// config/auth.ts
|
|
131
|
+
api: authkitClientGuard({
|
|
132
|
+
provider: authkitIdentityProvider(),
|
|
133
|
+
resolver: resolvers.jwt({ tokenSource: 'bearer' }), // Bearer <jwt> or DPoP <jwt> + DPoP proof
|
|
134
|
+
}),
|
|
135
|
+
|
|
136
|
+
// start/routes.ts
|
|
137
|
+
router.group(() => { /* … */ }).prefix('/api').use(middleware.auth({ guards: ['api'] }))
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
`resolver` replaces the configured resolver for that guard only; `resolvers.opaque({
|
|
141
|
+
tokenSource: 'bearer' })` and `resolvers.pat({ introspectionUrl, introspectionSecret })`
|
|
142
|
+
work the same way. A DPoP-bound token (`cnf.jkt`) sent as `Bearer` is refused.
|
|
97
143
|
|
|
98
144
|
### Pattern 2 — `resolveUser` per database topology
|
|
99
145
|
|
|
@@ -124,11 +170,9 @@ RP-initiated logout and role-based redirects in one call:
|
|
|
124
170
|
|
|
125
171
|
```ts
|
|
126
172
|
import router from '@adonisjs/core/services/router'
|
|
127
|
-
import middleware from '#start/kernel'
|
|
128
173
|
import { registerOidcClient } from '@adonis-agora/authkit-client'
|
|
129
174
|
|
|
130
175
|
registerOidcClient(router, {
|
|
131
|
-
loginMiddleware: middleware.guest(),
|
|
132
176
|
redirects: { byGlobalRole: { ADMIN: '/admin' }, default: '/' },
|
|
133
177
|
// prefix defaults to /auth → /auth/login, /auth/callback, /auth/logout
|
|
134
178
|
// backchannelLogout defaults to true → also registers POST /auth/backchannel-logout
|
|
@@ -162,16 +206,23 @@ Source: `src/define_config.ts` (`BackchannelLogoutInput`), README.
|
|
|
162
206
|
|
|
163
207
|
## Common mistakes
|
|
164
208
|
|
|
165
|
-
### HIGH — Session middleware running after `
|
|
209
|
+
### HIGH — Session middleware running after `authkit_context_middleware`
|
|
166
210
|
|
|
167
211
|
```ts
|
|
168
212
|
// Wrong — resolver reads an empty session; every request looks anonymous
|
|
169
|
-
router.use([
|
|
213
|
+
router.use([
|
|
214
|
+
() => import('@adonis-agora/authkit-client/authkit_context_middleware'),
|
|
215
|
+
() => import('@adonisjs/session/session_middleware'),
|
|
216
|
+
])
|
|
170
217
|
```
|
|
171
218
|
|
|
172
219
|
```ts
|
|
173
|
-
// Correct —
|
|
174
|
-
router.use([
|
|
220
|
+
// Correct — session, then initialize_auth, then the AuthKit context middleware
|
|
221
|
+
router.use([
|
|
222
|
+
() => import('@adonisjs/session/session_middleware'),
|
|
223
|
+
() => import('@adonisjs/auth/initialize_auth_middleware'),
|
|
224
|
+
() => import('@adonis-agora/authkit-client/authkit_context_middleware'),
|
|
225
|
+
])
|
|
175
226
|
```
|
|
176
227
|
|
|
177
228
|
With `resolvers.jwt({ tokenSource: 'session' })` (the default) the resolver reads
|
|
@@ -185,20 +236,20 @@ Source: `packages/authkit-client/README.md` § Ordem de middleware (importante).
|
|
|
185
236
|
|
|
186
237
|
```ts
|
|
187
238
|
// Wrong — expecting authkit to resolve app roles/permissions
|
|
188
|
-
if (
|
|
239
|
+
if (authkit.hasGlobalRole('ADMIN')) { /* app-role check? */ }
|
|
189
240
|
const can = await authkit.checkPermission(user, 'posts.edit') // does not exist
|
|
190
241
|
```
|
|
191
242
|
|
|
192
243
|
```ts
|
|
193
244
|
// Correct — AuthKit authenticates + exposes token globalRoles; authz decides access
|
|
194
|
-
if (
|
|
245
|
+
if (authkit.hasGlobalRole('ADMIN')) { /* global role from the token claims */ }
|
|
195
246
|
// App roles/permissions: configure resolveRoles in @adonis-agora/authz
|
|
196
|
-
// and enforce with authz/Bouncer route guards, not
|
|
247
|
+
// and enforce with authz/Bouncer route guards, not middleware.auth().
|
|
197
248
|
```
|
|
198
249
|
|
|
199
250
|
AuthKit only authenticates and knows the `globalRoles` claim; app roles (domain
|
|
200
251
|
tables) and permissions were deliberately moved out to `@adonis-agora/authz`.
|
|
201
|
-
|
|
252
|
+
`middleware.auth()` only enforces login.
|
|
202
253
|
|
|
203
254
|
Source: `packages/authkit-client/README.md` callout ("Autorização por app-role saiu
|
|
204
255
|
do AuthKit"), `stubs/config/authkit_client.stub` comment.
|
|
@@ -227,21 +278,25 @@ token's `exp`, and the cache holds at most 10,000 tokens per resolver.
|
|
|
227
278
|
|
|
228
279
|
Source: `src/resolvers/factory.ts` (`resolvers.opaque` throw), README.
|
|
229
280
|
|
|
230
|
-
### MEDIUM —
|
|
281
|
+
### MEDIUM — API routes on the session guard
|
|
231
282
|
|
|
232
283
|
```ts
|
|
233
|
-
// Wrong —
|
|
234
|
-
|
|
284
|
+
// Wrong — the web guard reads the OIDC session; a Bearer header is ignored
|
|
285
|
+
router.group(() => { /* … */ }).prefix('/api').use(middleware.auth())
|
|
235
286
|
```
|
|
236
287
|
|
|
237
288
|
```ts
|
|
238
|
-
// Correct —
|
|
239
|
-
|
|
289
|
+
// Correct — an API guard with its own resolver
|
|
290
|
+
api: authkitClientGuard({
|
|
291
|
+
provider: authkitIdentityProvider(),
|
|
292
|
+
resolver: resolvers.jwt({ tokenSource: 'bearer' }),
|
|
293
|
+
}),
|
|
294
|
+
router.group(() => { /* … */ }).prefix('/api').use(middleware.auth({ guards: ['api'] }))
|
|
240
295
|
```
|
|
241
296
|
|
|
242
|
-
`tokenSource` decides where the
|
|
243
|
-
apps) vs `'bearer'` (`Authorization` header
|
|
244
|
-
|
|
297
|
+
`tokenSource` decides where the token is read from: `'session'` (default, cookie
|
|
298
|
+
apps) vs `'bearer'` (`Authorization` header). The mismatched source yields a silent
|
|
299
|
+
`null` identity (a 401 / redirect), not an error.
|
|
245
300
|
|
|
246
301
|
Source: `packages/authkit-client/README.md` § Resolução de sessão,
|
|
247
302
|
`src/resolvers/factory.ts` (`JwtResolverFactoryConfig`).
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{{{
|
|
2
|
+
exports({ to: app.configPath('auth.ts') })
|
|
3
|
+
}}}
|
|
4
|
+
import { defineConfig } from '@adonisjs/auth'
|
|
5
|
+
import { sessionUserProvider } from '@adonisjs/auth/session'
|
|
6
|
+
import type { InferAuthenticators } from '@adonisjs/auth/types'
|
|
7
|
+
import { authkitClientGuard } from '@adonis-agora/authkit-client/auth'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* ctx.auth is backed by the AuthKit session. The guard takes the user from
|
|
11
|
+
* resolveUser in config/authkit_client.ts when you set one (e.g. lucidMirror,
|
|
12
|
+
* which creates the local row on first login), and falls back to this provider
|
|
13
|
+
* otherwise. The model's primary key must be the IdP's sub.
|
|
14
|
+
*/
|
|
15
|
+
const authConfig = defineConfig({
|
|
16
|
+
default: 'web',
|
|
17
|
+
guards: {
|
|
18
|
+
web: authkitClientGuard({
|
|
19
|
+
provider: sessionUserProvider({
|
|
20
|
+
model: () => import('{{ modelImport }}'),
|
|
21
|
+
}),
|
|
22
|
+
}),
|
|
23
|
+
},
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
export default authConfig
|
|
27
|
+
|
|
28
|
+
declare module '@adonisjs/auth/types' {
|
|
29
|
+
export interface Authenticators extends InferAuthenticators<typeof authConfig> {}
|
|
30
|
+
}
|
|
@@ -10,7 +10,8 @@ const authkitClientConfig = defineConfig({
|
|
|
10
10
|
clientSecret: env.get('AUTHKIT_CLIENT_SECRET'),
|
|
11
11
|
redirectUri: env.get('AUTHKIT_REDIRECT_URI'),
|
|
12
12
|
resolver: resolvers.jwt({ tokenSource: 'session' }),
|
|
13
|
-
//
|
|
13
|
+
// Creates/updates the local user on each login; ctx.auth.user is this user.
|
|
14
|
+
// resolveUser: lucidMirror(User, { sync: (identity) => ({ email: identity.email }) }),
|
|
14
15
|
// Roles do app? Configure resolveRoles no @adonis-agora/authz (não aqui — o AuthKit só autentica).
|
|
15
16
|
})
|
|
16
17
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{{{
|
|
2
|
+
exports({ to: app.middlewarePath('auth_middleware.ts') })
|
|
3
|
+
}}}
|
|
4
|
+
import type { HttpContext } from '@adonisjs/core/http'
|
|
5
|
+
import type { NextFn } from '@adonisjs/core/types/http'
|
|
6
|
+
import type { Authenticators } from '@adonisjs/auth/types'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Denies access to unauthenticated requests and sends them to the AuthKit
|
|
10
|
+
* login route registered by registerOidcClient(router).
|
|
11
|
+
*/
|
|
12
|
+
export default class AuthMiddleware {
|
|
13
|
+
redirectTo = '/auth/login'
|
|
14
|
+
|
|
15
|
+
async handle(
|
|
16
|
+
ctx: HttpContext,
|
|
17
|
+
next: NextFn,
|
|
18
|
+
options: {
|
|
19
|
+
guards?: (keyof Authenticators)[]
|
|
20
|
+
} = {}
|
|
21
|
+
) {
|
|
22
|
+
await ctx.auth.authenticateUsing(options.guards, { loginRoute: this.redirectTo })
|
|
23
|
+
return next()
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{{{
|
|
2
|
+
exports({ to: app.middlewarePath('silent_auth_middleware.ts') })
|
|
3
|
+
}}}
|
|
4
|
+
import type { HttpContext } from '@adonisjs/core/http'
|
|
5
|
+
import type { NextFn } from '@adonisjs/core/types/http'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Checks the AuthKit session without requiring one, so ctx.auth.user is set
|
|
9
|
+
* on routes that also serve guests.
|
|
10
|
+
*/
|
|
11
|
+
export default class SilentAuthMiddleware {
|
|
12
|
+
async handle(ctx: HttpContext, next: NextFn) {
|
|
13
|
+
await ctx.auth.check()
|
|
14
|
+
return next()
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import type { HttpContext } from '@adonisjs/core/http';
|
|
2
|
-
import type { Authenticator } from '../authenticator.js';
|
|
3
|
-
/** Read the authenticator the middleware placed on the context. */
|
|
4
|
-
export declare function ctxAuth(ctx: HttpContext): Authenticator;
|
|
5
|
-
/** Place the authenticator on the context. */
|
|
6
|
-
export declare function setCtxAuth(ctx: HttpContext, authenticator: Authenticator): void;
|