@adonis-agora/authkit-server 0.75.0 → 0.77.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/README.md +148 -0
- package/build/host/views/account/apps.edge +30 -0
- package/build/host/views/agents/consent.edge +60 -0
- package/build/host/views/agents/done.edge +24 -0
- package/build/host/views/consent.edge +9 -1
- package/build/host/views/mfa-challenge.edge +42 -3
- package/build/host/views/partials/styles.edge +1 -1
- package/build/index.d.ts +18 -1
- package/build/index.js +9 -1
- package/build/providers/authkit_server_provider.js +75 -50
- package/build/src/accounts/account_store.d.ts +2 -0
- package/build/src/accounts/lucid_account_store.js +2 -0
- package/build/src/adapters/adapter_contract.d.ts +2 -0
- package/build/src/adapters/database_adapter.d.ts +1 -0
- package/build/src/adapters/database_adapter.js +26 -0
- package/build/src/adapters/redis_adapter.d.ts +1 -0
- package/build/src/adapters/redis_adapter.js +30 -0
- package/build/src/agents/agent_identity.d.ts +33 -0
- package/build/src/agents/agent_identity.js +156 -0
- package/build/src/agents/config.d.ts +99 -0
- package/build/src/agents/config.js +101 -0
- package/build/src/agents/delegation_service.d.ts +154 -0
- package/build/src/agents/delegation_service.js +394 -0
- package/build/src/agents/delegation_store.d.ts +93 -0
- package/build/src/agents/delegation_store.js +222 -0
- package/build/src/agents/middleware.d.ts +73 -0
- package/build/src/agents/middleware.js +113 -0
- package/build/src/agents/protocol.d.ts +62 -0
- package/build/src/agents/protocol.js +51 -0
- package/build/src/agents/runtime.d.ts +36 -0
- package/build/src/agents/runtime.js +65 -0
- package/build/src/agents/signer.d.ts +34 -0
- package/build/src/agents/signer.js +70 -0
- package/build/src/audit/audit_sink.d.ts +1 -1
- package/build/src/audit/audit_sink.js +5 -0
- package/build/src/controllers/authorization_server_metadata_controller.d.ts +10 -0
- package/build/src/controllers/authorization_server_metadata_controller.js +20 -0
- package/build/src/define_config.d.ts +50 -3
- package/build/src/define_config.js +42 -1
- package/build/src/host/account_api/account_api_controller.d.ts +12 -0
- package/build/src/host/account_api/account_api_controller.js +40 -0
- package/build/src/host/admin_api/dto.d.ts +1 -1
- package/build/src/host/admin_sessions_service.d.ts +2 -0
- package/build/src/host/admin_sessions_service.js +18 -0
- package/build/src/host/auth_host_config.d.ts +4 -0
- package/build/src/host/client_names.d.ts +11 -0
- package/build/src/host/client_names.js +12 -0
- package/build/src/host/controllers/account_apps_controller.d.ts +2 -0
- package/build/src/host/controllers/account_apps_controller.js +39 -3
- package/build/src/host/controllers/account_orgs_controller.js +2 -1
- package/build/src/host/controllers/account_security_controller.js +8 -4
- package/build/src/host/controllers/account_session_controller.js +2 -1
- package/build/src/host/controllers/agent_consent_controller.d.ts +17 -0
- package/build/src/host/controllers/agent_consent_controller.js +138 -0
- package/build/src/host/controllers/agent_oauth_controller.d.ts +23 -0
- package/build/src/host/controllers/agent_oauth_controller.js +110 -0
- package/build/src/host/controllers/interaction_controller.d.ts +13 -1
- package/build/src/host/controllers/interaction_controller.js +380 -24
- package/build/src/host/csrf.d.ts +8 -18
- package/build/src/host/csrf.js +28 -2
- package/build/src/host/custom_login.d.ts +36 -0
- package/build/src/host/custom_login.js +76 -0
- package/build/src/host/custom_login_completion.d.ts +4 -0
- package/build/src/host/custom_login_completion.js +33 -0
- package/build/src/host/custom_mfa.d.ts +77 -0
- package/build/src/host/custom_mfa.js +268 -0
- package/build/src/host/i18n.d.ts +58 -0
- package/build/src/host/i18n.js +64 -6
- package/build/src/host/idp_session_bridge.d.ts +5 -3
- package/build/src/host/idp_session_bridge.js +5 -3
- package/build/src/host/oidc_rp_guard.d.ts +5 -1
- package/build/src/host/oidc_rp_guard.js +15 -2
- package/build/src/host/persistent_rp_session.d.ts +8 -0
- package/build/src/host/persistent_rp_session.js +74 -0
- package/build/src/host/redirect_exact.d.ts +12 -0
- package/build/src/host/redirect_exact.js +17 -0
- package/build/src/host/register_auth_host.js +60 -7
- package/build/src/host/request_url.d.ts +10 -0
- package/build/src/host/request_url.js +17 -0
- package/build/src/host/sudo/methods/magic_link.js +2 -1
- package/build/src/host/sudo/runtime.js +3 -2
- package/build/src/host/sudo_mode.js +4 -4
- package/build/src/host/whatsapp_code_sender.d.ts +20 -0
- package/build/src/host/whatsapp_code_sender.js +9 -0
- package/build/src/host/whatsapp_senders/http.d.ts +5 -0
- package/build/src/host/whatsapp_senders/http.js +33 -0
- package/build/src/host/whatsapp_senders/meta_whatsapp_code_sender.d.ts +20 -0
- package/build/src/host/whatsapp_senders/meta_whatsapp_code_sender.js +51 -0
- package/build/src/host/whatsapp_senders/whatsmiau_code_sender.d.ts +14 -0
- package/build/src/host/whatsapp_senders/whatsmiau_code_sender.js +36 -0
- package/build/src/mcp/mcp_oauth.d.ts +85 -0
- package/build/src/mcp/mcp_oauth.js +154 -0
- package/build/src/provider/build_provider.js +15 -1
- package/build/src/provider/oidc_service.d.ts +8 -0
- package/build/src/provider/oidc_service.js +16 -2
- package/build/src/schema/ensure.js +92 -0
- package/build/stubs/ui/react/pages/consent.tsx +17 -1
- package/build/stubs/ui/react/pages/mfa-challenge.tsx +207 -37
- package/package.json +2 -1
- package/stubs/ui/react/pages/consent.tsx +17 -1
- package/stubs/ui/react/pages/mfa-challenge.tsx +207 -37
|
@@ -3,6 +3,9 @@ import { supportsLoginMethodsPreference, supportsMagicLink, supportsOtpLogin, su
|
|
|
3
3
|
import { AdminSessionsService } from '../admin_sessions_service.js';
|
|
4
4
|
import { guardBotProtection, resolveEffectiveBotProtection } from '../bot_protection.js';
|
|
5
5
|
import { brandFor, isFirstParty } from '../branding.js';
|
|
6
|
+
import { isCustomLoginMethodId } from '../custom_login.js';
|
|
7
|
+
import { completeCustomLoginSession } from '../custom_login_completion.js';
|
|
8
|
+
import { beginCustomMfa, clearMfaFlow, customMfaViewProps, getMfaFlow, initializeMfaFlow, recordVerifiedMfaFactor, resolveCustomMfaMethods, verifyCustomMfa, } from '../custom_mfa.js';
|
|
6
9
|
import { sendMagicLinkEmail, sendOtpUnlockEmail } from '../default_mailer.js';
|
|
7
10
|
import { normalizeEmailIdentifier } from '../email_identifier.js';
|
|
8
11
|
import { translate } from '../i18n.js';
|
|
@@ -51,6 +54,8 @@ const MFA_PENDING_KEY = 'authkit_mfa_pending';
|
|
|
51
54
|
* começou e o `amr` do id_token perdia o primeiro fator (ver `mfaCompletion`).
|
|
52
55
|
*/
|
|
53
56
|
const MFA_PRIMARY_KEY = 'authkit_mfa_primary';
|
|
57
|
+
/** Written only after trusted custom authentication reaches the MFA gate. */
|
|
58
|
+
const MFA_CUSTOM_PRIMARY_KEY = 'authkit_mfa_custom_primary';
|
|
54
59
|
/** Desafio WebAuthn pendente (autenticação) guardado entre begin/finish no login. */
|
|
55
60
|
const PASSKEY_AUTH_CHALLENGE_KEY = 'authkit_passkey_auth_challenge';
|
|
56
61
|
/**
|
|
@@ -64,6 +69,72 @@ function forgetLoginEmail(ctx) {
|
|
|
64
69
|
ctx.session.forget(SESSION_KEY);
|
|
65
70
|
}
|
|
66
71
|
export default class AuthInteractionController {
|
|
72
|
+
/** Shared completion for trusted host authentication; primary proof is the host's responsibility. */
|
|
73
|
+
async completeCustomLogin(ctx, input) {
|
|
74
|
+
if (!isCustomLoginMethodId(input.method))
|
|
75
|
+
throw new Error('Invalid custom login method');
|
|
76
|
+
const service = await ctx.containerResolver.make('authkit.server');
|
|
77
|
+
const cfg = service.config;
|
|
78
|
+
const render = cfg.render;
|
|
79
|
+
if (!render)
|
|
80
|
+
throw new Error('AuthKit login renderer is not configured');
|
|
81
|
+
const details = await service.interactions.details(ctx);
|
|
82
|
+
if (details.uid !== ctx.request.param('uid') || details.prompt.name !== 'login') {
|
|
83
|
+
throw new Error('Invalid login interaction');
|
|
84
|
+
}
|
|
85
|
+
const account = await cfg.accountStore.findById(input.accountId);
|
|
86
|
+
if (!account)
|
|
87
|
+
throw new Error('Unknown account');
|
|
88
|
+
const settings = await resolveRuntimeSettingsOrNoop(ctx);
|
|
89
|
+
const brand = brandFor(cfg.branding, details.params.client_id, details.params.audience);
|
|
90
|
+
const fail = (key) => render(ctx, 'login', {
|
|
91
|
+
uid: details.uid,
|
|
92
|
+
csrfToken: ctx.request.csrfToken,
|
|
93
|
+
brand,
|
|
94
|
+
error: translate(cfg.messages, key),
|
|
95
|
+
});
|
|
96
|
+
if (await isEmailUnverifiedBlock(cfg, account.id, settings))
|
|
97
|
+
return fail('errors.email_unverified');
|
|
98
|
+
const status = await assertLoginAllowed(cfg, account.id, {
|
|
99
|
+
email: account.email,
|
|
100
|
+
ip: ctx.request.ip(),
|
|
101
|
+
clientId: details.params.client_id,
|
|
102
|
+
settings,
|
|
103
|
+
passwordless: input.passwordless === true,
|
|
104
|
+
});
|
|
105
|
+
if (!status.allowed)
|
|
106
|
+
return fail(accountStatusErrorKey(status.reason));
|
|
107
|
+
const maintenance = await resolveEffectiveMaintenanceMode(settings);
|
|
108
|
+
if (maintenance.enabled &&
|
|
109
|
+
!(cfg.admin?.roles ?? ['ADMIN']).some((role) => (account.globalRoles ?? []).includes(role))) {
|
|
110
|
+
return render(ctx, 'maintenance', {
|
|
111
|
+
uid: details.uid,
|
|
112
|
+
csrfToken: ctx.request.csrfToken,
|
|
113
|
+
brand,
|
|
114
|
+
message: maintenance.message ?? translate(cfg.messages, 'maintenance.default_message'),
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
const gate = await this.secondFactorGate(ctx, cfg, account.id, details, input.method, input.factorId, input.remember, input.passwordless === true);
|
|
118
|
+
if (gate.kind === 'challenge') {
|
|
119
|
+
ctx.session.put(MFA_CUSTOM_PRIMARY_KEY, {
|
|
120
|
+
method: input.method,
|
|
121
|
+
accountId: account.id,
|
|
122
|
+
uid: details.uid,
|
|
123
|
+
remember: input.remember === true,
|
|
124
|
+
});
|
|
125
|
+
return gate.response;
|
|
126
|
+
}
|
|
127
|
+
await notifyLoginSuccess(ctx, cfg, {
|
|
128
|
+
accountId: account.id,
|
|
129
|
+
email: account.email,
|
|
130
|
+
ip: ctx.request.ip(),
|
|
131
|
+
clientId: details.params.client_id ?? null,
|
|
132
|
+
metadata: { method: input.method },
|
|
133
|
+
trustedDevice: gate.kind === 'trusted',
|
|
134
|
+
});
|
|
135
|
+
forgetLoginEmail(ctx);
|
|
136
|
+
return completeCustomLoginSession(ctx, account.id, { amr: [input.method] }, input.remember === true);
|
|
137
|
+
}
|
|
67
138
|
/**
|
|
68
139
|
* Métodos de login efetivos (com os pins do `cfg.authMethods`) para QUALQUER render do
|
|
69
140
|
* passo login. Sem isto, os re-renders (erro de senha, magic link enviado, lockout…) mandam
|
|
@@ -180,6 +251,7 @@ export default class AuthInteractionController {
|
|
|
180
251
|
params: details.params,
|
|
181
252
|
csrfToken: ctx.request.csrfToken,
|
|
182
253
|
brand,
|
|
254
|
+
...(await consentSubject(service, details, cfg.messages)),
|
|
183
255
|
});
|
|
184
256
|
}
|
|
185
257
|
const email = ctx.session.get(SESSION_KEY);
|
|
@@ -480,13 +552,225 @@ export default class AuthInteractionController {
|
|
|
480
552
|
* 2º fator: lê o accountId pendente da sessão e aceita um código TOTP (`code`)
|
|
481
553
|
* OU um recovery code (`recoveryCode`). Em caso de sucesso finaliza a interaction.
|
|
482
554
|
*/
|
|
555
|
+
async renderMfaChallenge(ctx, cfg, props) {
|
|
556
|
+
const flow = getMfaFlow(ctx);
|
|
557
|
+
const custom = flow ? await customMfaViewProps(ctx) : {};
|
|
558
|
+
const used = new Set(flow ? [flow.primaryFactorId, ...flow.completed.map((entry) => entry.factorId)] : []);
|
|
559
|
+
return cfg.render(ctx, 'mfa-challenge', {
|
|
560
|
+
...props,
|
|
561
|
+
...custom,
|
|
562
|
+
...(used.has('totp') ? { totpAvailable: false } : {}),
|
|
563
|
+
...(used.has('webauthn') ? { passkeyAvailable: false } : {}),
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
async configuredMfaGate(ctx, cfg, accountId, details, primary, primaryFactorId, rememberRequested, passwordless = primary !== 'pwd') {
|
|
567
|
+
const requiredFactors = cfg.mfa?.requiredFactors ?? 2;
|
|
568
|
+
const mfaRequired = this.acrRequiresMfa(cfg, details) || cfg.mfa?.requiredFactors !== undefined;
|
|
569
|
+
const mfa = (await cfg.accountStore.getMfaState?.(accountId)) ?? { enabled: false };
|
|
570
|
+
const enrolledTotp = mfa.totp ?? mfa.enabled;
|
|
571
|
+
const enrolledPasskey = await this.hasPasskeys(cfg, accountId);
|
|
572
|
+
ctx.session.put(MFA_PENDING_KEY, accountId);
|
|
573
|
+
ctx.session.put(MFA_PRIMARY_KEY, primary);
|
|
574
|
+
const rememberValue = ctx.request.input('remember');
|
|
575
|
+
initializeMfaFlow(ctx, {
|
|
576
|
+
uid: details.uid,
|
|
577
|
+
accountId,
|
|
578
|
+
primaryMethod: primary,
|
|
579
|
+
primaryFactorId,
|
|
580
|
+
passwordless,
|
|
581
|
+
requiredFactors,
|
|
582
|
+
remember: rememberRequested ??
|
|
583
|
+
(rememberValue === true || ['on', '1', 'true'].includes(String(rememberValue))),
|
|
584
|
+
});
|
|
585
|
+
const flow = getMfaFlow(ctx);
|
|
586
|
+
const custom = await resolveCustomMfaMethods(ctx, accountId, details.uid, primary, flow.challengeId);
|
|
587
|
+
const totpAvailable = Boolean(enrolledTotp && primaryFactorId !== 'totp');
|
|
588
|
+
const passkeyAvailable = Boolean(enrolledPasskey && primaryFactorId !== 'webauthn');
|
|
589
|
+
const groups = new Set(custom.map((method) => method.factorId).filter((group) => group !== primaryFactorId));
|
|
590
|
+
if (totpAvailable)
|
|
591
|
+
groups.add('totp');
|
|
592
|
+
if (passkeyAvailable)
|
|
593
|
+
groups.add('webauthn');
|
|
594
|
+
const enrolled = Boolean(enrolledTotp || enrolledPasskey || custom.length);
|
|
595
|
+
if (!enrolled && !mfaRequired) {
|
|
596
|
+
clearMfaFlow(ctx);
|
|
597
|
+
ctx.session.forget(MFA_PENDING_KEY);
|
|
598
|
+
ctx.session.forget(MFA_PRIMARY_KEY);
|
|
599
|
+
return { kind: 'none' };
|
|
600
|
+
}
|
|
601
|
+
const brand = brandFor(cfg.branding, details.params.client_id, details.params.audience);
|
|
602
|
+
if (groups.size < requiredFactors - 1) {
|
|
603
|
+
clearMfaFlow(ctx);
|
|
604
|
+
ctx.session.forget(MFA_PENDING_KEY);
|
|
605
|
+
ctx.session.forget(MFA_PRIMARY_KEY);
|
|
606
|
+
return {
|
|
607
|
+
kind: 'challenge',
|
|
608
|
+
response: await this.renderMfaChallenge(ctx, cfg, {
|
|
609
|
+
uid: details.uid,
|
|
610
|
+
csrfToken: ctx.request.csrfToken,
|
|
611
|
+
brand,
|
|
612
|
+
noEnrollment: true,
|
|
613
|
+
totpAvailable: false,
|
|
614
|
+
passkeyAvailable: false,
|
|
615
|
+
error: translate(cfg.messages, 'mfa_challenge.required_no_enrollment'),
|
|
616
|
+
}),
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
// Custom enrollment does not expose a revocation timestamp; never bypass it with a legacy trusted-device cookie.
|
|
620
|
+
if (cfg.trustedDevices.enabled &&
|
|
621
|
+
!mfaRequired &&
|
|
622
|
+
!custom.length &&
|
|
623
|
+
requiredFactors === 2 &&
|
|
624
|
+
(await this.checkTrustedDevice(ctx, accountId, mfa.enabledAt ?? null))) {
|
|
625
|
+
clearMfaFlow(ctx);
|
|
626
|
+
ctx.session.forget(MFA_PENDING_KEY);
|
|
627
|
+
ctx.session.forget(MFA_PRIMARY_KEY);
|
|
628
|
+
return { kind: 'trusted' };
|
|
629
|
+
}
|
|
630
|
+
return {
|
|
631
|
+
kind: 'challenge',
|
|
632
|
+
response: await this.renderMfaChallenge(ctx, cfg, {
|
|
633
|
+
uid: details.uid,
|
|
634
|
+
csrfToken: ctx.request.csrfToken,
|
|
635
|
+
brand,
|
|
636
|
+
totpAvailable,
|
|
637
|
+
passkeyAvailable,
|
|
638
|
+
trustedDevicesEnabled: cfg.trustedDevices.enabled && !custom.length,
|
|
639
|
+
trustedDeviceDays: cfg.trustedDevices.days,
|
|
640
|
+
}),
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
async customMfaBegin(ctx) {
|
|
644
|
+
await beginCustomMfa(ctx, String(ctx.request.param('method')));
|
|
645
|
+
const service = await ctx.containerResolver.make('authkit.server');
|
|
646
|
+
const flow = getMfaFlow(ctx);
|
|
647
|
+
return this.renderMfaChallenge(ctx, service.config, {
|
|
648
|
+
uid: flow.uid,
|
|
649
|
+
csrfToken: ctx.request.csrfToken,
|
|
650
|
+
totpAvailable: await this.hasTotp(service.config, flow.accountId),
|
|
651
|
+
passkeyAvailable: await this.hasPasskeys(service.config, flow.accountId),
|
|
652
|
+
brand: brandFor(service.config.branding, undefined),
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
async customMfaVerify(ctx) {
|
|
656
|
+
const service = await ctx.containerResolver.make('authkit.server');
|
|
657
|
+
const details = await service.interactions.details(ctx);
|
|
658
|
+
const method = String(ctx.request.param('method'));
|
|
659
|
+
let ok;
|
|
660
|
+
try {
|
|
661
|
+
ok = await verifyCustomMfa(ctx, method);
|
|
662
|
+
}
|
|
663
|
+
catch (error) {
|
|
664
|
+
await service.config.audit?.record({
|
|
665
|
+
type: 'login.failure',
|
|
666
|
+
accountId: getMfaFlow(ctx)?.accountId ?? null,
|
|
667
|
+
metadata: { stage: 'mfa', method },
|
|
668
|
+
});
|
|
669
|
+
throw error;
|
|
670
|
+
}
|
|
671
|
+
const flow = getMfaFlow(ctx);
|
|
672
|
+
if (!ok) {
|
|
673
|
+
await service.config.audit?.record({
|
|
674
|
+
type: 'login.failure',
|
|
675
|
+
accountId: flow?.accountId ?? null,
|
|
676
|
+
metadata: { stage: 'mfa', method },
|
|
677
|
+
});
|
|
678
|
+
if (!flow)
|
|
679
|
+
return ctx.response.redirect(`/auth/interaction/${ctx.request.param('uid')}`);
|
|
680
|
+
return this.renderMfaChallenge(ctx, service.config, {
|
|
681
|
+
uid: flow.uid,
|
|
682
|
+
csrfToken: ctx.request.csrfToken,
|
|
683
|
+
error: translate(service.config.messages, 'errors.invalid_code'),
|
|
684
|
+
totpAvailable: await this.hasTotp(service.config, flow.accountId),
|
|
685
|
+
passkeyAvailable: await this.hasPasskeys(service.config, flow.accountId),
|
|
686
|
+
brand: brandFor(service.config.branding, undefined),
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
if (!flow)
|
|
690
|
+
throw new Error('Missing MFA flow');
|
|
691
|
+
const descriptors = await resolveCustomMfaMethods(ctx, flow.accountId, flow.uid, flow.primaryMethod, flow.challengeId);
|
|
692
|
+
const descriptor = descriptors.find((item) => item.id === method);
|
|
693
|
+
if (!descriptor)
|
|
694
|
+
throw new Error('MFA method is no longer enabled');
|
|
695
|
+
return this.finishVerifiedMfa(ctx, service.config, details, method, descriptor.factorId);
|
|
696
|
+
}
|
|
697
|
+
async finishVerifiedMfa(ctx, cfg, details, method, factorId) {
|
|
698
|
+
const flow = getMfaFlow(ctx);
|
|
699
|
+
if (!flow)
|
|
700
|
+
throw new Error('Missing MFA flow');
|
|
701
|
+
const liveDetails = await (await ctx.containerResolver.make('authkit.server')).interactions.details(ctx);
|
|
702
|
+
if (liveDetails.uid !== flow.uid || liveDetails.prompt.name !== 'login') {
|
|
703
|
+
this.takeMfaPending(ctx);
|
|
704
|
+
clearMfaFlow(ctx);
|
|
705
|
+
return ctx.response.redirect(`/auth/interaction/${ctx.request.param('uid')}`);
|
|
706
|
+
}
|
|
707
|
+
const progress = recordVerifiedMfaFactor(ctx, method, factorId);
|
|
708
|
+
if (!progress.complete)
|
|
709
|
+
return this.renderMfaChallenge(ctx, cfg, {
|
|
710
|
+
uid: flow.uid,
|
|
711
|
+
csrfToken: ctx.request.csrfToken,
|
|
712
|
+
totpAvailable: await this.hasTotp(cfg, flow.accountId),
|
|
713
|
+
passkeyAvailable: await this.hasPasskeys(cfg, flow.accountId),
|
|
714
|
+
brand: brandFor(cfg.branding, undefined),
|
|
715
|
+
});
|
|
716
|
+
const settings = await resolveRuntimeSettingsOrNoop(ctx);
|
|
717
|
+
const account = await cfg.accountStore.findById(flow.accountId);
|
|
718
|
+
const status = account
|
|
719
|
+
? await assertLoginAllowed(cfg, account.id, {
|
|
720
|
+
email: account.email,
|
|
721
|
+
ip: ctx.request.ip?.() ?? null,
|
|
722
|
+
settings,
|
|
723
|
+
passwordless: flow.passwordless === true,
|
|
724
|
+
})
|
|
725
|
+
: { allowed: false, reason: 'disabled' };
|
|
726
|
+
this.takeMfaPending(ctx);
|
|
727
|
+
clearMfaFlow(ctx);
|
|
728
|
+
if (!account ||
|
|
729
|
+
!status.allowed ||
|
|
730
|
+
(await isEmailUnverifiedBlock(cfg, flow.accountId, settings))) {
|
|
731
|
+
return cfg.render(ctx, 'login', {
|
|
732
|
+
uid: flow.uid,
|
|
733
|
+
csrfToken: ctx.request.csrfToken,
|
|
734
|
+
error: translate(cfg.messages, !status.allowed ? accountStatusErrorKey(status.reason) : 'errors.email_unverified'),
|
|
735
|
+
brand: brandFor(cfg.branding, undefined),
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
const maintenance = await resolveEffectiveMaintenanceMode(settings);
|
|
739
|
+
if (maintenance.enabled &&
|
|
740
|
+
!(cfg.admin?.roles ?? ['ADMIN']).some((role) => (account.globalRoles ?? []).includes(role))) {
|
|
741
|
+
return cfg.render(ctx, 'maintenance', {
|
|
742
|
+
uid: flow.uid,
|
|
743
|
+
csrfToken: ctx.request.csrfToken,
|
|
744
|
+
message: maintenance.message ?? translate(cfg.messages, 'maintenance.default_message'),
|
|
745
|
+
brand: brandFor(cfg.branding, undefined),
|
|
746
|
+
adminLoginAllowed: true,
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
forgetLoginEmail(ctx);
|
|
750
|
+
await notifyLoginSuccess(ctx, cfg, {
|
|
751
|
+
accountId: account.id,
|
|
752
|
+
email: account.email,
|
|
753
|
+
metadata: { mfa: flow.completed.map((entry) => entry.method) },
|
|
754
|
+
});
|
|
755
|
+
const stepUp = this.acrRequiresMfa(cfg, details) ? { acr: cfg.stepUp?.mfaAcr } : {};
|
|
756
|
+
return completeCustomLoginSession(ctx, account.id, {
|
|
757
|
+
...stepUp,
|
|
758
|
+
amr: [flow.primaryMethod, 'mfa', ...flow.completed.map((entry) => entry.method)],
|
|
759
|
+
}, flow.remember === true);
|
|
760
|
+
}
|
|
483
761
|
async mfaVerify(ctx) {
|
|
484
762
|
const service = await ctx.containerResolver.make('authkit.server');
|
|
485
763
|
const cfg = service.config;
|
|
486
|
-
const render = cfg.render;
|
|
487
764
|
const details = await service.interactions.details(ctx);
|
|
488
765
|
const brand = brandFor(cfg.branding, details.params.client_id, details.params.audience);
|
|
489
766
|
const accountId = ctx.session.get(MFA_PENDING_KEY);
|
|
767
|
+
if (cfg.mfa && !getMfaFlow(ctx))
|
|
768
|
+
return ctx.response.redirect(`/auth/interaction/${ctx.request.param('uid')}`);
|
|
769
|
+
if (cfg.mfa && (details.uid !== ctx.request.param('uid') || details.prompt.name !== 'login')) {
|
|
770
|
+
this.takeMfaPending(ctx);
|
|
771
|
+
clearMfaFlow(ctx);
|
|
772
|
+
return ctx.response.redirect(`/auth/interaction/${ctx.request.param('uid')}`);
|
|
773
|
+
}
|
|
490
774
|
const ip = ctx.request.ip?.() ?? null;
|
|
491
775
|
const clientId = details.params.client_id ?? null;
|
|
492
776
|
if (!accountId) {
|
|
@@ -500,7 +784,7 @@ export default class AuthInteractionController {
|
|
|
500
784
|
const otpLockout = createOtpLockout(otpLockoutCfg, ctx.logger);
|
|
501
785
|
// Verifica se o fator OTP está travado ANTES de tentar verificar.
|
|
502
786
|
if (otpLockoutCfg.enabled && (await otpLockout.isLocked(accountId))) {
|
|
503
|
-
return
|
|
787
|
+
return this.renderMfaChallenge(ctx, cfg, {
|
|
504
788
|
uid: ctx.request.param('uid'),
|
|
505
789
|
csrfToken: ctx.request.csrfToken,
|
|
506
790
|
error: translate(cfg.messages, 'errors.otp_locked'),
|
|
@@ -534,7 +818,7 @@ export default class AuthInteractionController {
|
|
|
534
818
|
if (nowLocked) {
|
|
535
819
|
// Emite e-mail de desbloqueio (best-effort, fail-safe).
|
|
536
820
|
await this.sendOtpUnlockEmailIfAble(ctx, cfg, accountId, otpLockoutCfg.unlockTtlHours);
|
|
537
|
-
return
|
|
821
|
+
return this.renderMfaChallenge(ctx, cfg, {
|
|
538
822
|
uid: ctx.request.param('uid'),
|
|
539
823
|
csrfToken: ctx.request.csrfToken,
|
|
540
824
|
error: translate(cfg.messages, 'errors.otp_locked'),
|
|
@@ -546,7 +830,7 @@ export default class AuthInteractionController {
|
|
|
546
830
|
otpLocked: true,
|
|
547
831
|
});
|
|
548
832
|
}
|
|
549
|
-
return
|
|
833
|
+
return this.renderMfaChallenge(ctx, cfg, {
|
|
550
834
|
uid: ctx.request.param('uid'),
|
|
551
835
|
csrfToken: ctx.request.csrfToken,
|
|
552
836
|
error: translate(cfg.messages, 'errors.invalid_code'),
|
|
@@ -559,10 +843,12 @@ export default class AuthInteractionController {
|
|
|
559
843
|
}
|
|
560
844
|
// Código correto: zera o contador de falhas OTP.
|
|
561
845
|
await otpLockout.clearFailures(accountId);
|
|
846
|
+
if (cfg.mfa)
|
|
847
|
+
return this.finishVerifiedMfa(ctx, cfg, details, usedRecovery ? 'recovery' : 'totp', 'totp');
|
|
562
848
|
// Sucesso no 2º fator: opcionalmente confia neste dispositivo (checkbox).
|
|
563
849
|
await this.maybeTrustDevice(ctx, cfg, accountId);
|
|
564
850
|
// Finaliza a interaction para o accountId pendente.
|
|
565
|
-
const { primary } = this.takeMfaPending(ctx);
|
|
851
|
+
const { primary, customRemember } = this.takeMfaPending(ctx);
|
|
566
852
|
forgetLoginEmail(ctx);
|
|
567
853
|
await notifyLoginSuccess(ctx, cfg, {
|
|
568
854
|
accountId,
|
|
@@ -570,9 +856,13 @@ export default class AuthInteractionController {
|
|
|
570
856
|
clientId,
|
|
571
857
|
metadata: { mfa: usedRecovery ? 'recovery' : 'totp' },
|
|
572
858
|
});
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
859
|
+
const extra = this.mfaCompletion(cfg, details, primary, usedRecovery ? 'recovery' : 'totp');
|
|
860
|
+
if (customRemember !== undefined) {
|
|
861
|
+
await completeCustomLoginSession(ctx, accountId, extra, customRemember);
|
|
862
|
+
}
|
|
863
|
+
else {
|
|
864
|
+
await service.interactions.completeLogin(ctx, accountId, extra);
|
|
865
|
+
}
|
|
576
866
|
}
|
|
577
867
|
/**
|
|
578
868
|
* true se o authorize request exige MFA via acr_values (contém o mfaAcr da
|
|
@@ -618,9 +908,25 @@ export default class AuthInteractionController {
|
|
|
618
908
|
*/
|
|
619
909
|
takeMfaPending(ctx) {
|
|
620
910
|
const raw = ctx.session.get(MFA_PRIMARY_KEY);
|
|
911
|
+
const custom = ctx.session.get(MFA_CUSTOM_PRIMARY_KEY);
|
|
912
|
+
const accountId = ctx.session.get(MFA_PENDING_KEY);
|
|
621
913
|
ctx.session.forget(MFA_PENDING_KEY);
|
|
622
914
|
ctx.session.forget(MFA_PRIMARY_KEY);
|
|
623
|
-
|
|
915
|
+
ctx.session.forget(MFA_CUSTOM_PRIMARY_KEY);
|
|
916
|
+
if (raw === 'pwd' || raw === 'email')
|
|
917
|
+
return { primary: raw };
|
|
918
|
+
const boundCustom = custom !== null &&
|
|
919
|
+
typeof custom === 'object' &&
|
|
920
|
+
'method' in custom &&
|
|
921
|
+
custom.method === raw &&
|
|
922
|
+
'accountId' in custom &&
|
|
923
|
+
custom.accountId === accountId &&
|
|
924
|
+
'uid' in custom &&
|
|
925
|
+
custom.uid === ctx.request.param('uid');
|
|
926
|
+
return {
|
|
927
|
+
...(boundCustom ? { customRemember: 'remember' in custom && custom.remember === true } : {}),
|
|
928
|
+
primary: boundCustom && typeof raw === 'string' && isCustomLoginMethodId(raw) ? raw : undefined,
|
|
929
|
+
};
|
|
624
930
|
}
|
|
625
931
|
/**
|
|
626
932
|
* Gate do segundo fator, compartilhado pelos caminhos de login: senha, link
|
|
@@ -644,7 +950,11 @@ export default class AuthInteractionController {
|
|
|
644
950
|
* - caso geral → `challenge`, com o accountId em `MFA_PENDING_KEY` e o fator
|
|
645
951
|
* primário (`primary`) em `MFA_PRIMARY_KEY`.
|
|
646
952
|
*/
|
|
647
|
-
async secondFactorGate(ctx, cfg, accountId, details, primary) {
|
|
953
|
+
async secondFactorGate(ctx, cfg, accountId, details, primary, primaryFactorId = primary, rememberRequested, passwordless = primary !== 'pwd') {
|
|
954
|
+
ctx.session.forget(MFA_CUSTOM_PRIMARY_KEY);
|
|
955
|
+
clearMfaFlow(ctx);
|
|
956
|
+
if (cfg.mfa)
|
|
957
|
+
return this.configuredMfaGate(ctx, cfg, accountId, details, primary, primaryFactorId, rememberRequested, passwordless);
|
|
648
958
|
const mfaRequired = this.acrRequiresMfa(cfg, details);
|
|
649
959
|
const mfa = (await cfg.accountStore.getMfaState?.(accountId)) ?? { enabled: false };
|
|
650
960
|
// Passkey disponível como alternativa ao TOTP se o store suporta E a conta
|
|
@@ -657,13 +967,12 @@ export default class AuthInteractionController {
|
|
|
657
967
|
const hasFactor = passkeyAvailable || totpAvailable;
|
|
658
968
|
if (!hasFactor && !mfaRequired)
|
|
659
969
|
return { kind: 'none' };
|
|
660
|
-
const render = cfg.render;
|
|
661
970
|
const brand = brandFor(cfg.branding, details.params.client_id, details.params.audience);
|
|
662
971
|
const uid = ctx.request.param('uid');
|
|
663
972
|
if (!hasFactor) {
|
|
664
973
|
return {
|
|
665
974
|
kind: 'challenge',
|
|
666
|
-
response: await
|
|
975
|
+
response: await this.renderMfaChallenge(ctx, cfg, {
|
|
667
976
|
uid,
|
|
668
977
|
csrfToken: ctx.request.csrfToken,
|
|
669
978
|
brand,
|
|
@@ -684,7 +993,7 @@ export default class AuthInteractionController {
|
|
|
684
993
|
ctx.session.put(MFA_PRIMARY_KEY, primary);
|
|
685
994
|
return {
|
|
686
995
|
kind: 'challenge',
|
|
687
|
-
response: await
|
|
996
|
+
response: await this.renderMfaChallenge(ctx, cfg, {
|
|
688
997
|
uid,
|
|
689
998
|
csrfToken: ctx.request.csrfToken,
|
|
690
999
|
brand,
|
|
@@ -1233,8 +1542,15 @@ export default class AuthInteractionController {
|
|
|
1233
1542
|
async passkeyVerify(ctx) {
|
|
1234
1543
|
const service = await ctx.containerResolver.make('authkit.server');
|
|
1235
1544
|
const cfg = service.config;
|
|
1236
|
-
const render = cfg.render;
|
|
1237
1545
|
const details = await service.interactions.details(ctx);
|
|
1546
|
+
if (cfg.mfa && (details.uid !== ctx.request.param('uid') || details.prompt.name !== 'login')) {
|
|
1547
|
+
this.takeMfaPending(ctx);
|
|
1548
|
+
clearMfaFlow(ctx);
|
|
1549
|
+
return ctx.response.redirect(`/auth/interaction/${ctx.request.param('uid')}`);
|
|
1550
|
+
}
|
|
1551
|
+
if (cfg.mfa && ctx.session.get(MFA_PENDING_KEY) && !getMfaFlow(ctx)) {
|
|
1552
|
+
return ctx.response.redirect(`/auth/interaction/${ctx.request.param('uid')}`);
|
|
1553
|
+
}
|
|
1238
1554
|
const brand = brandFor(cfg.branding, details.params.client_id, details.params.audience);
|
|
1239
1555
|
let accountId = await this.resolvePasskeyAccountId(ctx, cfg);
|
|
1240
1556
|
const challenge = ctx.session.get(PASSKEY_AUTH_CHALLENGE_KEY);
|
|
@@ -1299,7 +1615,7 @@ export default class AuthInteractionController {
|
|
|
1299
1615
|
clientId,
|
|
1300
1616
|
metadata: { stage: 'mfa', method: 'webauthn', reason: 'method_disabled_by_user' },
|
|
1301
1617
|
});
|
|
1302
|
-
return
|
|
1618
|
+
return this.renderMfaChallenge(ctx, cfg, {
|
|
1303
1619
|
uid: ctx.request.param('uid'),
|
|
1304
1620
|
csrfToken: ctx.request.csrfToken,
|
|
1305
1621
|
error: translate(cfg.messages, 'mfa_challenge.passkey_error'),
|
|
@@ -1318,7 +1634,7 @@ export default class AuthInteractionController {
|
|
|
1318
1634
|
clientId,
|
|
1319
1635
|
metadata: { stage: 'mfa', method: 'webauthn' },
|
|
1320
1636
|
});
|
|
1321
|
-
return
|
|
1637
|
+
return this.renderMfaChallenge(ctx, cfg, {
|
|
1322
1638
|
uid: ctx.request.param('uid'),
|
|
1323
1639
|
csrfToken: ctx.request.csrfToken,
|
|
1324
1640
|
error: translate(cfg.messages, 'mfa_challenge.passkey_error'),
|
|
@@ -1341,7 +1657,7 @@ export default class AuthInteractionController {
|
|
|
1341
1657
|
clientId,
|
|
1342
1658
|
metadata: { stage: 'mfa', method: 'webauthn', reason: 'unverified' },
|
|
1343
1659
|
});
|
|
1344
|
-
return
|
|
1660
|
+
return this.renderMfaChallenge(ctx, cfg, {
|
|
1345
1661
|
uid: ctx.request.param('uid'),
|
|
1346
1662
|
csrfToken: ctx.request.csrfToken,
|
|
1347
1663
|
error: translate(cfg.messages, 'errors.email_unverified'),
|
|
@@ -1372,7 +1688,7 @@ export default class AuthInteractionController {
|
|
|
1372
1688
|
passwordless: true,
|
|
1373
1689
|
});
|
|
1374
1690
|
if (!passkeyStatusGate.allowed) {
|
|
1375
|
-
return
|
|
1691
|
+
return this.renderMfaChallenge(ctx, cfg, {
|
|
1376
1692
|
uid: ctx.request.param('uid'),
|
|
1377
1693
|
csrfToken: ctx.request.csrfToken,
|
|
1378
1694
|
error: translate(cfg.messages, accountStatusErrorKey(passkeyStatusGate.reason)),
|
|
@@ -1383,12 +1699,22 @@ export default class AuthInteractionController {
|
|
|
1383
1699
|
trustedDeviceDays: cfg.trustedDevices.days,
|
|
1384
1700
|
});
|
|
1385
1701
|
}
|
|
1702
|
+
if (cfg.mfa && ctx.session.get(MFA_PENDING_KEY)) {
|
|
1703
|
+
if (!getMfaFlow(ctx))
|
|
1704
|
+
return ctx.response.redirect(`/auth/interaction/${ctx.request.param('uid')}`);
|
|
1705
|
+
return this.finishVerifiedMfa(ctx, cfg, details, 'webauthn', 'webauthn');
|
|
1706
|
+
}
|
|
1707
|
+
if (cfg.mfa) {
|
|
1708
|
+
const gate = await this.secondFactorGate(ctx, cfg, accountId, details, 'webauthn');
|
|
1709
|
+
if (gate.kind === 'challenge')
|
|
1710
|
+
return gate.response;
|
|
1711
|
+
}
|
|
1386
1712
|
// Passkey OK: opcionalmente confia neste dispositivo (checkbox no challenge).
|
|
1387
1713
|
await this.maybeTrustDevice(ctx, cfg, accountId);
|
|
1388
1714
|
// Passkey como 2º fator (havia desafio pendente) ou passkey-first (não havia)?
|
|
1389
1715
|
// Lido ANTES de esquecer: é o que decide o amr abaixo.
|
|
1390
1716
|
const wasSecondFactor = ctx.session.get(MFA_PENDING_KEY) === accountId;
|
|
1391
|
-
const { primary } = this.takeMfaPending(ctx);
|
|
1717
|
+
const { primary, customRemember } = this.takeMfaPending(ctx);
|
|
1392
1718
|
forgetLoginEmail(ctx);
|
|
1393
1719
|
await notifyLoginSuccess(ctx, cfg, {
|
|
1394
1720
|
accountId,
|
|
@@ -1396,12 +1722,15 @@ export default class AuthInteractionController {
|
|
|
1396
1722
|
clientId,
|
|
1397
1723
|
metadata: { mfa: 'webauthn' },
|
|
1398
1724
|
});
|
|
1399
|
-
|
|
1400
|
-
// TOTP. Passkey-first: a passkey É o login (amr `['webauthn']`); o step-up
|
|
1401
|
-
// carimba acr/amr quando solicitado.
|
|
1402
|
-
await service.interactions.completeLogin(ctx, accountId, wasSecondFactor
|
|
1725
|
+
const extra = wasSecondFactor
|
|
1403
1726
|
? this.mfaCompletion(cfg, details, primary, 'webauthn')
|
|
1404
|
-
: (this.stepUpExtra(cfg, details, 'webauthn') ?? { amr: ['webauthn'] })
|
|
1727
|
+
: (this.stepUpExtra(cfg, details, 'webauthn') ?? { amr: ['webauthn'] });
|
|
1728
|
+
if (customRemember !== undefined) {
|
|
1729
|
+
await completeCustomLoginSession(ctx, accountId, extra, customRemember);
|
|
1730
|
+
}
|
|
1731
|
+
else {
|
|
1732
|
+
await service.interactions.completeLogin(ctx, accountId, extra);
|
|
1733
|
+
}
|
|
1405
1734
|
}
|
|
1406
1735
|
/**
|
|
1407
1736
|
* GET /auth/interaction/:uid/switch
|
|
@@ -1564,3 +1893,30 @@ export default class AuthInteractionController {
|
|
|
1564
1893
|
}
|
|
1565
1894
|
}
|
|
1566
1895
|
}
|
|
1896
|
+
/**
|
|
1897
|
+
* Quem pede o acesso e o quê, para a tela de consent: o nome que o client registrou (um cliente
|
|
1898
|
+
* MCP registra o seu, ex.: "Claude Code") e os escopos pedidos, com o rótulo de cada um. O nome
|
|
1899
|
+
* vem de fora (registro dinâmico aberto) — `clientNameHtml` sai escapado para o `{{{ }}}`.
|
|
1900
|
+
*/
|
|
1901
|
+
async function consentSubject(service, details, messages) {
|
|
1902
|
+
const clientId = String(details.params?.client_id ?? '');
|
|
1903
|
+
const client = clientId ? await service.provider.Client.find(clientId).catch(() => null) : null;
|
|
1904
|
+
const clientName = client?.clientName || clientId;
|
|
1905
|
+
const requested = String(details.params?.scope ?? '')
|
|
1906
|
+
.split(' ')
|
|
1907
|
+
.filter((scope) => scope !== '' && scope !== 'openid');
|
|
1908
|
+
const scopes = requested.map((scope) => {
|
|
1909
|
+
const key = `consent.scope.${scope}`;
|
|
1910
|
+
const label = translate(messages, key);
|
|
1911
|
+
return { id: scope, label: label === key ? scope : label };
|
|
1912
|
+
});
|
|
1913
|
+
return { clientName, clientNameHtml: escapeHtml(clientName), scopes };
|
|
1914
|
+
}
|
|
1915
|
+
function escapeHtml(value) {
|
|
1916
|
+
return value
|
|
1917
|
+
.replace(/&/g, '&')
|
|
1918
|
+
.replace(/</g, '<')
|
|
1919
|
+
.replace(/>/g, '>')
|
|
1920
|
+
.replace(/"/g, '"')
|
|
1921
|
+
.replace(/'/g, ''');
|
|
1922
|
+
}
|
package/build/src/host/csrf.d.ts
CHANGED
|
@@ -1,29 +1,19 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Helper de exceções de CSRF para as rotas machine-to-machine do AuthKit.
|
|
3
|
-
*
|
|
4
|
-
* Os endpoints do IdP (token/introspection/PAT) e o back-channel logout são
|
|
5
|
-
* server-to-server (sem browser/token XSRF). Em vez de cada app reescrever a
|
|
6
|
-
* checagem de URL no `config/shield.ts`, use este helper:
|
|
7
|
-
*
|
|
8
|
-
* ```ts
|
|
9
|
-
* // config/shield.ts
|
|
10
|
-
* import { authkitCsrfExceptions } from '@adonis-agora/authkit-server'
|
|
11
|
-
* csrf: {
|
|
12
|
-
* enabled: true,
|
|
13
|
-
* exceptRoutes: (ctx) => authkitCsrfExceptions(ctx.request.url(), { mountPath: '/oidc' })
|
|
14
|
-
* || ctx.request.url().includes('/api'),
|
|
15
|
-
* }
|
|
16
|
-
* ```
|
|
17
|
-
*/
|
|
18
1
|
export interface AuthkitCsrfOptions {
|
|
19
2
|
/** mountPath do IdP (mesmo de defineConfig/registerAuthHost). Default: `/oidc`. */
|
|
20
3
|
mountPath?: string;
|
|
21
4
|
/** Inclui a rota de back-channel logout do CLIENT (default: `/auth/backchannel-logout`). */
|
|
22
5
|
backchannelLogoutPath?: string | false;
|
|
6
|
+
/**
|
|
7
|
+
* Prefixo de `personalAgents`: isenta os endpoints OAuth que os agentes chamam
|
|
8
|
+
* (`{prefix}/oauth/*`). Default: o prefixo do config quando `personalAgents`
|
|
9
|
+
* está ligado; nada quando não está. `false` desliga. A tela de
|
|
10
|
+
* consentimento (`{prefix}/consent`) continua protegida.
|
|
11
|
+
*/
|
|
12
|
+
personalAgentsPrefix?: string | false;
|
|
23
13
|
}
|
|
24
14
|
/**
|
|
25
15
|
* Retorna `true` quando `url` é uma rota AuthKit que deve ser ISENTA de CSRF
|
|
26
16
|
* (machine-to-machine). Cobre o mountPath do IdP, a introspecção de PAT e a
|
|
27
|
-
* rota de back-channel logout do client.
|
|
17
|
+
* rota de back-channel logout do client e os endpoints OAuth de personal agents.
|
|
28
18
|
*/
|
|
29
19
|
export declare function authkitCsrfExceptions(url: string, options?: AuthkitCsrfOptions): boolean;
|
package/build/src/host/csrf.js
CHANGED
|
@@ -1,14 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helper de exceções de CSRF para as rotas machine-to-machine do AuthKit.
|
|
3
|
+
*
|
|
4
|
+
* Os endpoints do IdP (token/introspection/PAT) e o back-channel logout são
|
|
5
|
+
* server-to-server (sem browser/token XSRF). Em vez de cada app reescrever a
|
|
6
|
+
* checagem de URL no `config/shield.ts`, use este helper:
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* // config/shield.ts
|
|
10
|
+
* import { authkitCsrfExceptions } from '@adonis-agora/authkit-server'
|
|
11
|
+
* csrf: {
|
|
12
|
+
* enabled: true,
|
|
13
|
+
* exceptRoutes: (ctx) => authkitCsrfExceptions(ctx.request.url(), { mountPath: '/oidc' })
|
|
14
|
+
* || ctx.request.url().includes('/api'),
|
|
15
|
+
* }
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
import { normalizePersonalAgentsPrefix } from '../agents/config.js';
|
|
19
|
+
import { getAuthHostConfig } from './auth_host_config.js';
|
|
1
20
|
/**
|
|
2
21
|
* Retorna `true` quando `url` é uma rota AuthKit que deve ser ISENTA de CSRF
|
|
3
22
|
* (machine-to-machine). Cobre o mountPath do IdP, a introspecção de PAT e a
|
|
4
|
-
* rota de back-channel logout do client.
|
|
23
|
+
* rota de back-channel logout do client e os endpoints OAuth de personal agents.
|
|
5
24
|
*/
|
|
6
25
|
export function authkitCsrfExceptions(url, options = {}) {
|
|
7
26
|
const mountPath = options.mountPath ?? '/oidc';
|
|
8
27
|
const backchannel = options.backchannelLogoutPath === false
|
|
9
28
|
? null
|
|
10
29
|
: (options.backchannelLogoutPath ?? '/auth/backchannel-logout');
|
|
30
|
+
const agentsPrefix = options.personalAgentsPrefix === false
|
|
31
|
+
? undefined
|
|
32
|
+
: options.personalAgentsPrefix !== undefined
|
|
33
|
+
? normalizePersonalAgentsPrefix(options.personalAgentsPrefix)
|
|
34
|
+
: getAuthHostConfig()?.personalAgents?.prefix;
|
|
35
|
+
const agentsOAuth = agentsPrefix ? `${agentsPrefix}/oauth/` : null;
|
|
11
36
|
return (url.includes(mountPath) ||
|
|
12
37
|
url.includes('/authkit/pat') ||
|
|
13
|
-
(backchannel !== null && url === backchannel)
|
|
38
|
+
(backchannel !== null && url === backchannel) ||
|
|
39
|
+
(agentsOAuth !== null && url.startsWith(agentsOAuth)));
|
|
14
40
|
}
|