@adonis-agora/authkit-server 0.76.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 +145 -0
- package/build/host/views/mfa-challenge.edge +42 -3
- package/build/index.d.ts +9 -1
- package/build/index.js +5 -1
- 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/define_config.d.ts +24 -0
- package/build/src/define_config.js +8 -0
- package/build/src/host/controllers/account_security_controller.js +8 -4
- package/build/src/host/controllers/interaction_controller.d.ts +13 -1
- package/build/src/host/controllers/interaction_controller.js +352 -24
- 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 +4 -0
- package/build/src/host/i18n.js +10 -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/register_auth_host.js +12 -0
- 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/provider/oidc_service.js +6 -2
- package/build/stubs/ui/react/pages/mfa-challenge.tsx +207 -37
- package/package.json +2 -1
- package/stubs/ui/react/pages/mfa-challenge.tsx +207 -37
|
@@ -441,6 +441,18 @@ export function registerAuthHost(router, opts = {}) {
|
|
|
441
441
|
router.post('/auth/interaction/:uid/identifier', [C.interaction, 'identifier']);
|
|
442
442
|
withLogin(router.post('/auth/interaction/:uid/login', [C.interaction, 'login']));
|
|
443
443
|
withLogin(router.post('/auth/interaction/:uid/mfa', [C.interaction, 'mfaVerify']));
|
|
444
|
+
const customMfaBeginRoute = router.post('/auth/interaction/:uid/mfa/custom/:method/begin', [
|
|
445
|
+
C.interaction,
|
|
446
|
+
'customMfaBegin',
|
|
447
|
+
]);
|
|
448
|
+
withLogin(customMfaBeginRoute);
|
|
449
|
+
customMfaBeginRoute.as('authkit.mfa.custom.begin');
|
|
450
|
+
const customMfaVerifyRoute = router.post('/auth/interaction/:uid/mfa/custom/:method/verify', [
|
|
451
|
+
C.interaction,
|
|
452
|
+
'customMfaVerify',
|
|
453
|
+
]);
|
|
454
|
+
withLogin(customMfaVerifyRoute);
|
|
455
|
+
customMfaVerifyRoute.as('authkit.mfa.custom.verify');
|
|
444
456
|
// Troca de senha obrigatória quando a senha expirou (password expiration gate).
|
|
445
457
|
withLogin(router.post('/auth/interaction/:uid/password-expired', [
|
|
446
458
|
C.interaction,
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { HttpContext } from '@adonisjs/core/http';
|
|
2
|
+
/** Provider-neutral delivery input. Only the host generates and verifies the OTP. */
|
|
3
|
+
export interface WhatsappCodeInput {
|
|
4
|
+
phone: string;
|
|
5
|
+
code: string;
|
|
6
|
+
locale: string;
|
|
7
|
+
expiresInSeconds: number;
|
|
8
|
+
/** Optional host-localized copy for transports that send free-form messages. */
|
|
9
|
+
text?: string;
|
|
10
|
+
}
|
|
11
|
+
/** Implement this interface to support any WhatsApp provider. Reject on failed delivery. */
|
|
12
|
+
export interface WhatsappCodeSender {
|
|
13
|
+
sendCode(input: WhatsappCodeInput): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
/** A configured instance, or an injectable class resolved by the request container. */
|
|
16
|
+
export type WhatsappCodeSenderConstructor = new (...args: never[]) => WhatsappCodeSender;
|
|
17
|
+
export type WhatsappCodeSenderBinding = WhatsappCodeSender | WhatsappCodeSenderConstructor;
|
|
18
|
+
export declare function resolveWhatsappCodeSender(resolver: Pick<HttpContext['containerResolver'], 'make'>, binding: WhatsappCodeSenderBinding): Promise<WhatsappCodeSender>;
|
|
19
|
+
export { MetaWhatsappCodeSender, type MetaWhatsappCodeSenderOptions, } from './whatsapp_senders/meta_whatsapp_code_sender.js';
|
|
20
|
+
export { WhatsmiauCodeSender, type WhatsmiauCodeSenderOptions, } from './whatsapp_senders/whatsmiau_code_sender.js';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export async function resolveWhatsappCodeSender(resolver, binding) {
|
|
2
|
+
const sender = typeof binding === 'function' ? await resolver.make(binding) : binding;
|
|
3
|
+
if (!sender || typeof sender.sendCode !== 'function') {
|
|
4
|
+
throw new Error('Invalid WhatsApp code sender: expected sendCode(input)');
|
|
5
|
+
}
|
|
6
|
+
return sender;
|
|
7
|
+
}
|
|
8
|
+
export { MetaWhatsappCodeSender, } from './whatsapp_senders/meta_whatsapp_code_sender.js';
|
|
9
|
+
export { WhatsmiauCodeSender, } from './whatsapp_senders/whatsmiau_code_sender.js';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Never include configuration values in validation errors. */
|
|
2
|
+
export declare function requireString(value: string, field: string): void;
|
|
3
|
+
export declare function recipientPhone(phone: string): string;
|
|
4
|
+
/** Response bodies and fetch causes can contain credentials and OTPs: discard both. */
|
|
5
|
+
export declare function postJson(provider: string, url: URL, headers: Record<string, string>, body: unknown): Promise<void>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** Never include configuration values in validation errors. */
|
|
2
|
+
export function requireString(value, field) {
|
|
3
|
+
if (typeof value !== 'string' || !value.trim() || /[\r\n]/.test(value)) {
|
|
4
|
+
throw new Error(`Invalid WhatsApp sender ${field}`);
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export function recipientPhone(phone) {
|
|
8
|
+
if (typeof phone !== 'string' || !/^\+?[1-9]\d{6,14}$/.test(phone)) {
|
|
9
|
+
throw new Error('Invalid WhatsApp recipient phone');
|
|
10
|
+
}
|
|
11
|
+
return phone.replace(/^\+/, '');
|
|
12
|
+
}
|
|
13
|
+
/** Response bodies and fetch causes can contain credentials and OTPs: discard both. */
|
|
14
|
+
export async function postJson(provider, url, headers, body) {
|
|
15
|
+
let response;
|
|
16
|
+
try {
|
|
17
|
+
response = await fetch(url, {
|
|
18
|
+
method: 'POST',
|
|
19
|
+
headers: { 'Content-Type': 'application/json', ...headers },
|
|
20
|
+
body: JSON.stringify(body),
|
|
21
|
+
signal: AbortSignal.timeout(15_000),
|
|
22
|
+
redirect: 'error',
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
throw new Error(`WhatsApp code delivery failed (${provider})`);
|
|
27
|
+
}
|
|
28
|
+
// Do not parse or log the body, even when a provider echoes the submitted code.
|
|
29
|
+
void response.body?.cancel().catch(() => { });
|
|
30
|
+
if (!response.ok) {
|
|
31
|
+
throw new Error(`WhatsApp code delivery failed (${provider}, HTTP ${response.status})`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { WhatsappCodeInput, WhatsappCodeSender } from '../whatsapp_code_sender.js';
|
|
2
|
+
export interface MetaWhatsappCodeSenderOptions {
|
|
3
|
+
accessToken: string;
|
|
4
|
+
phoneNumberId: string;
|
|
5
|
+
/** Explicit supported Graph API version, for example v23.0. */
|
|
6
|
+
apiVersion: string;
|
|
7
|
+
/** An approved authentication template with an OTP URL button. */
|
|
8
|
+
templateName: string;
|
|
9
|
+
/** Overrides the host locale when the template uses a different language. */
|
|
10
|
+
languageCode?: string;
|
|
11
|
+
}
|
|
12
|
+
/** Meta Cloud API authentication template transport (copy-code / OTP URL button). */
|
|
13
|
+
export declare class MetaWhatsappCodeSender implements WhatsappCodeSender {
|
|
14
|
+
private readonly accessToken;
|
|
15
|
+
private readonly endpoint;
|
|
16
|
+
private readonly templateName;
|
|
17
|
+
private readonly languageCode?;
|
|
18
|
+
constructor(options: MetaWhatsappCodeSenderOptions);
|
|
19
|
+
sendCode(input: WhatsappCodeInput): Promise<void>;
|
|
20
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { postJson, recipientPhone, requireString } from './http.js';
|
|
2
|
+
const LANGUAGE_CODES = { 'pt-BR': 'pt_BR', en: 'en_US', es: 'es' };
|
|
3
|
+
/** Meta Cloud API authentication template transport (copy-code / OTP URL button). */
|
|
4
|
+
export class MetaWhatsappCodeSender {
|
|
5
|
+
accessToken;
|
|
6
|
+
endpoint;
|
|
7
|
+
templateName;
|
|
8
|
+
languageCode;
|
|
9
|
+
constructor(options) {
|
|
10
|
+
requireString(options.accessToken, 'accessToken');
|
|
11
|
+
if (typeof options.apiVersion !== 'string' || !/^v\d+\.\d+$/.test(options.apiVersion)) {
|
|
12
|
+
throw new Error('Invalid WhatsApp sender apiVersion');
|
|
13
|
+
}
|
|
14
|
+
if (typeof options.phoneNumberId !== 'string' || !/^\d+$/.test(options.phoneNumberId)) {
|
|
15
|
+
throw new Error('Invalid WhatsApp sender phoneNumberId');
|
|
16
|
+
}
|
|
17
|
+
if (typeof options.templateName !== 'string' || !/^[a-z0-9_]+$/.test(options.templateName)) {
|
|
18
|
+
throw new Error('Invalid WhatsApp sender templateName');
|
|
19
|
+
}
|
|
20
|
+
if (options.languageCode !== undefined) {
|
|
21
|
+
requireString(options.languageCode, 'languageCode');
|
|
22
|
+
}
|
|
23
|
+
this.endpoint = new URL(`https://graph.facebook.com/${options.apiVersion}/${options.phoneNumberId}/messages`);
|
|
24
|
+
this.accessToken = options.accessToken;
|
|
25
|
+
this.templateName = options.templateName;
|
|
26
|
+
this.languageCode = options.languageCode;
|
|
27
|
+
}
|
|
28
|
+
async sendCode(input) {
|
|
29
|
+
await postJson('Meta', this.endpoint, { Authorization: `Bearer ${this.accessToken}` }, {
|
|
30
|
+
messaging_product: 'whatsapp',
|
|
31
|
+
recipient_type: 'individual',
|
|
32
|
+
to: recipientPhone(input.phone),
|
|
33
|
+
type: 'template',
|
|
34
|
+
template: {
|
|
35
|
+
name: this.templateName,
|
|
36
|
+
language: {
|
|
37
|
+
code: this.languageCode ?? LANGUAGE_CODES[input.locale] ?? input.locale.replace(/-/g, '_'),
|
|
38
|
+
},
|
|
39
|
+
components: [
|
|
40
|
+
{ type: 'body', parameters: [{ type: 'text', text: input.code }] },
|
|
41
|
+
{
|
|
42
|
+
type: 'button',
|
|
43
|
+
sub_type: 'url',
|
|
44
|
+
index: '0',
|
|
45
|
+
parameters: [{ type: 'text', text: input.code }],
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { WhatsappCodeInput, WhatsappCodeSender } from '../whatsapp_code_sender.js';
|
|
2
|
+
export interface WhatsmiauCodeSenderOptions {
|
|
3
|
+
apiKey: string;
|
|
4
|
+
instanceName: string;
|
|
5
|
+
/** Includes the API version path. Defaults to Whatsmiau Cloud's /v2 endpoint. */
|
|
6
|
+
baseUrl?: string;
|
|
7
|
+
}
|
|
8
|
+
/** Whatsmiau Cloud v2 / Evolution-compatible free-form text transport. */
|
|
9
|
+
export declare class WhatsmiauCodeSender implements WhatsappCodeSender {
|
|
10
|
+
private readonly apiKey;
|
|
11
|
+
private readonly endpoint;
|
|
12
|
+
constructor(options: WhatsmiauCodeSenderOptions);
|
|
13
|
+
sendCode(input: WhatsappCodeInput): Promise<void>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { postJson, recipientPhone, requireString } from './http.js';
|
|
2
|
+
/** Whatsmiau Cloud v2 / Evolution-compatible free-form text transport. */
|
|
3
|
+
export class WhatsmiauCodeSender {
|
|
4
|
+
apiKey;
|
|
5
|
+
endpoint;
|
|
6
|
+
constructor(options) {
|
|
7
|
+
requireString(options.apiKey, 'apiKey');
|
|
8
|
+
if (typeof options.instanceName !== 'string' ||
|
|
9
|
+
!/^[A-Za-z0-9_-]+$/.test(options.instanceName)) {
|
|
10
|
+
throw new Error('Invalid WhatsApp sender instanceName');
|
|
11
|
+
}
|
|
12
|
+
let baseUrl;
|
|
13
|
+
try {
|
|
14
|
+
baseUrl = new URL(options.baseUrl ?? 'https://api.whatsmiau.dev/v2');
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
throw new Error('Invalid WhatsApp sender baseUrl');
|
|
18
|
+
}
|
|
19
|
+
if (!['http:', 'https:'].includes(baseUrl.protocol) ||
|
|
20
|
+
baseUrl.username ||
|
|
21
|
+
baseUrl.password ||
|
|
22
|
+
baseUrl.search ||
|
|
23
|
+
baseUrl.hash) {
|
|
24
|
+
throw new Error('Invalid WhatsApp sender baseUrl');
|
|
25
|
+
}
|
|
26
|
+
baseUrl.pathname = `${baseUrl.pathname.replace(/\/+$/, '')}/message/sendText/${options.instanceName}`;
|
|
27
|
+
this.endpoint = baseUrl;
|
|
28
|
+
this.apiKey = options.apiKey;
|
|
29
|
+
}
|
|
30
|
+
async sendCode(input) {
|
|
31
|
+
await postJson('Whatsmiau', this.endpoint, { apikey: this.apiKey }, {
|
|
32
|
+
number: recipientPhone(input.phone),
|
|
33
|
+
text: input.text ?? input.code,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -137,8 +137,12 @@ export class OidcService {
|
|
|
137
137
|
claims: async (_use, _scope) => {
|
|
138
138
|
const base = {
|
|
139
139
|
sub: user.id,
|
|
140
|
-
|
|
141
|
-
|
|
140
|
+
...(user.email
|
|
141
|
+
? {
|
|
142
|
+
email: user.email,
|
|
143
|
+
email_verified: (await config.accountStore.isEmailVerified?.(user.id)) ?? false,
|
|
144
|
+
}
|
|
145
|
+
: {}),
|
|
142
146
|
name: user.name,
|
|
143
147
|
picture: user.avatarUrl,
|
|
144
148
|
};
|
|
@@ -2,34 +2,124 @@
|
|
|
2
2
|
exports({ to: app.makePath('inertia/pages/authkit/mfa-challenge.tsx') });
|
|
3
3
|
}
|
|
4
4
|
|
|
5
|
-
import
|
|
5
|
+
import { useRef, useState } from 'react';
|
|
6
|
+
import AuthShell, { type AuthBrand } from '../../components/auth_shell';
|
|
7
|
+
|
|
8
|
+
interface CustomMethod {
|
|
9
|
+
id: string;
|
|
10
|
+
factorId: string;
|
|
11
|
+
label: string;
|
|
12
|
+
fields?: {
|
|
13
|
+
name: string;
|
|
14
|
+
label: string;
|
|
15
|
+
type?: 'text' | 'password';
|
|
16
|
+
inputMode?: 'numeric' | 'text';
|
|
17
|
+
autoComplete?: string;
|
|
18
|
+
}[];
|
|
19
|
+
requiresBegin: boolean;
|
|
20
|
+
started: boolean;
|
|
21
|
+
beginUrl: string;
|
|
22
|
+
verifyUrl: string;
|
|
23
|
+
}
|
|
6
24
|
|
|
7
25
|
const inputClass =
|
|
8
|
-
'w-full rounded-lg border border-gray-300 px-3 py-2 text-
|
|
26
|
+
'w-full rounded-lg border border-gray-300 px-3 py-2 text-base outline-none transition focus:border-gray-900 focus:ring-2 focus:ring-gray-900';
|
|
27
|
+
const buttonClass =
|
|
28
|
+
'w-full rounded-lg bg-gray-900 px-3 py-2.5 text-sm font-semibold text-white transition hover:opacity-90 disabled:opacity-50';
|
|
29
|
+
const secondaryButtonClass =
|
|
30
|
+
'w-full rounded-lg border border-gray-300 px-3 py-2.5 text-sm font-semibold text-gray-700 transition hover:bg-gray-50 disabled:opacity-50';
|
|
9
31
|
|
|
10
32
|
export default function AuthkitMfaChallenge({
|
|
11
33
|
uid,
|
|
12
34
|
csrfToken,
|
|
13
35
|
error,
|
|
36
|
+
brand,
|
|
37
|
+
messages = {},
|
|
38
|
+
noEnrollment = false,
|
|
39
|
+
otpLocked = false,
|
|
40
|
+
totpAvailable = true,
|
|
41
|
+
passkeyAvailable = false,
|
|
42
|
+
trustedDevicesEnabled = false,
|
|
43
|
+
trustedDeviceDays = 30,
|
|
44
|
+
customMfaMethods = [],
|
|
45
|
+
completedMfaMethods = [],
|
|
46
|
+
requiredMfaFactors = 2,
|
|
14
47
|
}: {
|
|
15
48
|
uid: string;
|
|
16
49
|
csrfToken: string;
|
|
17
50
|
error?: string;
|
|
51
|
+
brand?: AuthBrand;
|
|
52
|
+
messages?: Record<string, string>;
|
|
53
|
+
noEnrollment?: boolean;
|
|
54
|
+
otpLocked?: boolean;
|
|
55
|
+
totpAvailable?: boolean;
|
|
56
|
+
passkeyAvailable?: boolean;
|
|
57
|
+
trustedDevicesEnabled?: boolean;
|
|
58
|
+
trustedDeviceDays?: number;
|
|
59
|
+
customMfaMethods?: CustomMethod[];
|
|
60
|
+
completedMfaMethods?: string[];
|
|
61
|
+
requiredMfaFactors?: number;
|
|
18
62
|
}) {
|
|
63
|
+
const [passkeyPending, setPasskeyPending] = useState(false);
|
|
64
|
+
const [passkeyError, setPasskeyError] = useState(false);
|
|
65
|
+
const [trustDevice, setTrustDevice] = useState(false);
|
|
66
|
+
const passkeyForm = useRef<HTMLFormElement>(null);
|
|
67
|
+
const passkeyResponse = useRef<HTMLInputElement>(null);
|
|
68
|
+
const t = (key: string, fallback: string) => messages[`mfa_challenge.${key}`] ?? fallback;
|
|
69
|
+
const showTotp = !noEnrollment && !otpLocked && totpAvailable;
|
|
70
|
+
const showPasskey = !noEnrollment && passkeyAvailable;
|
|
71
|
+
const remainingMethods = noEnrollment
|
|
72
|
+
? []
|
|
73
|
+
: customMfaMethods.filter((method) => !completedMfaMethods.includes(method.id));
|
|
74
|
+
|
|
75
|
+
async function authenticatePasskey() {
|
|
76
|
+
setPasskeyPending(true);
|
|
77
|
+
setPasskeyError(false);
|
|
78
|
+
try {
|
|
79
|
+
const result = await fetch(`/auth/interaction/${uid}/passkey/options`, {
|
|
80
|
+
method: 'POST',
|
|
81
|
+
headers: { 'content-type': 'application/json', 'x-csrf-token': csrfToken },
|
|
82
|
+
body: JSON.stringify({}),
|
|
83
|
+
});
|
|
84
|
+
if (!result.ok) throw new Error('Passkey options failed');
|
|
85
|
+
const moduleUrl = '/authkit/assets/webauthn.js';
|
|
86
|
+
const { startAuthentication } = await import(/* @vite-ignore */ moduleUrl);
|
|
87
|
+
const assertion = await startAuthentication({ optionsJSON: await result.json() });
|
|
88
|
+
if (!passkeyResponse.current || !passkeyForm.current) throw new Error('Missing passkey form');
|
|
89
|
+
passkeyResponse.current.value = JSON.stringify(assertion);
|
|
90
|
+
passkeyForm.current.submit();
|
|
91
|
+
} catch {
|
|
92
|
+
setPasskeyError(true);
|
|
93
|
+
setPasskeyPending(false);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
19
97
|
return (
|
|
20
|
-
<AuthShell>
|
|
21
|
-
<
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
98
|
+
<AuthShell brand={brand}>
|
|
99
|
+
<h1 className="text-xl font-semibold text-gray-900">
|
|
100
|
+
{t('title', 'Verificação multifator')}
|
|
101
|
+
</h1>
|
|
102
|
+
<p className="mt-1 text-sm text-gray-500">
|
|
103
|
+
{t('intro', 'Verifique sua identidade usando um método disponível.')}
|
|
104
|
+
</p>
|
|
105
|
+
{requiredMfaFactors > 2 && (
|
|
106
|
+
<p className="mt-3 text-sm text-gray-600" role="status">
|
|
107
|
+
{t('progress', '{completed} de {required} fatores verificados.')
|
|
108
|
+
.replace('{completed}', String(1 + completedMfaMethods.length))
|
|
109
|
+
.replace('{required}', String(requiredMfaFactors))}
|
|
26
110
|
</p>
|
|
111
|
+
)}
|
|
112
|
+
{error && (
|
|
113
|
+
<p className="mt-4 text-sm text-red-600" role="alert">
|
|
114
|
+
{error}
|
|
115
|
+
</p>
|
|
116
|
+
)}
|
|
27
117
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
118
|
+
{showTotp && (
|
|
119
|
+
<form method="POST" action={`/auth/interaction/${uid}/mfa`} className="mt-6">
|
|
120
|
+
<input type="hidden" name="_csrf" value={csrfToken} />
|
|
31
121
|
<label htmlFor="code" className="mb-1 block text-sm font-medium text-gray-700">
|
|
32
|
-
Código
|
|
122
|
+
{t('code_label', 'Código')}
|
|
33
123
|
</label>
|
|
34
124
|
<input
|
|
35
125
|
id="code"
|
|
@@ -40,33 +130,113 @@ export default function AuthkitMfaChallenge({
|
|
|
40
130
|
maxLength={6}
|
|
41
131
|
className={inputClass}
|
|
42
132
|
/>
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm outline-none focus:border-gray-900"
|
|
61
|
-
/>
|
|
62
|
-
<button
|
|
63
|
-
type="submit"
|
|
64
|
-
className="mt-3 w-full rounded-lg border border-gray-300 py-2.5 text-sm font-semibold text-gray-700 transition hover:bg-gray-50"
|
|
65
|
-
>
|
|
66
|
-
Entrar com código de recuperação
|
|
133
|
+
{trustedDevicesEnabled && (
|
|
134
|
+
<label className="mt-4 flex items-center gap-2 text-sm text-gray-600">
|
|
135
|
+
<input
|
|
136
|
+
type="checkbox"
|
|
137
|
+
name="trustDevice"
|
|
138
|
+
value="on"
|
|
139
|
+
checked={trustDevice}
|
|
140
|
+
onChange={(event) => setTrustDevice(event.target.checked)}
|
|
141
|
+
/>
|
|
142
|
+
{t('trust_device', 'Confiar neste dispositivo por {days} dias').replace(
|
|
143
|
+
'{days}',
|
|
144
|
+
String(trustedDeviceDays),
|
|
145
|
+
)}
|
|
146
|
+
</label>
|
|
147
|
+
)}
|
|
148
|
+
<button type="submit" className={`mt-6 ${buttonClass}`}>
|
|
149
|
+
{t('submit', 'Verificar')}
|
|
67
150
|
</button>
|
|
68
151
|
</form>
|
|
69
|
-
|
|
152
|
+
)}
|
|
153
|
+
|
|
154
|
+
{remainingMethods.map((method) => (
|
|
155
|
+
<section key={method.id} className="mt-6">
|
|
156
|
+
<h2 className="text-sm font-semibold text-gray-900">{method.label}</h2>
|
|
157
|
+
{method.requiresBegin && !method.started ? (
|
|
158
|
+
<form method="POST" action={method.beginUrl} className="mt-3">
|
|
159
|
+
<input type="hidden" name="_csrf" value={csrfToken} />
|
|
160
|
+
<button type="submit" className={secondaryButtonClass}>
|
|
161
|
+
{t('start', 'Iniciar verificação')}
|
|
162
|
+
</button>
|
|
163
|
+
</form>
|
|
164
|
+
) : (
|
|
165
|
+
<form method="POST" action={method.verifyUrl} className="mt-3 space-y-3">
|
|
166
|
+
<input type="hidden" name="_csrf" value={csrfToken} />
|
|
167
|
+
{(method.fields ?? []).map((field) => (
|
|
168
|
+
<div key={field.name}>
|
|
169
|
+
<label
|
|
170
|
+
htmlFor={`custom-${method.id}-${field.name}`}
|
|
171
|
+
className="mb-1 block text-sm font-medium text-gray-700"
|
|
172
|
+
>
|
|
173
|
+
{field.label}
|
|
174
|
+
</label>
|
|
175
|
+
<input
|
|
176
|
+
id={`custom-${method.id}-${field.name}`}
|
|
177
|
+
name={field.name}
|
|
178
|
+
type={field.type ?? 'text'}
|
|
179
|
+
inputMode={field.inputMode ?? 'text'}
|
|
180
|
+
autoComplete={field.autoComplete ?? 'off'}
|
|
181
|
+
className={inputClass}
|
|
182
|
+
/>
|
|
183
|
+
</div>
|
|
184
|
+
))}
|
|
185
|
+
<button type="submit" className={buttonClass}>
|
|
186
|
+
{t('submit', 'Verificar')}
|
|
187
|
+
</button>
|
|
188
|
+
</form>
|
|
189
|
+
)}
|
|
190
|
+
</section>
|
|
191
|
+
))}
|
|
192
|
+
|
|
193
|
+
{showPasskey && (
|
|
194
|
+
<div className="mt-4">
|
|
195
|
+
<form ref={passkeyForm} method="POST" action={`/auth/interaction/${uid}/passkey/verify`}>
|
|
196
|
+
<input type="hidden" name="_csrf" value={csrfToken} />
|
|
197
|
+
<input ref={passkeyResponse} type="hidden" name="response" />
|
|
198
|
+
<input type="hidden" name="trustDevice" value={trustDevice ? 'on' : ''} />
|
|
199
|
+
<button
|
|
200
|
+
type="button"
|
|
201
|
+
onClick={authenticatePasskey}
|
|
202
|
+
disabled={passkeyPending}
|
|
203
|
+
aria-busy={passkeyPending}
|
|
204
|
+
className={secondaryButtonClass}
|
|
205
|
+
>
|
|
206
|
+
{t('passkey_button', 'Usar passkey')}
|
|
207
|
+
</button>
|
|
208
|
+
</form>
|
|
209
|
+
{passkeyError && (
|
|
210
|
+
<p className="mt-3 text-sm text-red-600" role="alert">
|
|
211
|
+
{t('passkey_error', 'Não foi possível autenticar com a passkey. Tente novamente.')}
|
|
212
|
+
</p>
|
|
213
|
+
)}
|
|
214
|
+
</div>
|
|
215
|
+
)}
|
|
216
|
+
|
|
217
|
+
{showTotp && (
|
|
218
|
+
<details className="mt-6 text-sm text-gray-600">
|
|
219
|
+
<summary className="cursor-pointer hover:underline">
|
|
220
|
+
{t('recovery_summary', 'Usar um código de recuperação')}
|
|
221
|
+
</summary>
|
|
222
|
+
<form method="POST" action={`/auth/interaction/${uid}/mfa`} className="mt-3">
|
|
223
|
+
<input type="hidden" name="_csrf" value={csrfToken} />
|
|
224
|
+
<label htmlFor="recovery-code" className="mb-1 block text-sm font-medium text-gray-700">
|
|
225
|
+
{t('recovery_summary', 'Usar um código de recuperação')}
|
|
226
|
+
</label>
|
|
227
|
+
<input
|
|
228
|
+
id="recovery-code"
|
|
229
|
+
name="recoveryCode"
|
|
230
|
+
autoComplete="off"
|
|
231
|
+
placeholder="xxxxx-xxxxx"
|
|
232
|
+
className={inputClass}
|
|
233
|
+
/>
|
|
234
|
+
<button type="submit" className={`mt-3 ${secondaryButtonClass}`}>
|
|
235
|
+
{t('recovery_submit', 'Entrar com código de recuperação')}
|
|
236
|
+
</button>
|
|
237
|
+
</form>
|
|
238
|
+
</details>
|
|
239
|
+
)}
|
|
70
240
|
</AuthShell>
|
|
71
241
|
);
|
|
72
242
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adonis-agora/authkit-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.77.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",
|
|
@@ -153,6 +153,7 @@
|
|
|
153
153
|
"react-dom": "19.2.8",
|
|
154
154
|
"react-error-boundary": "6.1.5",
|
|
155
155
|
"recharts": "3.10.1",
|
|
156
|
+
"reflect-metadata": "0.2.2",
|
|
156
157
|
"tempura": "^0.4.1",
|
|
157
158
|
"typescript": "7.0.2",
|
|
158
159
|
"vite": "8.2.2"
|