@adonis-agora/authkit-server 0.59.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/index.d.ts +4 -2
- package/build/index.js +3 -1
- package/build/providers/authkit_server_provider.js +7 -0
- package/build/src/accounts/account_store.d.ts +20 -1
- package/build/src/accounts/account_store.js +4 -0
- package/build/src/accounts/lucid_account_store.js +4 -0
- package/build/src/accounts/lucid_store/login_methods.d.ts +11 -0
- package/build/src/accounts/lucid_store/login_methods.js +32 -0
- package/build/src/define_config.d.ts +23 -0
- package/build/src/define_config.js +6 -0
- package/build/src/host/account_api/account_api_controller.d.ts +38 -0
- package/build/src/host/account_api/account_api_controller.js +84 -3
- 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/controllers/interaction_controller.js +159 -12
- package/build/src/host/controllers/social_controller.js +29 -1
- 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 +18 -0
- package/build/src/host/ui-dist/assets/{index-6cE5JMyP.js → index-Cb38N16r.js} +1 -1
- package/build/src/host/ui-dist/index.html +1 -1
- package/build/src/host/user_login_methods.d.ts +94 -0
- package/build/src/host/user_login_methods.js +132 -0
- package/package.json +7 -4
- package/skills/authkit-idp-setup/SKILL.md +222 -0
- package/skills/authkit-interactions/SKILL.md +236 -0
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
9
9
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
10
10
|
<link href="https://fonts.googleapis.com/css2?family=Sora:wght@400;500;600;700&family=JetBrains+Mono:ital,wght@0,400;0,500;0,600;1,400&display=swap" rel="stylesheet">
|
|
11
|
-
<script type="module" crossorigin src="/__AUTHKIT_BASE__/assets/index-
|
|
11
|
+
<script type="module" crossorigin src="/__AUTHKIT_BASE__/assets/index-Cb38N16r.js"></script>
|
|
12
12
|
<link rel="stylesheet" crossorigin href="/__AUTHKIT_BASE__/assets/index-DTSmD4RU.css">
|
|
13
13
|
</head>
|
|
14
14
|
<body>
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tipos de login POR USUÁRIO — preferência self-service do dono da conta.
|
|
3
|
+
*
|
|
4
|
+
* Diferente do runtime setting global `auth_methods` (que o operador controla via
|
|
5
|
+
* console admin), aqui é o PRÓPRIO usuário quem decide, no console de conta,
|
|
6
|
+
* quais métodos de login ficam habilitados para a SUA conta. A preferência é
|
|
7
|
+
* persistida na coluna `login_methods` (JSONB) de `auth.users` e SEMPRE se
|
|
8
|
+
* intersecta com os métodos globais efetivos: o usuário nunca pode LIGAR um
|
|
9
|
+
* método que o host desligou (config pin ou setting), só restringir.
|
|
10
|
+
*
|
|
11
|
+
* Contratos:
|
|
12
|
+
* - Coluna ausente no model → capacidade AUSENTE → feature no-op (mesmo padrão
|
|
13
|
+
* das demais colunas opcionais; hosts adotam por migração própria).
|
|
14
|
+
* - `NULL` na coluna = sem preferência = herda os globais (comportamento atual).
|
|
15
|
+
* - Fail-safe all-off: uma preferência que zere TODOS os métodos disponíveis
|
|
16
|
+
* deixa de restringir (nunca trancamos o usuário fora da própria conta).
|
|
17
|
+
*/
|
|
18
|
+
/** Chaves de método que o usuário pode ligar/desligar para a própria conta. */
|
|
19
|
+
export type UserLoginMethodKey = 'password' | 'magicLink' | 'passkey' | 'social';
|
|
20
|
+
export declare const USER_LOGIN_METHOD_KEYS: readonly UserLoginMethodKey[];
|
|
21
|
+
/**
|
|
22
|
+
* Shape persistido na coluna `login_methods` (JSONB) de `auth.users`.
|
|
23
|
+
* Campos ausentes = herda o global. Campos presentes = ON/OFF explícito.
|
|
24
|
+
*/
|
|
25
|
+
export interface UserLoginMethods {
|
|
26
|
+
password?: boolean;
|
|
27
|
+
magicLink?: boolean;
|
|
28
|
+
passkey?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* O método social como um todo (não por provider — a interseção com os
|
|
31
|
+
* providers configurados já acontece no resolver global).
|
|
32
|
+
*/
|
|
33
|
+
social?: boolean;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Preferência NORMALIZADA pronta para consumo: cada campo presente indica o que
|
|
37
|
+
* o usuário escolheu; ausente = herda o global. É o que o store devolve/gravа.
|
|
38
|
+
*/
|
|
39
|
+
export type NormalizedUserLoginMethods = UserLoginMethods;
|
|
40
|
+
/**
|
|
41
|
+
* Resultado da interseção global × por-usuário, pronto para as telas/POSTs.
|
|
42
|
+
* Espelha {@link ResolvedAuthMethods} (global) mas já filtrado pela preferência.
|
|
43
|
+
*/
|
|
44
|
+
export interface ResolvedUserLoginMethods {
|
|
45
|
+
password: boolean;
|
|
46
|
+
magicLink: boolean;
|
|
47
|
+
passkey: boolean;
|
|
48
|
+
/** Providers sociais finais (global ∩ preferência). */
|
|
49
|
+
social: string[];
|
|
50
|
+
forgotPassword: boolean;
|
|
51
|
+
passkeyAutofill: boolean;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Normaliza um valor cru vindo do DB (JSONB) ou do request body para o shape
|
|
55
|
+
* canônico. Campos inválidos são descartados silenciosamente (fail-safe).
|
|
56
|
+
* Objetos vazios viram null (sem preferência).
|
|
57
|
+
*/
|
|
58
|
+
export declare function normalizeUserLoginMethods(raw: unknown): NormalizedUserLoginMethods | null;
|
|
59
|
+
/**
|
|
60
|
+
* Valida e normaliza o payload de update vindo do PUT da account API.
|
|
61
|
+
* Espera `{ methods: {...} }`; só aceita chaves conhecidas com valores booleanos.
|
|
62
|
+
* `{ methods: {} }` = limpar a preferência (voltar a herdar os globais).
|
|
63
|
+
*/
|
|
64
|
+
export declare function parseUserLoginMethodsPayload(body: unknown): ParsedUserLoginMethodsPayload;
|
|
65
|
+
export type ParsedUserLoginMethodsPayload = {
|
|
66
|
+
ok: true;
|
|
67
|
+
value: UserLoginMethods;
|
|
68
|
+
} | {
|
|
69
|
+
ok: false;
|
|
70
|
+
error: string;
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Interseção dos métodos globais efetivos com a preferência do usuário.
|
|
74
|
+
*
|
|
75
|
+
* Regras:
|
|
76
|
+
* - `pref` null/vazio → globais inalterados (herança).
|
|
77
|
+
* - Campo `false` na preferência DESLIGA o método (só pode restringir).
|
|
78
|
+
* - Campo `true` NUNCA liga método globalmente indisponível (interseção).
|
|
79
|
+
* - FAIL-SAFE all-off: se a preferência zerar todos os métodos disponíveis,
|
|
80
|
+
* volta aos globais (nunca deixar a conta sem nenhum método de entrada).
|
|
81
|
+
* Loga um aviso (console.warn) — sinaliza preferência órfã após o host ter
|
|
82
|
+
* desligado métodos globalmente.
|
|
83
|
+
* - Qualquer erro → globais (a chamada nunca deve derrubar o login).
|
|
84
|
+
*/
|
|
85
|
+
export declare function resolveEffectiveUserLoginMethods(global: ResolvedAuthMethodsLike, pref: UserLoginMethods | null | undefined): ResolvedUserLoginMethods;
|
|
86
|
+
/** Mínimo que o resolver precisa dos métodos globais (evita import circular). */
|
|
87
|
+
export interface ResolvedAuthMethodsLike {
|
|
88
|
+
password: boolean;
|
|
89
|
+
magicLink: boolean;
|
|
90
|
+
passkey: boolean;
|
|
91
|
+
social: string[];
|
|
92
|
+
forgotPassword: boolean;
|
|
93
|
+
passkeyAutofill: boolean;
|
|
94
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tipos de login POR USUÁRIO — preferência self-service do dono da conta.
|
|
3
|
+
*
|
|
4
|
+
* Diferente do runtime setting global `auth_methods` (que o operador controla via
|
|
5
|
+
* console admin), aqui é o PRÓPRIO usuário quem decide, no console de conta,
|
|
6
|
+
* quais métodos de login ficam habilitados para a SUA conta. A preferência é
|
|
7
|
+
* persistida na coluna `login_methods` (JSONB) de `auth.users` e SEMPRE se
|
|
8
|
+
* intersecta com os métodos globais efetivos: o usuário nunca pode LIGAR um
|
|
9
|
+
* método que o host desligou (config pin ou setting), só restringir.
|
|
10
|
+
*
|
|
11
|
+
* Contratos:
|
|
12
|
+
* - Coluna ausente no model → capacidade AUSENTE → feature no-op (mesmo padrão
|
|
13
|
+
* das demais colunas opcionais; hosts adotam por migração própria).
|
|
14
|
+
* - `NULL` na coluna = sem preferência = herda os globais (comportamento atual).
|
|
15
|
+
* - Fail-safe all-off: uma preferência que zere TODOS os métodos disponíveis
|
|
16
|
+
* deixa de restringir (nunca trancamos o usuário fora da própria conta).
|
|
17
|
+
*/
|
|
18
|
+
export const USER_LOGIN_METHOD_KEYS = [
|
|
19
|
+
'password',
|
|
20
|
+
'magicLink',
|
|
21
|
+
'passkey',
|
|
22
|
+
'social',
|
|
23
|
+
];
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Normalização / validação
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
function boolOrUndefined(v) {
|
|
28
|
+
return typeof v === 'boolean' ? v : undefined;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Normaliza um valor cru vindo do DB (JSONB) ou do request body para o shape
|
|
32
|
+
* canônico. Campos inválidos são descartados silenciosamente (fail-safe).
|
|
33
|
+
* Objetos vazios viram null (sem preferência).
|
|
34
|
+
*/
|
|
35
|
+
export function normalizeUserLoginMethods(raw) {
|
|
36
|
+
if (raw === null || raw === undefined)
|
|
37
|
+
return null;
|
|
38
|
+
if (typeof raw !== 'object' || Array.isArray(raw))
|
|
39
|
+
return null;
|
|
40
|
+
const r = raw;
|
|
41
|
+
const out = {};
|
|
42
|
+
for (const key of USER_LOGIN_METHOD_KEYS) {
|
|
43
|
+
const v = boolOrUndefined(r[key]);
|
|
44
|
+
if (v !== undefined)
|
|
45
|
+
out[key] = v;
|
|
46
|
+
}
|
|
47
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Valida e normaliza o payload de update vindo do PUT da account API.
|
|
51
|
+
* Espera `{ methods: {...} }`; só aceita chaves conhecidas com valores booleanos.
|
|
52
|
+
* `{ methods: {} }` = limpar a preferência (voltar a herdar os globais).
|
|
53
|
+
*/
|
|
54
|
+
export function parseUserLoginMethodsPayload(body) {
|
|
55
|
+
if (typeof body !== 'object' || body === null || Array.isArray(body)) {
|
|
56
|
+
return { ok: false, error: 'invalid_body' };
|
|
57
|
+
}
|
|
58
|
+
const b = body;
|
|
59
|
+
if (!('methods' in b) || typeof b.methods !== 'object' || b.methods === null) {
|
|
60
|
+
return { ok: false, error: 'invalid_methods' };
|
|
61
|
+
}
|
|
62
|
+
const methods = normalizeUserLoginMethods(b.methods);
|
|
63
|
+
// Objeto com só campos inválidos → trata como reset ({}).
|
|
64
|
+
return { ok: true, value: methods ?? {} };
|
|
65
|
+
}
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Resolver puro — interseção global × por-usuário
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
/**
|
|
70
|
+
* Interseção dos métodos globais efetivos com a preferência do usuário.
|
|
71
|
+
*
|
|
72
|
+
* Regras:
|
|
73
|
+
* - `pref` null/vazio → globais inalterados (herança).
|
|
74
|
+
* - Campo `false` na preferência DESLIGA o método (só pode restringir).
|
|
75
|
+
* - Campo `true` NUNCA liga método globalmente indisponível (interseção).
|
|
76
|
+
* - FAIL-SAFE all-off: se a preferência zerar todos os métodos disponíveis,
|
|
77
|
+
* volta aos globais (nunca deixar a conta sem nenhum método de entrada).
|
|
78
|
+
* Loga um aviso (console.warn) — sinaliza preferência órfã após o host ter
|
|
79
|
+
* desligado métodos globalmente.
|
|
80
|
+
* - Qualquer erro → globais (a chamada nunca deve derrubar o login).
|
|
81
|
+
*/
|
|
82
|
+
export function resolveEffectiveUserLoginMethods(global, pref) {
|
|
83
|
+
try {
|
|
84
|
+
const p = normalizeUserLoginMethods(pref);
|
|
85
|
+
if (!p) {
|
|
86
|
+
return passthrough(global);
|
|
87
|
+
}
|
|
88
|
+
const password = applyPref(global.password, p.password);
|
|
89
|
+
const magicLink = applyPref(global.magicLink, p.magicLink);
|
|
90
|
+
const passkey = applyPref(global.passkey, p.passkey);
|
|
91
|
+
const social = p.social === false ? [] : global.social;
|
|
92
|
+
const resolved = {
|
|
93
|
+
password,
|
|
94
|
+
magicLink,
|
|
95
|
+
passkey,
|
|
96
|
+
social,
|
|
97
|
+
// forgotPassword é derivado do password (mesma derivação do global).
|
|
98
|
+
forgotPassword: password && global.forgotPassword,
|
|
99
|
+
passkeyAutofill: passkey && global.passkeyAutofill,
|
|
100
|
+
};
|
|
101
|
+
const allOff = !resolved.password &&
|
|
102
|
+
!resolved.magicLink &&
|
|
103
|
+
!resolved.passkey &&
|
|
104
|
+
resolved.social.length === 0;
|
|
105
|
+
if (allOff) {
|
|
106
|
+
console.warn('[authkit] user login_methods zerou todos os métodos — ignorando a preferência (fail-safe).');
|
|
107
|
+
return passthrough(global);
|
|
108
|
+
}
|
|
109
|
+
return resolved;
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
// Nunca derruba o login por causa da preferência.
|
|
113
|
+
return passthrough(global);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/** Interseção: false do usuário desliga; true não liga o que o global não tem. */
|
|
117
|
+
function applyPref(globalValue, prefValue) {
|
|
118
|
+
if (prefValue === false)
|
|
119
|
+
return false;
|
|
120
|
+
return globalValue;
|
|
121
|
+
}
|
|
122
|
+
/** Cópia dos métodos globais como resultado por-usuário (sem preferência). */
|
|
123
|
+
function passthrough(global) {
|
|
124
|
+
return {
|
|
125
|
+
password: global.password,
|
|
126
|
+
magicLink: global.magicLink,
|
|
127
|
+
passkey: global.passkey,
|
|
128
|
+
social: [...(global.social ?? [])],
|
|
129
|
+
forgotPassword: global.forgotPassword,
|
|
130
|
+
passkeyAutofill: global.passkeyAutofill,
|
|
131
|
+
};
|
|
132
|
+
}
|
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.
|
|
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.
|