Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions endpoints/registerPasskeyEndpoints.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import type { IHttpServer } from "adminforth";

export function registerPasskeyEndpoints(server: IHttpServer, handlers: any): void {
server.endpoint({
method: 'POST',
path: `/plugin/passkeys/registerPasskeyRequest`,
noAuth: false,
handler: async ({ body, adminUser, response, cookies, headers }) =>
handlers.registerPasskeyRequest({ body, adminUser, response, cookies, headers }),
});

server.endpoint({
method: 'POST',
path: `/plugin/passkeys/finishRegisteringPasskey`,
noAuth: false,
handler: async ({ body, adminUser, cookies }) => handlers.finishRegisteringPasskey({ body, adminUser, cookies }),
});

server.endpoint({
method: 'POST',
path: `/plugin/passkeys/signInRequest`,
noAuth: true,
handler: async ({ response }) => handlers.createSignInRequest({ response }),
});

server.endpoint({
method: 'GET',
path: `/plugin/passkeys/getPasskeys`,
noAuth: false,
handler: async ({ adminUser }) => handlers.getPasskeys({ adminUser }),
});

server.endpoint({
method: 'DELETE',
path: `/plugin/passkeys/deletePasskey`,
noAuth: false,
handler: async ({ body, adminUser }) => handlers.deletePasskey({ body, adminUser }),
});

server.endpoint({
method: 'POST',
path: `/plugin/passkeys/renamePasskey`,
noAuth: false,
handler: async ({ body, adminUser }) => handlers.renamePasskey({ body, adminUser }),
});

server.endpoint({
method: 'POST',
path: `/plugin/passkeys/checkIfUserHasPasskeys`,
noAuth: true,
handler: async ({ cookies }) => handlers.checkIfUserHasPasskeys({ cookies }),
});

server.endpoint({
method: 'POST',
path: `/plugin/passkeys/resolveVerifyAuto`,
noAuth: false,
handler: async ({ body, adminUser, response, cookies, headers }) =>
handlers.resolveVerifyAuto({ body, adminUser, response, cookies, headers }),
});
}
45 changes: 45 additions & 0 deletions endpoints/registerTwoFaEndpoints.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import type { IHttpServer } from "adminforth";

export function registerTwoFaEndpoints(server: IHttpServer, handlers: any): void {
server.endpoint({
method: 'POST',
path: `/plugin/twofa/initSetup`,
noAuth: true,
handler: async ({ cookies }) => handlers.initSetup({ cookies }),
});

server.endpoint({
method: 'POST',
path: `/plugin/twofa/confirmLogin`,
noAuth: true,
handler: async ({ body, response, cookies }) => handlers.confirmLogin({ body, response, cookies }),
});

server.endpoint({
method: 'POST',
path: `/plugin/twofa/confirmLoginWithPasskey`,
noAuth: true,
handler: async ({ body, response, cookies, headers, requestUrl, query }) =>
handlers.confirmLoginWithPasskey({ body, response, cookies, headers, requestUrl, query }),
});

server.endpoint({
method: "GET",
path: "/plugin/twofa/skip-allow",
noAuth: true,
handler: async ({ cookies }) => handlers.skipAllow({ cookies }),
});

server.endpoint({
method: "GET",
path: "/plugin/twofa/skip-allow-modal",
handler: async ({ adminUser, headers, cookies }) => handlers.skipAllowModal({ adminUser, headers, cookies }),
});

server.endpoint({
method: 'POST',
path: `/plugin/twofa/verify`,
noAuth: false,
handler: async ({ adminUser, body }) => handlers.verifyTotp({ adminUser, body }),
});
}
102 changes: 102 additions & 0 deletions handlers/createPasskeyHandlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import type { HttpExtra } from "adminforth";
import { errorMessage } from "../utils/errors.js";

export function createPasskeyHandlers(ctx: any) {
return {
registerPasskeyRequest: async ({ body, adminUser, response, cookies, headers }) => {
const mode = body?.mode;

const confirmationResult = body?.confirmationResult;
const verificationResult = await ctx.verifyMfaConfirmation(confirmationResult, {
adminUser: adminUser,
userPk: adminUser.pk,
cookies: cookies,
response: response,
extra: {
headers,
} as HttpExtra
});
if (!verificationResult || !('ok' in verificationResult)) {
return { ok: false, error: 'error' in verificationResult ? verificationResult.error : 'Verification failed' };
}

return ctx.passkeyService.createRegisterPasskeyRequest(mode, adminUser, response);
},

finishRegisteringPasskey: async ({ body, adminUser, cookies }) => {
return ctx.passkeyService.finishRegisteringPasskey(body, adminUser, cookies);
},

createSignInRequest: async ({ response }) => {
return ctx.passkeyService.createSignInRequest(response);
},

getPasskeys: async ({ adminUser }) => {
return ctx.passkeyService.getPasskeys(adminUser);
},

deletePasskey: async ({ body, adminUser }) => {
return ctx.passkeyService.deletePasskey(body.passkeyId, adminUser);
},

renamePasskey: async ({ body, adminUser }) => {
return ctx.passkeyService.renamePasskey(body.passkeyId, body.newName, adminUser);
},

checkIfUserHasPasskeys: async ({ cookies }) => {
return ctx.passkeyService.checkIfUserHasPasskeys(cookies);
},

resolveVerifyAuto: async ({ body, adminUser, response, cookies, headers }) => {
const sessionsIds = body?.sessionsIds;
const confirmationResult = body?.confirmationResult;
const idsToResolve = Array.isArray(sessionsIds) ? sessionsIds : [];

const resolveAllIdsAsFailed = (message) => {
for (const id of idsToResolve) {
ctx.autoVerify.resolveResponse(id, { ok: false, error: message });
}
return { ok: false, error: message };
}

try {
if (!idsToResolve.length || !confirmationResult) {
return(resolveAllIdsAsFailed('Confirmation window was closed or did not return required data'));
}

for (const id of idsToResolve) {
const validationResult = await ctx.adminforth.auth.verify(id, 'auto2FA', false);
if (!validationResult) {
return(resolveAllIdsAsFailed('Invalid session ID or confirmation result'));
}
if (validationResult.adminUserPk !== adminUser.pk) {
return(resolveAllIdsAsFailed('Session does not belong to the authenticated user'));
}
}

const verificationResult = await ctx.verifyMfaConfirmation(confirmationResult, {
adminUser: adminUser,
userPk: adminUser.pk,
cookies: cookies,
response: response,
extra: {
headers: headers,
} as HttpExtra
});
if ( !verificationResult || !('ok' in verificationResult) ) {
return(resolveAllIdsAsFailed('Verification failed'));
}
if ('ok' in verificationResult && verificationResult.ok){
for (const id of idsToResolve) {
ctx.autoVerify.resolveResponse(id, { ok: true, passkeyConfirmed: verificationResult });
}
return { ok: true };
}
return(resolveAllIdsAsFailed('Verification failed'));
} catch (error) {
console.error('[AdminForth 2FA] Error resolving automatic 2FA verification', error);
return(resolveAllIdsAsFailed(errorMessage(error)));
}
},
};
}
161 changes: 161 additions & 0 deletions handlers/createTwoFaHandlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
export function createTwoFaHandlers(ctx: any) {
return {
initSetup: async ({ cookies }) => {
const toReturn = {totpJWT:null,status:'ok',}
const totpTemporaryJWT = ctx.cookieService.getTotpTemporary(cookies);
if (totpTemporaryJWT){
toReturn.totpJWT = totpTemporaryJWT
}
return toReturn
},

confirmLogin: async ({ body, response, cookies }) => {
const totpTemporaryJWT = ctx.cookieService.getTotpTemporary(cookies);
if (!totpTemporaryJWT) {
return { error: 'Login session expired. Please log in again.' }
}
const decoded = await ctx.cookieService.verifyTotpTemporary(cookies);
if (!decoded) {
return { error: 'Login session expired. Please log in again.' }
}

if (decoded.newSecret) {
const verified = body.skip && decoded.userCanSkipSetup ? true : ctx.totpService.verifySecret(decoded.newSecret, body.code);
if (verified) {
if (!body.skip) {
await ctx.totpService.saveSecret(decoded.pk, decoded.newSecret);
}
ctx.cookieService.removeTotpTemporary(response)
ctx.cookieService.setAuthCookie({expireInDuration: decoded.sessionDuration, response, username:decoded.userName, pk:decoded.pk})
return { status: 'ok', allowedLogin: true }
} else {
return {error: 'Wrong or expired OTP code'}
}
}

let verified = null;
if (body.usePasskey && ctx.options.passkeys) {
const cookiesValidationResult = await ctx.passkeyService.validateCookiesForPasskeyLogin(cookies);
if (!cookiesValidationResult.ok) {
return { error: cookiesValidationResult.error };
}
const res = await ctx.passkeyService.verifyPasskeyResponse(body.passkeyOptions, decoded.pk, cookiesValidationResult.decodedPasskeysCookies);
if (res.ok && res.passkeyConfirmed) {
verified = true;
}
} else {
const verificationResult = await ctx.totpService.verifyUserCode(decoded.pk, body.code);
verified = 'ok' in verificationResult && verificationResult.ok;
}
if (verified) {
ctx.cookieService.removeTotpTemporary(response)
ctx.cookieService.setAuthCookie({expireInDuration: decoded.sessionDuration, response, username:decoded.userName, pk:decoded.pk})
return { status: 'ok', allowedLogin: true }
} else {
response.setStatus(403, "Wrong or expired TOTP code");
return {error: 'Wrong or expired TOTP code', }
}
},

confirmLoginWithPasskey: async ({ body, response, cookies, headers, requestUrl, query }) => {
if ( ctx.options.passkeys.allowLoginWithPasskeys !== true ) {
return { error: 'Login with passkeys is not allowed' };
}

const passkeyResponse = body.passkeyResponse;
if (!passkeyResponse) {
return { error: 'Passkey response is required' };
}

const passkeyLoginResult = await ctx.passkeyService.getLoginUserByPasskeyResponse(passkeyResponse, cookies);
if (!passkeyLoginResult.ok) {
return { error: passkeyLoginResult.error };
}
const user = passkeyLoginResult.user;
const username = user[ctx.adminforth.config.auth.usernameField];

const adminUser = {
dbUser: user,
pk: user.id,
username,
};

const toReturn = { allowedLogin: true, error: '' };
const rememberMe = body?.rememberMe || false;

await ctx.adminforth.restApi.processLoginCallbacks(
adminUser,
toReturn,
response,
{
headers,
cookies,
requestUrl,
query,
body: {},
response,
meta: {
loginAllowedByPasskeyDirectSignIn: true
},
},
rememberMe ? ctx.adminforth.config.auth.rememberMeDuration || '30d' : '1d',
);

const rememberDaysAfterPasskeyLogin = (ctx.options.passkeys.rememberDaysAfterPasskeyLogin ? ctx.options.passkeys.rememberDaysAfterPasskeyLogin.toString().concat('d') : null);
if ( toReturn.allowedLogin === true ) {
ctx.cookieService.setAuthCookie({
response,
username,
pk: user.id,
expireInDuration: rememberDaysAfterPasskeyLogin ? rememberDaysAfterPasskeyLogin : ctx.adminforth.config.auth.rememberMeDuration,
});
}
return toReturn;
},

skipAllow: async ({ cookies }) => {
const decoded = await ctx.cookieService.verifyTotpTemporary(cookies);
if (!decoded) {
return { status: "error", message: "Invalid token" };
}
if (!decoded.newSecret) {
return { status: "ok", skipAllowed: false };
} else {
return {
status: "ok",
skipAllowed: decoded.userCanSkipSetup,
};
}
},

skipAllowModal: async ({ adminUser, headers, cookies }) => {
if ( ctx.options.usersFilterToApply ) {
const res = ctx.options.usersFilterToApply(adminUser);
if ( res === false ) {
return { skipAllowed: true };
}
}
if ( ctx.options.usersFilterToAllowSkipSetup ) {
const res = await ctx.checkIfSkipSetupAllowSkipVerify(adminUser);
if ( res.skipAllowed === true ) {
return { skipAllowed: true };
}
}

if ( ctx.options.stepUpMfaGracePeriodSeconds ) {
const verificationResult = await ctx.cookieService.isMfaGraceValid(headers, cookies, true);
if ( verificationResult === true ) {
return { skipAllowed: true };
}
}

return { skipAllowed: false };
},

verifyTotp: async ({ adminUser, body }) => {
if (!body?.code) return { error: 'Code is required' };

return ctx.totpService.verifyAdminUserCode(adminUser, body.code);
},
};
}
Loading