@adonis-agora/authkit-server 0.60.0 → 0.61.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/build/providers/authkit_server_provider.js +7 -0
- package/build/src/define_config.d.ts +23 -0
- package/build/src/define_config.js +6 -0
- package/build/src/host/auth_host_config.d.ts +9 -0
- package/build/src/host/controllers/headless_login_methods_controller.d.ts +31 -0
- package/build/src/host/controllers/headless_login_methods_controller.js +79 -0
- package/build/src/host/login_methods_state.d.ts +39 -0
- package/build/src/host/login_methods_state.js +52 -0
- package/build/src/host/register_auth_host.js +15 -0
- package/package.json +7 -4
- package/skills/authkit-idp-setup/SKILL.md +222 -0
- package/skills/authkit-interactions/SKILL.md +236 -0
|
@@ -77,6 +77,13 @@ export default class AuthkitServerProvider {
|
|
|
77
77
|
// Defaults estruturais de `config.routes` (o argumento ainda vence).
|
|
78
78
|
routes: typeof config.routes === 'object' ? config.routes : undefined,
|
|
79
79
|
lockedRouteOptions: config.lockedRouteOptions,
|
|
80
|
+
// API headless — repassa pro registerAuthHost montar as rotas.
|
|
81
|
+
headless: config.headless
|
|
82
|
+
? {
|
|
83
|
+
baseUrl: config.headless.baseUrl,
|
|
84
|
+
resolveAccountId: config.headless.resolveAccountId,
|
|
85
|
+
}
|
|
86
|
+
: undefined,
|
|
80
87
|
});
|
|
81
88
|
}
|
|
82
89
|
}
|
|
@@ -822,6 +822,24 @@ export interface AuthServerConfigInput {
|
|
|
822
822
|
i18n?: I18nConfig;
|
|
823
823
|
/** Configuração de providers sociais. */
|
|
824
824
|
social?: AuthSocialConfig;
|
|
825
|
+
/**
|
|
826
|
+
* API headless do AuthKit (Clerk-style). Quando declarada, o host-kit monta
|
|
827
|
+
* uma JSON API (`GET/PUT {baseUrl}/login-methods`) autenticada por um resolver
|
|
828
|
+
* de conta fornecido pelo HOST (`resolveAccountId`) — tipicamente a própria
|
|
829
|
+
* sessão do RP (`await ctx.auth.getUserOrFail()`). Assim um app usa AuthKit
|
|
830
|
+
* como frontend sem expor o console `/account/*`. Default: desligado (opt-in).
|
|
831
|
+
*/
|
|
832
|
+
headless?: {
|
|
833
|
+
/** Base da JSON API headless. Default: `/api/authkit`. */
|
|
834
|
+
baseUrl?: string;
|
|
835
|
+
/**
|
|
836
|
+
* Resolve o id da conta (na identidade do IdP) a partir do contexto da
|
|
837
|
+
* request do HOST. Retorne null quando não autenticado → 401. Um `throw`
|
|
838
|
+
* do resolver também vira 401. O id deve ser o mesmo `sub`/`AuthAccount.id`
|
|
839
|
+
* usado pelo account store (no entre-textos, `auth.user.id === app.user.id`).
|
|
840
|
+
*/
|
|
841
|
+
resolveAccountId: (ctx: import('@adonisjs/core/http').HttpContext) => string | null;
|
|
842
|
+
};
|
|
825
843
|
/** Segredo para autenticar requests de introspecção de PAT. */
|
|
826
844
|
patIntrospectionSecret?: string;
|
|
827
845
|
/** Rate-limiting das rotas sensíveis (anti-brute-force). Default: ligado (no-op se o limiter não estiver configurado). */
|
|
@@ -1075,6 +1093,11 @@ export interface ResolvedServerConfig {
|
|
|
1075
1093
|
firstPartyClients?: string[];
|
|
1076
1094
|
social?: AuthSocialConfig;
|
|
1077
1095
|
patIntrospectionSecret?: string;
|
|
1096
|
+
/** API headless resolvida (Clerk-style). Presente só quando o host a declarou. */
|
|
1097
|
+
headless?: {
|
|
1098
|
+
baseUrl: string;
|
|
1099
|
+
resolveAccountId: (ctx: import('@adonisjs/core/http').HttpContext) => string | null;
|
|
1100
|
+
};
|
|
1078
1101
|
rateLimit: ResolvedRateLimitConfig;
|
|
1079
1102
|
/** Bloqueio progressivo de conta resolvido (sempre presente; default ligado). */
|
|
1080
1103
|
lockout: ResolvedLockoutConfig;
|
|
@@ -395,6 +395,12 @@ export function defineConfig(config) {
|
|
|
395
395
|
passwordless: resolvePasswordless(config.passwordless),
|
|
396
396
|
authMethods: resolveAuthMethodsConfig(config.authMethods),
|
|
397
397
|
login: resolveLogin(config.login),
|
|
398
|
+
headless: config.headless
|
|
399
|
+
? {
|
|
400
|
+
baseUrl: config.headless.baseUrl ?? '/api/authkit',
|
|
401
|
+
resolveAccountId: config.headless.resolveAccountId,
|
|
402
|
+
}
|
|
403
|
+
: undefined,
|
|
398
404
|
registration: resolveRegistration(config.registration),
|
|
399
405
|
accessTokens: resolveAccessTokens(config.issuer, config.accessTokens),
|
|
400
406
|
admin: resolveAdmin(config.admin),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { HttpContext } from '@adonisjs/core/http';
|
|
1
2
|
import type { AuthSocialConfig, ResolvedRateLimitConfig } from '../define_config.js';
|
|
2
3
|
import type { PolicyRouteOption } from './config_locks.js';
|
|
3
4
|
import type { AuthHostOptions } from './register_auth_host.js';
|
|
@@ -34,6 +35,14 @@ export interface AuthHostRuntimeConfig {
|
|
|
34
35
|
* Ver `deriveLockedRouteOptions`.
|
|
35
36
|
*/
|
|
36
37
|
lockedRouteOptions?: PolicyRouteOption[];
|
|
38
|
+
/**
|
|
39
|
+
* API headless (Clerk-style). Presente quando o host a declarou — o provider o
|
|
40
|
+
* stash no boot para `registerAuthHost` montar as rotas sem reler o config inteiro.
|
|
41
|
+
*/
|
|
42
|
+
headless?: {
|
|
43
|
+
baseUrl: string;
|
|
44
|
+
resolveAccountId: (ctx: HttpContext) => string | null;
|
|
45
|
+
};
|
|
37
46
|
}
|
|
38
47
|
/** Stash dos bits de routing (chamado no boot do provider). */
|
|
39
48
|
export declare function setAuthHostConfig(config: AuthHostRuntimeConfig): void;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { HttpContext } from '@adonisjs/core/http';
|
|
2
|
+
/**
|
|
3
|
+
* API headless do AuthKit (Clerk-style).
|
|
4
|
+
*
|
|
5
|
+
* Diferente da JSON API do console (`/account/api/*`, guardada pela sessão de
|
|
6
|
+
* CONTA), esta API é protegida por um resolver de conta fornecido pelo HOST
|
|
7
|
+
* (`headless.resolveAccountId`) — tipicamente a própria sessão do app
|
|
8
|
+
* (`await ctx.auth.getUserOrFail()`). Assim um RP integrado usa o app como
|
|
9
|
+
* frontend sem expor o console `/account/*`.
|
|
10
|
+
*/
|
|
11
|
+
export default class HeadlessLoginMethodsController {
|
|
12
|
+
#private;
|
|
13
|
+
/**
|
|
14
|
+
* GET {baseUrl}/login-methods
|
|
15
|
+
* Estado final de tipos de login para a conta resolvida pelo host.
|
|
16
|
+
*/
|
|
17
|
+
index(ctx: HttpContext): Promise<void | import("../login_methods_state.js").LoginMethodsState>;
|
|
18
|
+
/**
|
|
19
|
+
* PUT {baseUrl}/login-methods
|
|
20
|
+
* Grava a preferência de tipos de login para a conta resolvida pelo host.
|
|
21
|
+
*/
|
|
22
|
+
update(ctx: HttpContext): Promise<void | {
|
|
23
|
+
ok: boolean;
|
|
24
|
+
methods: {
|
|
25
|
+
password?: boolean;
|
|
26
|
+
magicLink?: boolean;
|
|
27
|
+
passkey?: boolean;
|
|
28
|
+
social?: boolean;
|
|
29
|
+
};
|
|
30
|
+
}>;
|
|
31
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { supportsLoginMethodsPreference } from '../../accounts/account_store.js';
|
|
2
|
+
import { loginMethodsStateForAccount, resolveGlobalAuthMethods } from '../login_methods_state.js';
|
|
3
|
+
import { resolveRuntimeSettingsOrNoop } from '../runtime_settings.js';
|
|
4
|
+
import { parseUserLoginMethodsPayload } from '../user_login_methods.js';
|
|
5
|
+
/** Erro JSON padrão. */
|
|
6
|
+
function apiErr(code, message) {
|
|
7
|
+
return { error: { code, message } };
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* API headless do AuthKit (Clerk-style).
|
|
11
|
+
*
|
|
12
|
+
* Diferente da JSON API do console (`/account/api/*`, guardada pela sessão de
|
|
13
|
+
* CONTA), esta API é protegida por um resolver de conta fornecido pelo HOST
|
|
14
|
+
* (`headless.resolveAccountId`) — tipicamente a própria sessão do app
|
|
15
|
+
* (`await ctx.auth.getUserOrFail()`). Assim um RP integrado usa o app como
|
|
16
|
+
* frontend sem expor o console `/account/*`.
|
|
17
|
+
*/
|
|
18
|
+
export default class HeadlessLoginMethodsController {
|
|
19
|
+
/**
|
|
20
|
+
* GET {baseUrl}/login-methods
|
|
21
|
+
* Estado final de tipos de login para a conta resolvida pelo host.
|
|
22
|
+
*/
|
|
23
|
+
async index(ctx) {
|
|
24
|
+
const service = await ctx.containerResolver.make('authkit.server');
|
|
25
|
+
const cfg = service.config;
|
|
26
|
+
const accountId = await this.#resolveAccount(ctx, cfg);
|
|
27
|
+
if (!accountId) {
|
|
28
|
+
return ctx.response.unauthorized(apiErr('unauthorized', 'Not authenticated.'));
|
|
29
|
+
}
|
|
30
|
+
const settings = await resolveRuntimeSettingsOrNoop(ctx);
|
|
31
|
+
return loginMethodsStateForAccount(accountId, cfg.accountStore, settings, cfg);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* PUT {baseUrl}/login-methods
|
|
35
|
+
* Grava a preferência de tipos de login para a conta resolvida pelo host.
|
|
36
|
+
*/
|
|
37
|
+
async update(ctx) {
|
|
38
|
+
const service = await ctx.containerResolver.make('authkit.server');
|
|
39
|
+
const cfg = service.config;
|
|
40
|
+
const accountId = await this.#resolveAccount(ctx, cfg);
|
|
41
|
+
if (!accountId) {
|
|
42
|
+
return ctx.response.unauthorized(apiErr('unauthorized', 'Not authenticated.'));
|
|
43
|
+
}
|
|
44
|
+
if (!supportsLoginMethodsPreference(cfg.accountStore)) {
|
|
45
|
+
return ctx.response.notFound(apiErr('not_supported', 'Login methods preference not supported.'));
|
|
46
|
+
}
|
|
47
|
+
const parsed = parseUserLoginMethodsPayload(ctx.request.body());
|
|
48
|
+
if (!parsed.ok) {
|
|
49
|
+
return ctx.response.badRequest(apiErr(parsed.error, 'Invalid login methods payload.'));
|
|
50
|
+
}
|
|
51
|
+
// All-off guard: preferência não pode zerar todos os métodos globais.
|
|
52
|
+
const settings = await resolveRuntimeSettingsOrNoop(ctx);
|
|
53
|
+
const { resolved: global } = await resolveGlobalAuthMethods(settings, cfg);
|
|
54
|
+
const value = { ...parsed.value };
|
|
55
|
+
const wouldBeAllOff = (value.password === false || !global.password) &&
|
|
56
|
+
(value.magicLink === false || !global.magicLink) &&
|
|
57
|
+
(value.passkey === false || !global.passkey) &&
|
|
58
|
+
(value.social === false || global.social.length === 0);
|
|
59
|
+
if (wouldBeAllOff) {
|
|
60
|
+
return ctx.response.badRequest(apiErr('all_methods_off', 'Cannot disable every login method.'));
|
|
61
|
+
}
|
|
62
|
+
await cfg.accountStore.setLoginMethods(accountId, Object.keys(value).length > 0 ? value : null);
|
|
63
|
+
return { ok: true, methods: value };
|
|
64
|
+
}
|
|
65
|
+
/** Delega ao resolver do host; null (ou throw de auth) => 401. */
|
|
66
|
+
async #resolveAccount(ctx, cfg) {
|
|
67
|
+
const resolve = cfg.headless?.resolveAccountId;
|
|
68
|
+
if (!resolve)
|
|
69
|
+
return null;
|
|
70
|
+
try {
|
|
71
|
+
const id = await resolve(ctx);
|
|
72
|
+
return id ? String(id) : null;
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// Resolver do host lançou (ex.: auth.getUserOrFail sem sessão) → não-autorizado.
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ResolvedServerConfig } from '../define_config.js';
|
|
2
|
+
import type { SettingsCapability } from './runtime_settings.js';
|
|
3
|
+
import { type UserLoginMethods } from './user_login_methods.js';
|
|
4
|
+
/** Resultado compartilhado de "estado de tipos de login" entregue à UI. */
|
|
5
|
+
export interface LoginMethodsState {
|
|
6
|
+
supported: boolean;
|
|
7
|
+
/** Preferência crua do usuário; {} = sem preferência (herda globais). */
|
|
8
|
+
methods: UserLoginMethods | Record<string, never>;
|
|
9
|
+
/** Estado final por método (global ∩ preferência) — o que a tela de login mostra. */
|
|
10
|
+
available: {
|
|
11
|
+
password: boolean;
|
|
12
|
+
magicLink: boolean;
|
|
13
|
+
passkey: boolean;
|
|
14
|
+
social: string[];
|
|
15
|
+
forgotPassword: boolean;
|
|
16
|
+
} | null;
|
|
17
|
+
/** Métodos fora do controle do usuário (globalmente off ou pin de config). */
|
|
18
|
+
locked: {
|
|
19
|
+
password: boolean;
|
|
20
|
+
magicLink: boolean;
|
|
21
|
+
passkey: boolean;
|
|
22
|
+
social: boolean;
|
|
23
|
+
} | null;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Métodos globais efetivos + pins de config — resolução compartilhada entre o
|
|
27
|
+
* console (account API) e a API headless. Fail-safe: erro → defaults.
|
|
28
|
+
*/
|
|
29
|
+
export declare function resolveGlobalAuthMethods(settings: SettingsCapability, cfg: ResolvedServerConfig): Promise<{
|
|
30
|
+
resolved: import("./runtime_toggles.js").ResolvedAuthMethods;
|
|
31
|
+
locked: (keyof import("./runtime_toggles.js").AuthMethodsConfigOverride)[];
|
|
32
|
+
}>;
|
|
33
|
+
/**
|
|
34
|
+
* Monta o estado final de tipos de login para uma conta, intersectando a
|
|
35
|
+
* preferência do usuário com os métodos globais efetivos. Reusado pelo console
|
|
36
|
+
* de conta (`/account/api/login-methods`) e pela API headless
|
|
37
|
+
* (`{baseUrl}/login-methods`) — sem duplicar regra.
|
|
38
|
+
*/
|
|
39
|
+
export declare function loginMethodsStateForAccount(accountId: string, store: ResolvedServerConfig['accountStore'], settings: SettingsCapability, cfg: ResolvedServerConfig): Promise<LoginMethodsState>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { supportsMagicLink } from '../accounts/account_store.js';
|
|
2
|
+
import { supportsPasskeys } from '../accounts/account_store.js';
|
|
3
|
+
import { supportsLoginMethodsPreference } from '../accounts/account_store.js';
|
|
4
|
+
import { resolveEffectiveAuthMethods } from './runtime_toggles.js';
|
|
5
|
+
import { configLockedAuthMethods } from './runtime_toggles.js';
|
|
6
|
+
import { normalizeUserLoginMethods, resolveEffectiveUserLoginMethods, } from './user_login_methods.js';
|
|
7
|
+
/**
|
|
8
|
+
* Métodos globais efetivos + pins de config — resolução compartilhada entre o
|
|
9
|
+
* console (account API) e a API headless. Fail-safe: erro → defaults.
|
|
10
|
+
*/
|
|
11
|
+
export async function resolveGlobalAuthMethods(settings, cfg) {
|
|
12
|
+
const resolved = await resolveEffectiveAuthMethods(settings, {
|
|
13
|
+
configuredSocialProviders: cfg.social?.providers ?? [],
|
|
14
|
+
magicLinkCapable: cfg.passwordless?.magicLink && supportsMagicLink(cfg.accountStore),
|
|
15
|
+
passkeyCapable: supportsPasskeys(cfg.accountStore),
|
|
16
|
+
configOverrides: cfg.authMethods,
|
|
17
|
+
});
|
|
18
|
+
return { resolved, locked: configLockedAuthMethods(cfg.authMethods) };
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Monta o estado final de tipos de login para uma conta, intersectando a
|
|
22
|
+
* preferência do usuário com os métodos globais efetivos. Reusado pelo console
|
|
23
|
+
* de conta (`/account/api/login-methods`) e pela API headless
|
|
24
|
+
* (`{baseUrl}/login-methods`) — sem duplicar regra.
|
|
25
|
+
*/
|
|
26
|
+
export async function loginMethodsStateForAccount(accountId, store, settings, cfg) {
|
|
27
|
+
if (!supportsLoginMethodsPreference(store)) {
|
|
28
|
+
return { supported: false, methods: {}, available: null, locked: null };
|
|
29
|
+
}
|
|
30
|
+
const pref = normalizeUserLoginMethods(await store.getLoginMethods(accountId));
|
|
31
|
+
const { resolved: global, locked: cfgLocked } = await resolveGlobalAuthMethods(settings, cfg);
|
|
32
|
+
const effective = resolveEffectiveUserLoginMethods(global, pref);
|
|
33
|
+
return {
|
|
34
|
+
supported: true,
|
|
35
|
+
// Preferência crua do usuário; {} = sem preferência (herda globais).
|
|
36
|
+
methods: pref ?? {},
|
|
37
|
+
available: {
|
|
38
|
+
password: effective.password,
|
|
39
|
+
magicLink: effective.magicLink,
|
|
40
|
+
passkey: effective.passkey,
|
|
41
|
+
social: effective.social,
|
|
42
|
+
forgotPassword: effective.forgotPassword,
|
|
43
|
+
},
|
|
44
|
+
// Métodos que o usuário NÃO pode controlar (globalmente off ou pin de config).
|
|
45
|
+
locked: {
|
|
46
|
+
password: !global.password || cfgLocked.includes('password'),
|
|
47
|
+
magicLink: !global.magicLink || cfgLocked.includes('magicLink'),
|
|
48
|
+
passkey: !global.passkey || cfgLocked.includes('passkey'),
|
|
49
|
+
social: global.social.length === 0,
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -201,6 +201,8 @@ const C = {
|
|
|
201
201
|
apiKeys: () => import('./admin_api/api_keys_controller.js'),
|
|
202
202
|
// Account self-service JSON API (session-authed, under /account/api/*).
|
|
203
203
|
accountApi: () => import('./account_api/account_api_controller.js'),
|
|
204
|
+
// API headless (Clerk-style) — host-session-authed via `headless.resolveAccountId`.
|
|
205
|
+
headlessLoginMethods: () => import('./controllers/headless_login_methods_controller.js'),
|
|
204
206
|
};
|
|
205
207
|
/**
|
|
206
208
|
* Monta todas as rotas do host-kit do Authorization Server numa chamada.
|
|
@@ -622,6 +624,19 @@ export function registerAuthHost(router, opts = {}) {
|
|
|
622
624
|
router.get(`${apiBase}/orgs/:id`, [C.accountApi, 'showOrg']);
|
|
623
625
|
})
|
|
624
626
|
.use([accountGuard]);
|
|
627
|
+
// API headless (Clerk-style) — montada fora do `accountGuard` (não depende da
|
|
628
|
+
// sessão de CONTA), autenticada pelo resolver do host (`headless.resolveAccountId`).
|
|
629
|
+
// `PUT` usa CSRF igual à API do console (middleware de shield do host).
|
|
630
|
+
const headlessCfg = hostCfg?.headless;
|
|
631
|
+
if (headlessCfg) {
|
|
632
|
+
const hb = headlessCfg.baseUrl;
|
|
633
|
+
router
|
|
634
|
+
.get(`${hb}/login-methods`, [C.headlessLoginMethods, 'index'])
|
|
635
|
+
.as('authkit.headless.login_methods.index');
|
|
636
|
+
router
|
|
637
|
+
.put(`${hb}/login-methods`, [C.headlessLoginMethods, 'update'])
|
|
638
|
+
.as('authkit.headless.login_methods.update');
|
|
639
|
+
}
|
|
625
640
|
// Prefixos resolvidos dos consoles — `null` quando o grupo não foi montado.
|
|
626
641
|
// Compõem o `AuthHostRouteMap` devolvido no fim.
|
|
627
642
|
let resolvedAdminPrefix = null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adonis-agora/authkit-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.61.0",
|
|
4
4
|
"description": "AdonisJS OIDC/OAuth2 provider (Identity Provider) toolkit: ejectable auth server with sessions, rate-limiting, MFA/TOTP, audit log, federated logout and OpenTelemetry metrics.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "dudousxd",
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
"identity-provider",
|
|
24
24
|
"rate-limiting",
|
|
25
25
|
"mfa",
|
|
26
|
-
"totp"
|
|
26
|
+
"totp",
|
|
27
|
+
"tanstack-intent"
|
|
27
28
|
],
|
|
28
29
|
"publishConfig": {
|
|
29
30
|
"access": "public"
|
|
@@ -33,7 +34,8 @@
|
|
|
33
34
|
"types": "./build/index.d.ts",
|
|
34
35
|
"files": [
|
|
35
36
|
"build",
|
|
36
|
-
"stubs"
|
|
37
|
+
"stubs",
|
|
38
|
+
"skills"
|
|
37
39
|
],
|
|
38
40
|
"exports": {
|
|
39
41
|
".": "./build/index.js",
|
|
@@ -113,6 +115,7 @@
|
|
|
113
115
|
"devDependencies": {
|
|
114
116
|
"@adonis-agora/durable": "0.22.0",
|
|
115
117
|
"@adonis-agora/telescope": "0.6.0",
|
|
118
|
+
"@tanstack/intent": "^0.3.2",
|
|
116
119
|
"@adonisjs/ally": "6.3.0",
|
|
117
120
|
"@adonisjs/auth": "10.1.0",
|
|
118
121
|
"@adonisjs/core": "7.4.0",
|
|
@@ -150,7 +153,7 @@
|
|
|
150
153
|
"react-error-boundary": "6.1.2",
|
|
151
154
|
"nuqs": "2.9.5",
|
|
152
155
|
"recharts": "3.10.1",
|
|
153
|
-
"@adonis-agora/authkit-react": "0.20.
|
|
156
|
+
"@adonis-agora/authkit-react": "0.20.1"
|
|
154
157
|
},
|
|
155
158
|
"scripts": {
|
|
156
159
|
"build": "node scripts/build_host_css.mjs && node scripts/build_webauthn.mjs && node scripts/build_ui.mjs && node -e \"const fs=require('node:fs');for(const d of ['build/stubs','build/host/views'])fs.rmSync(d,{recursive:true,force:true})\" && tsc && node -e \"require('node:fs').cpSync('src/host/assets','build/src/host/assets',{recursive:true})\" && node -e \"require('node:fs').cpSync('stubs','build/stubs',{recursive:true,filter:(s)=>!s.endsWith('.ts')})\" && node -e \"const fs=require('node:fs');if(fs.existsSync('assets'))fs.cpSync('assets','build/assets',{recursive:true})\" && node -e \"require('node:fs').cpSync('src/host/views','build/host/views',{recursive:true})\" && node -e \"const fs=require('node:fs');fs.mkdirSync('build/host/ui',{recursive:true});fs.readdirSync('src/host/ui').filter(f=>f.endsWith('.html')).forEach(f=>fs.copyFileSync('src/host/ui/'+f,'build/host/ui/'+f))\" && node -e \"require('node:fs').copyFileSync('commands/commands.json','build/commands/commands.json')\" && node -e \"const fs=require('node:fs');fs.mkdirSync('build/src/password',{recursive:true});fs.copyFileSync('src/password/common_passwords.txt','build/src/password/common_passwords.txt')\"",
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: authkit-idp-setup
|
|
3
|
+
description: >-
|
|
4
|
+
Set up the @adonis-agora/authkit-server OIDC/OAuth2 Authorization Server (Identity
|
|
5
|
+
Provider) in an AdonisJS app. Covers `node ace add @adonis-agora/authkit-server`,
|
|
6
|
+
defineConfig({ issuer, adapter, jwks, ttl, globalRolesClaim, accountStore }) with
|
|
7
|
+
adapters.redis({ connection }) vs adapters.database({ connection? }),
|
|
8
|
+
lucidAccountStore(AuthUser), JWKS management ({ source: 'managed' | 'jwks' } or
|
|
9
|
+
'auto', keystore store file/drive/lucid/redis/hashicorp vaults plus
|
|
10
|
+
@adonis-agora/authkit-vault-aws/azure/gcp createKeystoreVault add-ons),
|
|
11
|
+
registerOidcRoutes(router, { mountPath?, metrics?, dashboard? }), mounting consoles
|
|
12
|
+
via registerAuthHost(router, { admin, adminApi, account... }) or config.routes,
|
|
13
|
+
and AUTHKIT_ISSUER / observability env. Use when booting an IdP app, wiring
|
|
14
|
+
config/authkit.ts, choosing a persistence adapter, managing signing keys, or
|
|
15
|
+
debugging 404s on /oidc, /account/*, /admin.
|
|
16
|
+
license: MIT
|
|
17
|
+
metadata:
|
|
18
|
+
type: core
|
|
19
|
+
library: "@adonis-agora/authkit-server"
|
|
20
|
+
library_version: "0.60.0"
|
|
21
|
+
framework: adonisjs
|
|
22
|
+
sources:
|
|
23
|
+
- "DavideCarvalho/adonis-authkit:README.md"
|
|
24
|
+
- "DavideCarvalho/adonis-authkit:packages/authkit-server/README.md"
|
|
25
|
+
- "DavideCarvalho/adonis-authkit:packages/authkit-server/src/define_config.ts"
|
|
26
|
+
- "DavideCarvalho/adonis-authkit:packages/authkit-server/src/register_routes.ts"
|
|
27
|
+
- "DavideCarvalho/adonis-authkit:packages/authkit-server/stubs/config/authkit.stub"
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
# Setting up the AuthKit Authorization Server
|
|
31
|
+
|
|
32
|
+
`@adonis-agora/authkit-server` turns an AdonisJS app into an OpenID Connect /
|
|
33
|
+
OAuth2 Authorization Server (IdP) on top of `oidc-provider`. Setup has three
|
|
34
|
+
independent pieces that all must exist before anything works: **config**
|
|
35
|
+
(`config/authkit.ts` via `defineConfig`), **OIDC routes** (`registerOidcRoutes`),
|
|
36
|
+
and the host routes/consoles (`registerAuthHost` or `config.routes`). The login
|
|
37
|
+
screens are a separate concern — see `authkit-interactions`.
|
|
38
|
+
|
|
39
|
+
## Setup
|
|
40
|
+
|
|
41
|
+
Install and run the configurator:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
node ace add @adonis-agora/authkit-server
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`configure` publishes `config/authkit.ts`, the model `app/models/auth_user.ts`, the
|
|
48
|
+
interaction controller stub and registers the provider. Then wire the routes:
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
// start/routes.ts
|
|
52
|
+
import router from '@adonisjs/core/services/router'
|
|
53
|
+
import { registerOidcRoutes } from '@adonis-agora/authkit-server'
|
|
54
|
+
|
|
55
|
+
registerOidcRoutes(router) // mounts the oidc-provider catch-all at /oidc
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Point `AUTHKIT_ISSUER` at `<host>/oidc` — the issuer must end with the mount path.
|
|
59
|
+
|
|
60
|
+
Source: `packages/authkit-server/README.md`, `src/register_routes.ts`.
|
|
61
|
+
|
|
62
|
+
## Core patterns
|
|
63
|
+
|
|
64
|
+
### Pattern 1 — `defineConfig`: issuer, adapter, accountStore
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
// config/authkit.ts
|
|
68
|
+
import env from '#start/env'
|
|
69
|
+
import AuthUser from '#models/auth_user'
|
|
70
|
+
import { defineConfig, adapters, lucidAccountStore } from '@adonis-agora/authkit-server'
|
|
71
|
+
|
|
72
|
+
const authServerConfig = defineConfig({
|
|
73
|
+
issuer: env.get('AUTHKIT_ISSUER'),
|
|
74
|
+
adapter: adapters.redis({ connection: 'main' }), // or adapters.database()
|
|
75
|
+
jwks: { source: 'managed', algorithm: 'RS256' },
|
|
76
|
+
ttl: { accessToken: '15m', refreshToken: '30d' },
|
|
77
|
+
globalRolesClaim: 'roles',
|
|
78
|
+
accountStore: lucidAccountStore(AuthUser),
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
export default authServerConfig
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`adapter` is where oidc-provider persists grants/tokens/codes: `adapters.redis({
|
|
85
|
+
connection })` requires `@adonisjs/redis` configured; `adapters.database({
|
|
86
|
+
connection? })` uses Lucid and requires running the `authkit_oidc_payloads`
|
|
87
|
+
migration. `accountStore` is the identity contract — `lucidAccountStore(AuthUser)`
|
|
88
|
+
derives `findAccount`/`verifyCredentials` from your user model.
|
|
89
|
+
|
|
90
|
+
Source: `stubs/config/authkit.stub`, `src/define_config.ts` (`AuthServerConfigInput`),
|
|
91
|
+
`src/adapters/factory.ts`, README § Persistência.
|
|
92
|
+
|
|
93
|
+
### Pattern 2 — signing keys: managed JWKS, `'auto'`, and inline keys
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
defineConfig({
|
|
97
|
+
issuer: env.get('AUTHKIT_ISSUER'),
|
|
98
|
+
adapter: adapters.redis({ connection: 'main' }),
|
|
99
|
+
// Ephemeral-friendly: AUTHKIT_JWKS inline JSON wins, else file-managed keystore.
|
|
100
|
+
jwks: 'auto',
|
|
101
|
+
accountStore: lucidAccountStore(AuthUser),
|
|
102
|
+
})
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Explicit forms: `{ source: 'managed' }` generates/rotates/persists the private
|
|
106
|
+
keystore (`rotationDays`, `algorithm`, `encrypt`, and `store` choosing where the
|
|
107
|
+
blob lives); `{ source: 'jwks', keys }` supplies keys inline.
|
|
108
|
+
|
|
109
|
+
The managed keystore's `store` accepts a path shortcut (`store: 'tmp/keystore.json'`)
|
|
110
|
+
or `{ driver: ... }`: built-in vaults are `file`, `drive` (a `@adonisjs/drive` disk),
|
|
111
|
+
`lucid` (table created on first write), `redis`, and `hashicorp`; for cloud secret
|
|
112
|
+
managers install the add-ons `@adonis-agora/authkit-vault-aws` / `-azure` / `-gcp`
|
|
113
|
+
and pass their `createKeystoreVault(...)` result as `store` (any object with a
|
|
114
|
+
`read()` method is accepted). With no `store` at all, keys are ephemeral per boot.
|
|
115
|
+
|
|
116
|
+
Rotation tooling ships as `node ace authkit:rotate-keys`; health checks via
|
|
117
|
+
`node ace authkit:doctor`.
|
|
118
|
+
|
|
119
|
+
Source: `src/keys/keystore_manager.ts` (`resolveKeystoreVault`),
|
|
120
|
+
`packages/authkit-core/src/types/server_config.ts` (`JwksConfig`), index.ts exports.
|
|
121
|
+
|
|
122
|
+
### Pattern 3 — mounting consoles with `registerAuthHost` (or `config.routes`)
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
// start/routes.ts
|
|
126
|
+
import router from '@adonisjs/core/services/router'
|
|
127
|
+
import { registerOidcRoutes, registerAuthHost } from '@adonis-agora/authkit-server'
|
|
128
|
+
|
|
129
|
+
registerOidcRoutes(router)
|
|
130
|
+
const authkitRoutes = registerAuthHost(router, {
|
|
131
|
+
mountPath: '/oidc',
|
|
132
|
+
admin: true, // admin console under /admin
|
|
133
|
+
adminApi: true, // Admin REST API under /api/authkit/v1
|
|
134
|
+
})
|
|
135
|
+
// Hand authkitRoutes to your frontend instead of hardcoding hrefs.
|
|
136
|
+
router.get('/', async ({ inertia }) => inertia.render('home', { authkitRoutes }))
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Alternatively set `routes: true` in `config/authkit.ts` to auto-mount in the
|
|
140
|
+
provider's `boot()` (before `start/routes.ts`) — then do NOT also call
|
|
141
|
+
`registerAuthHost` manually (double registration crashes the boot). Policy flags
|
|
142
|
+
(`admin`, `adminApi`, `social`, `rateLimit`) declared in `defineConfig` LOCK the
|
|
143
|
+
on/off switch: `registerAuthHost` may only adjust structural prefixes.
|
|
144
|
+
|
|
145
|
+
Observability routes are opt-in on the OIDC mount itself:
|
|
146
|
+
`registerOidcRoutes(router, { metrics: true, dashboard: true })` mounts
|
|
147
|
+
`GET /authkit/metrics` (JSON snapshot) and `GET /authkit/dashboard` (embedded HTML).
|
|
148
|
+
|
|
149
|
+
Source: `src/host/register_auth_host.ts` (`AuthHostOptions`, policy-vs-structural),
|
|
150
|
+
`src/define_config.ts` (`routes`, `admin`, `adminApi` JSDoc), server README
|
|
151
|
+
§ Observabilidade.
|
|
152
|
+
|
|
153
|
+
## Common mistakes
|
|
154
|
+
|
|
155
|
+
### CRITICAL — Issuer not ending with the mount path
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
// Wrong — issuer says /oauth but routes mount at default /oidc
|
|
159
|
+
defineConfig({ issuer: 'https://idp.example.com/oauth' })
|
|
160
|
+
registerOidcRoutes(router) // mounts /oidc/*
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
// Correct — issuer ends exactly at the mount path
|
|
165
|
+
defineConfig({ issuer: 'https://idp.example.com/oidc' })
|
|
166
|
+
registerOidcRoutes(router) // default mountPath '/oidc' matches the issuer
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
oidc-provider routes internally UNDER the issuer URL, so a trailing mismatch makes
|
|
170
|
+
discovery and every protocol endpoint unreachable while the app otherwise boots fine.
|
|
171
|
+
|
|
172
|
+
Source: `packages/authkit-server/README.md` § Montar as rotas OIDC;
|
|
173
|
+
`src/host/register_auth_host.ts` (`mountPath` doc: "Deve casar com o final do issuer").
|
|
174
|
+
|
|
175
|
+
### HIGH — Consoles/account screens 404 because no host routes were mounted
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
// Wrong — only the protocol endpoints exist; /account/* and /admin are dead
|
|
179
|
+
registerOidcRoutes(router)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
// Correct — mount the host surface too (or set config.routes: true)
|
|
184
|
+
registerOidcRoutes(router)
|
|
185
|
+
registerAuthHost(router, { mountPath: '/oidc', admin: true })
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
`registerOidcRoutes` only registers the oidc-provider catch-all; everything else
|
|
189
|
+
(account console `/account/*`, admin console, Admin API, sudo routes) exists solely
|
|
190
|
+
after `registerAuthHost` runs or `config.routes` auto-mounts them. Conversely,
|
|
191
|
+
calling `registerAuthHost` while `config.routes: true` double-registers named routes
|
|
192
|
+
and throws at boot — pick one.
|
|
193
|
+
|
|
194
|
+
Source: `src/define_config.ts` (`routes` JSDoc: "duplo registro ... derrubam o boot"),
|
|
195
|
+
`src/register_routes.ts`.
|
|
196
|
+
|
|
197
|
+
### HIGH — `jwks: 'auto'` in production without AUTHKIT_JWKS
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
// Wrong — prod deploy with no AUTHKIT_JWKS env: falls back to a file keystore in tmp/
|
|
201
|
+
jwks: 'auto'
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
// Correct — explicit managed keystore persisted in a durable vault for prod
|
|
206
|
+
jwks: {
|
|
207
|
+
source: 'managed',
|
|
208
|
+
algorithm: 'RS256',
|
|
209
|
+
store: { driver: 'lucid' }, // or redis/drive/hashicorp/vault add-on
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
`'auto'` prefers the `AUTHKIT_JWKS` env JSON but silently degrades to
|
|
214
|
+
`tmp/authkit_jwks.json` on disk — on ephemeral filesystems every restart mints new
|
|
215
|
+
signing keys and every issued token fails validation after redeploy.
|
|
216
|
+
|
|
217
|
+
Source: `src/define_config.ts` (`jwks` JSDoc: "'auto' (recomendado p/ deploys
|
|
218
|
+
efêmeros)... senão cai no managed persistido em arquivo"),
|
|
219
|
+
`packages/authkit-core/src/types/server_config.ts`.
|
|
220
|
+
|
|
221
|
+
See also: `authkit-interactions/SKILL.md` — after setup, the authorization flow ends
|
|
222
|
+
at the interaction screens, which the host must register and render itself.
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: authkit-interactions
|
|
3
|
+
description: >-
|
|
4
|
+
Implement the host-owned login/consent interaction screens of @adonis-agora/authkit-server.
|
|
5
|
+
Covers oidc-provider's interactions.url redirect to /auth/interaction/:uid, the three
|
|
6
|
+
routes every IdP app must register (show/login/consent on AuthInteractionController),
|
|
7
|
+
`node ace configure --ui=edge|react|headless` presets, the shell-controller +
|
|
8
|
+
service.interactions split (details(ctx), login(ctx,{email,password}), consent(ctx)),
|
|
9
|
+
overriding verifyCredentials in config/authkit.ts, renderers edgeRenderer/inertiaRenderer,
|
|
10
|
+
and end-to-end testing with @adonis-agora/authkit-testing (createTestIdentity,
|
|
11
|
+
mintTestIdToken, serveJwks, fakeAuthenticator). Use when the authorization flow 404s at
|
|
12
|
+
the login screen, wiring custom login UI, plugging an external user base, or testing
|
|
13
|
+
OIDC flows without booting an IdP.
|
|
14
|
+
license: MIT
|
|
15
|
+
metadata:
|
|
16
|
+
type: core
|
|
17
|
+
library: "@adonis-agora/authkit-server"
|
|
18
|
+
library_version: "0.60.0"
|
|
19
|
+
framework: adonisjs
|
|
20
|
+
sources:
|
|
21
|
+
- "DavideCarvalho/adonis-authkit:packages/authkit-server/README.md"
|
|
22
|
+
- "DavideCarvalho/adonis-authkit:packages/authkit-server/src/host/renderers/inertia_renderer.ts"
|
|
23
|
+
- "DavideCarvalho/adonis-authkit:packages/authkit-server/src/define_config.ts"
|
|
24
|
+
- "DavideCarvalho/adonis-authkit:packages/authkit-testing/README.md"
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
# Login & consent screens (interactions)
|
|
28
|
+
|
|
29
|
+
When `oidc-provider` meets an unauthenticated user it redirects to
|
|
30
|
+
`interactions.url` (`/auth/interaction/:uid`). Those screens are **yours** — the kit
|
|
31
|
+
ejects a controller shell via `node ace configure` and you register the routes that
|
|
32
|
+
point at it. Without them the authorization code flow dies with a 404 exactly when
|
|
33
|
+
the user should log in.
|
|
34
|
+
|
|
35
|
+
## Setup
|
|
36
|
+
|
|
37
|
+
Pick a UI preset when configuring (asks interactively if omitted):
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
node ace configure @adonis-agora/authkit-server --ui=edge
|
|
41
|
+
# values: edge | react | headless
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Each preset publishes `app/controllers/auth_interaction_controller.ts`; `edge` adds
|
|
45
|
+
Edge views, `react` adds Inertia pages (validating that `@adonisjs/inertia` + Vite +
|
|
46
|
+
React exist first), `headless` returns JSON only. Register the three routes:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
// start/routes.ts
|
|
50
|
+
import router from '@adonisjs/core/services/router'
|
|
51
|
+
import AuthInteractionController from '#controllers/auth_interaction_controller'
|
|
52
|
+
|
|
53
|
+
router.get('/auth/interaction/:uid', [AuthInteractionController, 'show'])
|
|
54
|
+
router.post('/auth/interaction/:uid/login', [AuthInteractionController, 'login'])
|
|
55
|
+
router.post('/auth/interaction/:uid/consent', [AuthInteractionController, 'consent'])
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Source: `packages/authkit-server/README.md` § Rotas de interaction.
|
|
59
|
+
|
|
60
|
+
## Core patterns
|
|
61
|
+
|
|
62
|
+
### Pattern 1 — edit the shell, keep logic in `service.interactions`
|
|
63
|
+
|
|
64
|
+
In all presets the ejected controller is a thin shell: the logic lives in
|
|
65
|
+
`service.interactions`, resolved via `containerResolver.make('authkit.server')`.
|
|
66
|
+
It exposes `details(ctx)` (the prompt + params), `login(ctx, { email, password })`
|
|
67
|
+
and `consent(ctx)`. You only edit the render/redirect parts:
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
// headless preset flavor — show() returns JSON, you build your own front
|
|
71
|
+
async show({ request, response }) {
|
|
72
|
+
const service = await this.ctx.containerResolver.make('authkit.server')
|
|
73
|
+
const details = await service.interactions.details(this.ctx)
|
|
74
|
+
return response.json({ uid: request.param('uid'), prompt: details.prompt, params: details.params })
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Keep validation, MFA prompts and grant bookkeeping inside `service.interactions`;
|
|
79
|
+
the shell only translates between HTTP and those calls.
|
|
80
|
+
|
|
81
|
+
Source: `packages/authkit-server/README.md` § UI de login/consent ("o controller
|
|
82
|
+
ejetado é casca: a lógica vive em `service.interactions`").
|
|
83
|
+
|
|
84
|
+
### Pattern 2 — plug your user base via `verifyCredentials`
|
|
85
|
+
|
|
86
|
+
`verifyCredentials` in `config/authkit.ts` decides whether credentials are valid;
|
|
87
|
+
`service.interactions.login` calls it. The default queries the `AuthUser` model by
|
|
88
|
+
email and uses `verifyPassword` — override to authenticate against anything else:
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
// config/authkit.ts
|
|
92
|
+
defineConfig({
|
|
93
|
+
issuer: env.get('AUTHKIT_ISSUER'),
|
|
94
|
+
adapter: adapters.redis({ connection: 'main' }),
|
|
95
|
+
accountStore: lucidAccountStore(AuthUser),
|
|
96
|
+
verifyCredentials: async (email, password) => {
|
|
97
|
+
// Return the account on success; throw/falsy paths fail the login.
|
|
98
|
+
const account = await AuthUser.query().where('email', email).first()
|
|
99
|
+
if (!account) throw new Error('Invalid credentials')
|
|
100
|
+
await verifyPassword(account.passwordHash, password)
|
|
101
|
+
return account
|
|
102
|
+
},
|
|
103
|
+
})
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Whatever it returns must be the same identity surface the configured
|
|
107
|
+
`accountStore` serves, or downstream `findAccount` lookups diverge from what just
|
|
108
|
+
logged in.
|
|
109
|
+
|
|
110
|
+
Source: `packages/authkit-server/README.md` § UI de login/consent (verifyCredentials),
|
|
111
|
+
`src/define_config.ts` (`accountStore` derives findAccount/verifyCredentials).
|
|
112
|
+
|
|
113
|
+
### Pattern 3 — custom rendering with `inertiaRenderer`
|
|
114
|
+
|
|
115
|
+
Hosts building their own React screens set the `render` option instead of relying
|
|
116
|
+
on the default Edge renderer:
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
import { defineConfig, inertiaRenderer } from '@adonis-agora/authkit-server'
|
|
120
|
+
|
|
121
|
+
defineConfig({
|
|
122
|
+
issuer: env.get('AUTHKIT_ISSUER'),
|
|
123
|
+
adapter: adapters.database(),
|
|
124
|
+
accountStore: lucidAccountStore(AuthUser),
|
|
125
|
+
render: inertiaRenderer(), // renders Inertia pages for /account/* and /auth/interaction/*
|
|
126
|
+
})
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Without any renderer, every `/account/*` and `/auth/interaction/*` request fails
|
|
130
|
+
with an unexplained 500 (`render` is undefined).
|
|
131
|
+
|
|
132
|
+
Source: `src/define_config.ts` (`render` JSDoc), `index.ts` exports
|
|
133
|
+
(`inertiaRenderer`, `edgeRenderer`).
|
|
134
|
+
|
|
135
|
+
### Pattern 4 — test flows without booting an IdP
|
|
136
|
+
|
|
137
|
+
`@adonis-agora/authkit-testing` mints real signed ID tokens validated by a local
|
|
138
|
+
JWKS, and fakes the authenticator for controller tests:
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
import { mintTestIdToken, serveJwks, fakeAuthenticator } from '@adonis-agora/authkit-testing'
|
|
142
|
+
import { resolvers } from '@adonis-agora/authkit-client'
|
|
143
|
+
|
|
144
|
+
const { token, jwks } = await mintTestIdToken({
|
|
145
|
+
issuer: 'https://idp.test',
|
|
146
|
+
clientId: 'my-app',
|
|
147
|
+
claims: { sub: 'user-42', email: 'jane@test.dev', roles: ['ADMIN'] },
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
const served = await serveJwks(jwks)
|
|
151
|
+
const factory = resolvers.jwt({ jwksUri: served.jwksUri })
|
|
152
|
+
const resolver = await factory.resolver({
|
|
153
|
+
issuer: 'https://idp.test',
|
|
154
|
+
clientId: 'my-app',
|
|
155
|
+
sessionKey: 'authkit',
|
|
156
|
+
globalRolesClaim: 'roles',
|
|
157
|
+
})
|
|
158
|
+
const ctx = { auth: fakeAuthenticator({ identity: null }) } // anonymous request fake
|
|
159
|
+
await served.close()
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Also available: `createTestIdentity(overrides?)` for valid `Identity` defaults,
|
|
163
|
+
`fakeAccountStore({ withMfa: true, ... })` for capability-probed store fakes.
|
|
164
|
+
|
|
165
|
+
Source: `packages/authkit-testing/README.md`.
|
|
166
|
+
|
|
167
|
+
## Common mistakes
|
|
168
|
+
|
|
169
|
+
### CRITICAL — Forgetting to register the interaction routes
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
// Wrong — only the protocol endpoints exist
|
|
173
|
+
registerOidcRoutes(router)
|
|
174
|
+
// no GET /auth/interaction/:uid anywhere
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
// Correct — the three host-owned routes are registered next to the OIDC mount
|
|
179
|
+
registerOidcRoutes(router)
|
|
180
|
+
router.get('/auth/interaction/:uid', [AuthInteractionController, 'show'])
|
|
181
|
+
router.post('/auth/interaction/:uid/login', [AuthInteractionController, 'login'])
|
|
182
|
+
router.post('/auth/interaction/:uid/consent', [AuthInteractionController, 'consent'])
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Nothing crashes at boot; the failure surfaces mid-flow — the user's browser lands
|
|
186
|
+
on `/auth/interaction/:uid` and gets a 404, so no client can ever complete login.
|
|
187
|
+
|
|
188
|
+
Source: `packages/authkit-server/README.md` § Rotas de interaction ("Sem essas rotas
|
|
189
|
+
o fluxo de autorização cai num 404 ao chegar na tela de login").
|
|
190
|
+
|
|
191
|
+
### HIGH — Writing login/consent logic inside the ejected controller shell
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
// Wrong — reimplementing prompt handling/grants in the ejected shell
|
|
195
|
+
async consent(ctx) {
|
|
196
|
+
// hand-rolling grant persistence against oidc-provider internals...
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
// Correct — shell delegates; the service owns the flow
|
|
202
|
+
async consent(ctx) {
|
|
203
|
+
const service = await ctx.containerResolver.make('authkit.server')
|
|
204
|
+
await service.interactions.consent(ctx) // handles the grant, returns the redirect
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
The shell is regenerated by `configure` and has no access to the provider's
|
|
209
|
+
interaction plumbing; duplicating logic there silently drifts from prompt/consent
|
|
210
|
+
semantics the provider expects.
|
|
211
|
+
|
|
212
|
+
Source: `packages/authkit-server/README.md` § UI de login/consent ("Você edita só a
|
|
213
|
+
parte de render/redirect").
|
|
214
|
+
|
|
215
|
+
### MEDIUM — Choosing `--ui=react` without the Inertia stack installed
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
# Wrong — react preset in a bare API-only AdonisJS app
|
|
219
|
+
node ace configure @adonis-agora/authkit-server --ui=react
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
# Correct — match the preset to the app stack
|
|
224
|
+
node ace configure @adonis-agora/authkit-server --ui=edge # server-rendered views
|
|
225
|
+
node ace configure @adonis-agora/authkit-server --ui=headless # build your own front
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
The `react` preset requires `@adonisjs/inertia` + Vite + React in the host app —
|
|
229
|
+
`configure` validates the stack before publishing, so the command aborts instead of
|
|
230
|
+
half-configuring your app.
|
|
231
|
+
|
|
232
|
+
Source: `packages/authkit-server/README.md` § UI de login/consent ("Exige
|
|
233
|
+
@adonisjs/inertia + Vite + React no app — o configure valida essa stack").
|
|
234
|
+
|
|
235
|
+
See also: `authkit-rp-client/SKILL.md` — the consuming side that ends up holding
|
|
236
|
+
the session these screens create.
|