mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-01-11 19:56:44 +00:00
Merge branch 'yubikey-auth'
This commit is contained in:
commit
349e64e3a2
52 changed files with 1691 additions and 399 deletions
|
|
@ -3,7 +3,7 @@
|
|||
#
|
||||
|
||||
# Pull base image nodejs image.
|
||||
FROM public.ecr.aws/docker/library/node:23.8-alpine3.21
|
||||
FROM public.ecr.aws/docker/library/node:24.9-alpine3.21
|
||||
RUN mkdir /tmp/npm && chmod 2777 /tmp/npm && chown 1000:1000 /tmp/npm && npm config set cache /tmp/npm --global
|
||||
|
||||
RUN npm config set fetch-retries 5
|
||||
|
|
|
|||
|
|
@ -10,5 +10,5 @@
|
|||
],
|
||||
"watchOptions": {"useFsEvents": false, "interval": 500},
|
||||
"env": {"TS_NODE_TRANSPILE_ONLY": "1", "TS_NODE_FILES": "false"},
|
||||
"exec": "node --inspect=0.0.0.0:9229 -r ts-node/register/transpile-only Index.ts"
|
||||
"exec": "node -r ts-node/register/transpile-only Index.ts"
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
#
|
||||
|
||||
# Pull base image nodejs image.
|
||||
FROM public.ecr.aws/docker/library/node:23.8-alpine3.21
|
||||
FROM public.ecr.aws/docker/library/node:24.9-alpine3.21
|
||||
RUN mkdir /tmp/npm && chmod 2777 /tmp/npm && chown 1000:1000 /tmp/npm && npm config set cache /tmp/npm --global
|
||||
|
||||
RUN npm config set fetch-retries 5
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import {
|
||||
LOGIN_API_URL,
|
||||
VERIFY_TWO_FACTOR_AUTH_API_URL,
|
||||
VERIFY_TOTP_AUTH_API_URL,
|
||||
GENERATE_WEBAUTHN_AUTH_OPTIONS_API_URL,
|
||||
VERIFY_WEBAUTHN_AUTH_API_URL,
|
||||
} from "../Utils/ApiPaths";
|
||||
import Route from "Common/Types/API/Route";
|
||||
import URL from "Common/Types/API/URL";
|
||||
|
|
@ -12,17 +14,20 @@ import { DASHBOARD_URL } from "Common/UI/Config";
|
|||
import OneUptimeLogo from "Common/UI/Images/logos/OneUptimeSVG/3-transparent.svg";
|
||||
import UiAnalytics from "Common/UI/Utils/Analytics";
|
||||
import LoginUtil from "Common/UI/Utils/Login";
|
||||
import UserTwoFactorAuth from "Common/Models/DatabaseModels/UserTwoFactorAuth";
|
||||
import UserTotpAuth from "Common/Models/DatabaseModels/UserTotpAuth";
|
||||
import UserWebAuthn from "Common/Models/DatabaseModels/UserWebAuthn";
|
||||
import Navigation from "Common/UI/Utils/Navigation";
|
||||
import UserUtil from "Common/UI/Utils/User";
|
||||
import User from "Common/Models/DatabaseModels/User";
|
||||
import React from "react";
|
||||
import useAsyncEffect from "use-async-effect";
|
||||
import StaticModelList from "Common/UI/Components/ModelList/StaticModelList";
|
||||
import BasicForm from "Common/UI/Components/Forms/BasicForm";
|
||||
import API from "Common/UI/Utils/API/API";
|
||||
import HTTPErrorResponse from "Common/Types/API/HTTPErrorResponse";
|
||||
import HTTPResponse from "Common/Types/API/HTTPResponse";
|
||||
import Base64 from "Common/Utils/Base64";
|
||||
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
|
||||
import ComponentLoader from "Common/UI/Components/ComponentLoader/ComponentLoader";
|
||||
|
||||
const LoginPage: () => JSX.Element = () => {
|
||||
const apiUrl: URL = LOGIN_API_URL;
|
||||
|
|
@ -36,14 +41,32 @@ const LoginPage: () => JSX.Element = () => {
|
|||
const [showTwoFactorAuth, setShowTwoFactorAuth] =
|
||||
React.useState<boolean>(false);
|
||||
|
||||
const [twoFactorAuthList, setTwoFactorAuthList] = React.useState<
|
||||
UserTwoFactorAuth[]
|
||||
>([]);
|
||||
const [totpAuthList, setTotpAuthList] = React.useState<UserTotpAuth[]>([]);
|
||||
|
||||
const [selectedTwoFactorAuth, setSelectedTwoFactorAuth] = React.useState<
|
||||
UserTwoFactorAuth | undefined
|
||||
const [webAuthnList, setWebAuthnList] = React.useState<UserWebAuthn[]>([]);
|
||||
|
||||
const [selectedTotpAuth, setSelectedTotpAuth] = React.useState<
|
||||
UserTotpAuth | undefined
|
||||
>(undefined);
|
||||
|
||||
const [selectedWebAuthn, setSelectedWebAuthn] = React.useState<
|
||||
UserWebAuthn | undefined
|
||||
>(undefined);
|
||||
|
||||
type TwoFactorMethod = {
|
||||
type: "totp" | "webauthn";
|
||||
item: UserTotpAuth | UserWebAuthn;
|
||||
};
|
||||
|
||||
const twoFactorMethods: TwoFactorMethod[] = [
|
||||
...totpAuthList.map((item: UserTotpAuth) => {
|
||||
return { type: "totp" as const, item };
|
||||
}),
|
||||
...webAuthnList.map((item: UserWebAuthn) => {
|
||||
return { type: "webauthn" as const, item };
|
||||
}),
|
||||
];
|
||||
|
||||
const [isTwoFactorAuthLoading, setIsTwoFactorAuthLoading] =
|
||||
React.useState<boolean>(false);
|
||||
const [twofactorAuthError, setTwoFactorAuthError] =
|
||||
|
|
@ -57,6 +80,96 @@ const LoginPage: () => JSX.Element = () => {
|
|||
}
|
||||
}, []);
|
||||
|
||||
useAsyncEffect(async () => {
|
||||
if (selectedWebAuthn) {
|
||||
setIsTwoFactorAuthLoading(true);
|
||||
try {
|
||||
const result: HTTPResponse<JSONObject> = await API.post({
|
||||
url: GENERATE_WEBAUTHN_AUTH_OPTIONS_API_URL,
|
||||
data: {
|
||||
data: {
|
||||
email: initialValues["email"],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (result instanceof HTTPErrorResponse) {
|
||||
throw result;
|
||||
}
|
||||
|
||||
const data: any = result.data as any;
|
||||
|
||||
// Convert base64url strings back to Uint8Array
|
||||
data.options.challenge = Base64.base64UrlToUint8Array(
|
||||
data.options.challenge,
|
||||
);
|
||||
if (data.options.allowCredentials) {
|
||||
data.options.allowCredentials.forEach((cred: any) => {
|
||||
cred.id = Base64.base64UrlToUint8Array(cred.id);
|
||||
});
|
||||
}
|
||||
|
||||
// Use WebAuthn API
|
||||
const credential: PublicKeyCredential =
|
||||
(await navigator.credentials.get({
|
||||
publicKey: data.options,
|
||||
})) as PublicKeyCredential;
|
||||
|
||||
const assertionResponse: AuthenticatorAssertionResponse =
|
||||
credential.response as AuthenticatorAssertionResponse;
|
||||
|
||||
// Verify
|
||||
const verifyResult: HTTPResponse<JSONObject> = await API.post({
|
||||
url: VERIFY_WEBAUTHN_AUTH_API_URL,
|
||||
data: {
|
||||
data: {
|
||||
...initialValues,
|
||||
challenge: data.challenge,
|
||||
credential: {
|
||||
id: credential.id,
|
||||
rawId: Base64.uint8ArrayToBase64Url(
|
||||
new Uint8Array(credential.rawId),
|
||||
),
|
||||
response: {
|
||||
authenticatorData: Base64.uint8ArrayToBase64Url(
|
||||
new Uint8Array(assertionResponse.authenticatorData),
|
||||
),
|
||||
clientDataJSON: Base64.uint8ArrayToBase64Url(
|
||||
new Uint8Array(assertionResponse.clientDataJSON),
|
||||
),
|
||||
signature: Base64.uint8ArrayToBase64Url(
|
||||
new Uint8Array(assertionResponse.signature),
|
||||
),
|
||||
userHandle: assertionResponse.userHandle
|
||||
? Base64.uint8ArrayToBase64Url(
|
||||
new Uint8Array(assertionResponse.userHandle),
|
||||
)
|
||||
: null,
|
||||
},
|
||||
type: credential.type,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (verifyResult instanceof HTTPErrorResponse) {
|
||||
throw verifyResult;
|
||||
}
|
||||
|
||||
const user: User = User.fromJSON(
|
||||
verifyResult.data as JSONObject,
|
||||
User,
|
||||
) as User;
|
||||
const miscData: JSONObject = {};
|
||||
|
||||
login(user as User, miscData);
|
||||
} catch (error) {
|
||||
setTwoFactorAuthError(API.getFriendlyErrorMessage(error as Error));
|
||||
}
|
||||
setIsTwoFactorAuthLoading(false);
|
||||
}
|
||||
}, [selectedWebAuthn]);
|
||||
|
||||
type LoginFunction = (user: User, miscData: JSONObject) => void;
|
||||
|
||||
const login: LoginFunction = (user: User, miscData: JSONObject): void => {
|
||||
|
|
@ -155,17 +268,24 @@ const LoginPage: () => JSX.Element = () => {
|
|||
miscData: JSONObject | undefined,
|
||||
) => {
|
||||
if (
|
||||
miscData &&
|
||||
(miscData as JSONObject)["twoFactorAuth"] === true
|
||||
(miscData &&
|
||||
(((miscData as JSONObject)["totpAuthList"] as JSONArray)
|
||||
?.length || 0) > 0) ||
|
||||
(((miscData as JSONObject)["webAuthnList"] as JSONArray)
|
||||
?.length || 0) > 0
|
||||
) {
|
||||
const twoFactorAuthList: Array<UserTwoFactorAuth> =
|
||||
UserTwoFactorAuth.fromJSONArray(
|
||||
(miscData as JSONObject)[
|
||||
"twoFactorAuthList"
|
||||
] as JSONArray,
|
||||
UserTwoFactorAuth,
|
||||
const totpAuthList: Array<UserTotpAuth> =
|
||||
UserTotpAuth.fromJSONArray(
|
||||
(miscData as JSONObject)["totpAuthList"] as JSONArray,
|
||||
UserTotpAuth,
|
||||
);
|
||||
setTwoFactorAuthList(twoFactorAuthList);
|
||||
const webAuthnList: Array<UserWebAuthn> =
|
||||
UserWebAuthn.fromJSONArray(
|
||||
(miscData as JSONObject)["webAuthnList"] as JSONArray,
|
||||
UserWebAuthn,
|
||||
);
|
||||
setTotpAuthList(totpAuthList);
|
||||
setWebAuthnList(webAuthnList);
|
||||
setShowTwoFactorAuth(true);
|
||||
return;
|
||||
}
|
||||
|
|
@ -187,19 +307,53 @@ const LoginPage: () => JSX.Element = () => {
|
|||
/>
|
||||
)}
|
||||
|
||||
{showTwoFactorAuth && !selectedTwoFactorAuth && (
|
||||
<StaticModelList<UserTwoFactorAuth>
|
||||
titleField="name"
|
||||
descriptionField=""
|
||||
selectedItems={[]}
|
||||
list={twoFactorAuthList}
|
||||
onClick={(item: UserTwoFactorAuth) => {
|
||||
setSelectedTwoFactorAuth(item);
|
||||
}}
|
||||
/>
|
||||
{showTwoFactorAuth && !selectedTotpAuth && !selectedWebAuthn && (
|
||||
<div className="space-y-4">
|
||||
{twoFactorMethods.map(
|
||||
(method: TwoFactorMethod, index: number) => {
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="cursor-pointer p-4 border border-gray-300 rounded-lg hover:bg-gray-50"
|
||||
onClick={() => {
|
||||
if (method.type === "totp") {
|
||||
setSelectedTotpAuth(method.item as UserTotpAuth);
|
||||
} else {
|
||||
setSelectedWebAuthn(method.item as UserWebAuthn);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="font-medium">
|
||||
{(method.item as any).name}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
{method.type === "totp"
|
||||
? "Authenticator App"
|
||||
: "Security Key"}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showTwoFactorAuth && selectedTwoFactorAuth && (
|
||||
{showTwoFactorAuth && selectedWebAuthn && (
|
||||
<div className="text-center">
|
||||
<div className="text-lg font-medium mb-4">
|
||||
Authenticating with Security Key
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 mb-4">
|
||||
Please follow the instructions on your security key device.
|
||||
</div>
|
||||
{isTwoFactorAuthLoading && <ComponentLoader />}
|
||||
{twofactorAuthError && (
|
||||
<ErrorMessage message={twofactorAuthError} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showTwoFactorAuth && selectedTotpAuth && (
|
||||
<BasicForm
|
||||
id="two-factor-auth-form"
|
||||
name="Two Factor Auth"
|
||||
|
|
@ -225,15 +379,17 @@ const LoginPage: () => JSX.Element = () => {
|
|||
try {
|
||||
const code: string = data["code"] as string;
|
||||
const twoFactorAuthId: string =
|
||||
selectedTwoFactorAuth.id?.toString() as string;
|
||||
selectedTotpAuth!.id?.toString() as string;
|
||||
|
||||
const result: HTTPErrorResponse | HTTPResponse<JSONObject> =
|
||||
await API.post({
|
||||
url: VERIFY_TWO_FACTOR_AUTH_API_URL,
|
||||
url: VERIFY_TOTP_AUTH_API_URL,
|
||||
data: {
|
||||
...initialValues,
|
||||
code: code,
|
||||
twoFactorAuthId: twoFactorAuthId,
|
||||
data: {
|
||||
...initialValues,
|
||||
code: code,
|
||||
twoFactorAuthId: twoFactorAuthId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -262,7 +418,7 @@ const LoginPage: () => JSX.Element = () => {
|
|||
)}
|
||||
</div>
|
||||
<div className="mt-10 text-center">
|
||||
{!selectedTwoFactorAuth && (
|
||||
{!selectedTotpAuth && !selectedWebAuthn && (
|
||||
<div className="text-muted mb-0 text-gray-500">
|
||||
Don't have an account?{" "}
|
||||
<Link
|
||||
|
|
@ -273,11 +429,12 @@ const LoginPage: () => JSX.Element = () => {
|
|||
</Link>
|
||||
</div>
|
||||
)}
|
||||
{selectedTwoFactorAuth ? (
|
||||
{selectedTotpAuth || selectedWebAuthn ? (
|
||||
<div className="text-muted mb-0 text-gray-500">
|
||||
<Link
|
||||
onClick={() => {
|
||||
setSelectedTwoFactorAuth(undefined);
|
||||
setSelectedTotpAuth(undefined);
|
||||
setSelectedWebAuthn(undefined);
|
||||
}}
|
||||
className="text-indigo-500 hover:text-indigo-900 cursor-pointer"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import Route from "Common/Types/API/Route";
|
||||
import URL from "Common/Types/API/URL";
|
||||
import { IDENTITY_URL } from "Common/UI/Config";
|
||||
import { IDENTITY_URL, APP_API_URL } from "Common/UI/Config";
|
||||
|
||||
export const SIGNUP_API_URL: URL = URL.fromURL(IDENTITY_URL).addRoute(
|
||||
new Route("/signup"),
|
||||
|
|
@ -9,9 +9,17 @@ export const LOGIN_API_URL: URL = URL.fromURL(IDENTITY_URL).addRoute(
|
|||
new Route("/login"),
|
||||
);
|
||||
|
||||
export const VERIFY_TWO_FACTOR_AUTH_API_URL: URL = URL.fromURL(
|
||||
export const VERIFY_TOTP_AUTH_API_URL: URL = URL.fromURL(IDENTITY_URL).addRoute(
|
||||
new Route("/verify-totp-auth"),
|
||||
);
|
||||
|
||||
export const GENERATE_WEBAUTHN_AUTH_OPTIONS_API_URL: URL = URL.fromURL(
|
||||
APP_API_URL,
|
||||
).addRoute(new Route("/user-webauthn/generate-authentication-options"));
|
||||
|
||||
export const VERIFY_WEBAUTHN_AUTH_API_URL: URL = URL.fromURL(
|
||||
IDENTITY_URL,
|
||||
).addRoute(new Route("/verify-two-factor-auth"));
|
||||
).addRoute(new Route("/verify-webauthn-auth"));
|
||||
|
||||
export const SERVICE_PROVIDER_LOGIN_URL: URL = URL.fromURL(
|
||||
IDENTITY_URL,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
#
|
||||
|
||||
# Pull base image nodejs image.
|
||||
FROM public.ecr.aws/docker/library/node:23.8-alpine3.21
|
||||
FROM public.ecr.aws/docker/library/node:24.9-alpine3.21
|
||||
RUN mkdir /tmp/npm && chmod 2777 /tmp/npm && chown 1000:1000 /tmp/npm && npm config set cache /tmp/npm --global
|
||||
|
||||
RUN npm config set fetch-retries 5
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
#
|
||||
|
||||
# Pull base image nodejs image.
|
||||
FROM public.ecr.aws/docker/library/node:23.8-alpine3.21
|
||||
FROM public.ecr.aws/docker/library/node:24.9-alpine3.21
|
||||
RUN mkdir /tmp/npm && chmod 2777 /tmp/npm && chown 1000:1000 /tmp/npm && npm config set cache /tmp/npm --global
|
||||
|
||||
RUN npm config set fetch-retries 5
|
||||
|
|
|
|||
|
|
@ -24,7 +24,8 @@ import WorkspaceNotificationRuleAPI from "Common/Server/API/WorkspaceNotificatio
|
|||
import StatusPageDomainAPI from "Common/Server/API/StatusPageDomainAPI";
|
||||
import StatusPageSubscriberAPI from "Common/Server/API/StatusPageSubscriberAPI";
|
||||
import UserCallAPI from "Common/Server/API/UserCallAPI";
|
||||
import UserTwoFactorAuthAPI from "Common/Server/API/UserTwoFactorAuthAPI";
|
||||
import UserTotpAuthAPI from "Common/Server/API/UserTotpAuthAPI";
|
||||
import UserWebAuthnAPI from "Common/Server/API/UserWebAuthnAPI";
|
||||
import MonitorTest from "Common/Models/DatabaseModels/MonitorTest";
|
||||
// User Notification methods.
|
||||
import UserEmailAPI from "Common/Server/API/UserEmailAPI";
|
||||
|
|
@ -1666,7 +1667,11 @@ const BaseAPIFeatureSet: FeatureSet = {
|
|||
app.use(`/${APP_NAME.toLocaleLowerCase()}`, new UserCallAPI().getRouter());
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new UserTwoFactorAuthAPI().getRouter(),
|
||||
new UserTotpAuthAPI().getRouter(),
|
||||
);
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new UserWebAuthnAPI().getRouter(),
|
||||
);
|
||||
app.use(`/${APP_NAME.toLocaleLowerCase()}`, new UserEmailAPI().getRouter());
|
||||
app.use(`/${APP_NAME.toLocaleLowerCase()}`, new UserSMSAPI().getRouter());
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import AccessTokenService from "Common/Server/Services/AccessTokenService";
|
|||
import EmailVerificationTokenService from "Common/Server/Services/EmailVerificationTokenService";
|
||||
import MailService from "Common/Server/Services/MailService";
|
||||
import UserService from "Common/Server/Services/UserService";
|
||||
import UserTwoFactorAuthService from "Common/Server/Services/UserTwoFactorAuthService";
|
||||
import UserTotpAuthService from "Common/Server/Services/UserTotpAuthService";
|
||||
import CookieUtil from "Common/Server/Utils/Cookie";
|
||||
import Express, {
|
||||
ExpressRequest,
|
||||
|
|
@ -34,10 +34,12 @@ import Express, {
|
|||
} from "Common/Server/Utils/Express";
|
||||
import logger from "Common/Server/Utils/Logger";
|
||||
import Response from "Common/Server/Utils/Response";
|
||||
import TwoFactorAuth from "Common/Server/Utils/TwoFactorAuth";
|
||||
import TotpAuth from "Common/Server/Utils/TotpAuth";
|
||||
import EmailVerificationToken from "Common/Models/DatabaseModels/EmailVerificationToken";
|
||||
import User from "Common/Models/DatabaseModels/User";
|
||||
import UserTwoFactorAuth from "Common/Models/DatabaseModels/UserTwoFactorAuth";
|
||||
import UserTotpAuth from "Common/Models/DatabaseModels/UserTotpAuth";
|
||||
import UserWebAuthn from "Common/Models/DatabaseModels/UserWebAuthn";
|
||||
import UserWebAuthnService from "Common/Server/Services/UserWebAuthnService";
|
||||
|
||||
const router: ExpressRouter = Express.getRouter();
|
||||
|
||||
|
|
@ -503,7 +505,7 @@ router.post(
|
|||
);
|
||||
|
||||
router.post(
|
||||
"/verify-two-factor-auth",
|
||||
"/verify-totp-auth",
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
|
|
@ -513,7 +515,25 @@ router.post(
|
|||
req: req,
|
||||
res: res,
|
||||
next: next,
|
||||
verifyTwoFactorAuth: true,
|
||||
verifyTotpAuth: true,
|
||||
verifyWebAuthn: false,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/verify-webauthn-auth",
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction,
|
||||
): Promise<void> => {
|
||||
return login({
|
||||
req: req,
|
||||
res: res,
|
||||
next: next,
|
||||
verifyTotpAuth: false,
|
||||
verifyWebAuthn: true,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
|
@ -529,62 +549,99 @@ router.post(
|
|||
req: req,
|
||||
res: res,
|
||||
next: next,
|
||||
verifyTwoFactorAuth: false,
|
||||
verifyTotpAuth: false,
|
||||
verifyWebAuthn: false,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
type FetchTwoFactorAuthListFunction = (
|
||||
userId: ObjectID,
|
||||
) => Promise<Array<UserTwoFactorAuth>>;
|
||||
type FetchTotpAuthListFunction = (userId: ObjectID) => Promise<{
|
||||
totpAuthList: Array<UserTotpAuth>;
|
||||
webAuthnList: Array<UserWebAuthn>;
|
||||
}>;
|
||||
|
||||
const fetchTwoFactorAuthList: FetchTwoFactorAuthListFunction = async (
|
||||
const fetchTotpAuthList: FetchTotpAuthListFunction = async (
|
||||
userId: ObjectID,
|
||||
): Promise<Array<UserTwoFactorAuth>> => {
|
||||
const twoFactorAuthList: Array<UserTwoFactorAuth> =
|
||||
await UserTwoFactorAuthService.findBy({
|
||||
query: {
|
||||
userId: userId,
|
||||
isVerified: true,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
userId: true,
|
||||
name: true,
|
||||
},
|
||||
limit: LIMIT_PER_PROJECT,
|
||||
skip: 0,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
): Promise<{
|
||||
totpAuthList: Array<UserTotpAuth>;
|
||||
webAuthnList: Array<UserWebAuthn>;
|
||||
}> => {
|
||||
const totpAuthList: Array<UserTotpAuth> = await UserTotpAuthService.findBy({
|
||||
query: {
|
||||
userId: userId,
|
||||
isVerified: true,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
userId: true,
|
||||
name: true,
|
||||
},
|
||||
limit: LIMIT_PER_PROJECT,
|
||||
skip: 0,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
return twoFactorAuthList;
|
||||
const webAuthnList: Array<UserWebAuthn> = await UserWebAuthnService.findBy({
|
||||
query: {
|
||||
userId: userId,
|
||||
isVerified: true,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
userId: true,
|
||||
name: true,
|
||||
},
|
||||
limit: LIMIT_PER_PROJECT,
|
||||
skip: 0,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
totpAuthList: totpAuthList || [],
|
||||
webAuthnList: webAuthnList || [],
|
||||
};
|
||||
};
|
||||
|
||||
type LoginFunction = (options: {
|
||||
req: ExpressRequest;
|
||||
res: ExpressResponse;
|
||||
next: NextFunction;
|
||||
verifyTwoFactorAuth: boolean;
|
||||
verifyTotpAuth: boolean;
|
||||
verifyWebAuthn: boolean;
|
||||
}) => Promise<void>;
|
||||
|
||||
const login: LoginFunction = async (options: {
|
||||
req: ExpressRequest;
|
||||
res: ExpressResponse;
|
||||
next: NextFunction;
|
||||
verifyTwoFactorAuth: boolean;
|
||||
verifyTotpAuth: boolean;
|
||||
verifyWebAuthn: boolean;
|
||||
}): Promise<void> => {
|
||||
const req: ExpressRequest = options.req;
|
||||
const res: ExpressResponse = options.res;
|
||||
const next: NextFunction = options.next;
|
||||
const verifyTwoFactorAuth: boolean = options.verifyTwoFactorAuth;
|
||||
const verifyTotpAuth: boolean = options.verifyTotpAuth;
|
||||
const verifyWebAuthn: boolean = options.verifyWebAuthn;
|
||||
|
||||
try {
|
||||
const data: JSONObject = req.body["data"];
|
||||
|
||||
logger.debug("Login request data: " + JSON.stringify(req.body, null, 2));
|
||||
|
||||
const user: User = BaseModel.fromJSON(data as JSONObject, User) as User;
|
||||
|
||||
if (!user.email || !user.password) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Email and password are required."),
|
||||
);
|
||||
}
|
||||
|
||||
await user.password?.hashValue(EncryptionSecret);
|
||||
|
||||
const alreadySavedUser: User | null = await UserService.findOneBy({
|
||||
|
|
@ -638,13 +695,21 @@ const login: LoginFunction = async (options: {
|
|||
);
|
||||
}
|
||||
|
||||
if (alreadySavedUser.enableTwoFactorAuth && !verifyTwoFactorAuth) {
|
||||
if (
|
||||
alreadySavedUser.enableTwoFactorAuth &&
|
||||
!verifyTotpAuth &&
|
||||
!verifyWebAuthn
|
||||
) {
|
||||
// If two factor auth is enabled then we will send the user to the two factor auth page.
|
||||
|
||||
const twoFactorAuthList: Array<UserTwoFactorAuth> =
|
||||
await fetchTwoFactorAuthList(alreadySavedUser.id!);
|
||||
const { totpAuthList, webAuthnList } = await fetchTotpAuthList(
|
||||
alreadySavedUser.id!,
|
||||
);
|
||||
|
||||
if (!twoFactorAuthList || twoFactorAuthList.length === 0) {
|
||||
if (
|
||||
(!totpAuthList || totpAuthList.length === 0) &&
|
||||
(!webAuthnList || webAuthnList.length === 0)
|
||||
) {
|
||||
const errorMessage: string = IsBillingEnabled
|
||||
? "Two Factor Authentication is enabled but no two factor auth is setup. Please contact OneUptime support for help."
|
||||
: "Two Factor Authentication is enabled but no two factor auth is setup. Please contact your server admin to disable two factor auth for this account.";
|
||||
|
|
@ -656,62 +721,68 @@ const login: LoginFunction = async (options: {
|
|||
);
|
||||
}
|
||||
|
||||
return Response.sendEntityResponse(req, res, user, User, {
|
||||
return Response.sendEntityResponse(req, res, alreadySavedUser, User, {
|
||||
miscData: {
|
||||
twoFactorAuthList: UserTwoFactorAuth.toJSONArray(
|
||||
twoFactorAuthList,
|
||||
UserTwoFactorAuth,
|
||||
),
|
||||
twoFactorAuth: true,
|
||||
totpAuthList: UserTotpAuth.toJSONArray(totpAuthList, UserTotpAuth),
|
||||
webAuthnList: UserWebAuthn.toJSONArray(webAuthnList, UserWebAuthn),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (verifyTwoFactorAuth) {
|
||||
// code from req
|
||||
const code: string = data["code"] as string;
|
||||
const twoFactorAuthId: string = data["twoFactorAuthId"] as string;
|
||||
if (verifyTotpAuth || verifyWebAuthn) {
|
||||
if (verifyTotpAuth) {
|
||||
// code from req
|
||||
const code: string = data["code"] as string;
|
||||
const twoFactorAuthId: string = data["twoFactorAuthId"] as string;
|
||||
|
||||
const twoFactorAuth: UserTwoFactorAuth | null =
|
||||
await UserTwoFactorAuthService.findOneBy({
|
||||
query: {
|
||||
_id: twoFactorAuthId,
|
||||
userId: alreadySavedUser.id!,
|
||||
isVerified: true,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
twoFactorSecret: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
const totpAuth: UserTotpAuth | null =
|
||||
await UserTotpAuthService.findOneBy({
|
||||
query: {
|
||||
_id: twoFactorAuthId,
|
||||
userId: alreadySavedUser.id!,
|
||||
isVerified: true,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
twoFactorSecret: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!totpAuth) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid two factor auth id."),
|
||||
);
|
||||
}
|
||||
|
||||
const isVerified: boolean = TotpAuth.verifyToken({
|
||||
token: code,
|
||||
secret: totpAuth.twoFactorSecret!,
|
||||
email: alreadySavedUser.email!,
|
||||
});
|
||||
|
||||
if (!twoFactorAuth) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid two factor auth id."),
|
||||
);
|
||||
if (!isVerified) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid code."),
|
||||
);
|
||||
}
|
||||
} else if (verifyWebAuthn) {
|
||||
const expectedChallenge: string = data["challenge"] as string;
|
||||
const credential: any = data["credential"];
|
||||
|
||||
await UserWebAuthnService.verifyAuthentication({
|
||||
userId: alreadySavedUser.id!.toString(),
|
||||
challenge: expectedChallenge,
|
||||
credential: credential,
|
||||
});
|
||||
}
|
||||
|
||||
const isVerified: boolean = TwoFactorAuth.verifyToken({
|
||||
token: code,
|
||||
secret: twoFactorAuth.twoFactorSecret!,
|
||||
email: alreadySavedUser.email!,
|
||||
});
|
||||
|
||||
if (!isVerified) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid code."),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh Permissions for this user here.
|
||||
} // Refresh Permissions for this user here.
|
||||
await AccessTokenService.refreshUserAllPermissions(alreadySavedUser.id!);
|
||||
|
||||
if (alreadySavedUser.password.toString() === user.password!.toString()) {
|
||||
|
|
|
|||
|
|
@ -35,6 +35,8 @@ export default class AuthenticationEmail {
|
|||
const host: Hostname = await DatabaseConfig.getHost();
|
||||
const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol();
|
||||
|
||||
logger.debug("Sending verification email");
|
||||
|
||||
MailService.sendMail({
|
||||
toEmail: user.email!,
|
||||
subject: "Please verify email.",
|
||||
|
|
@ -50,8 +52,13 @@ export default class AuthenticationEmail {
|
|||
).toString(),
|
||||
homeUrl: new URL(httpProtocol, host).toString(),
|
||||
},
|
||||
}).catch((err: Error) => {
|
||||
logger.error(err);
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
logger.debug("Verification email sent");
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
logger.debug("Error sending verification email");
|
||||
logger.error(err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,5 +16,5 @@
|
|||
"TS_NODE_TRANSPILE_ONLY": "1",
|
||||
"TS_NODE_FILES": "false"
|
||||
},
|
||||
"exec": "node --inspect=0.0.0.0:9229 -r ts-node/register/transpile-only Index.ts"
|
||||
"exec": "node -r ts-node/register/transpile-only Index.ts"
|
||||
}
|
||||
|
|
@ -146,7 +146,8 @@ import ServiceCatalogDependency from "./ServiceCatalogDependency";
|
|||
import ServiceCatalogMonitor from "./ServiceCatalogMonitor";
|
||||
import ServiceCatalogTelemetryService from "./ServiceCatalogTelemetryService";
|
||||
|
||||
import UserTwoFactorAuth from "./UserTwoFactorAuth";
|
||||
import UserTotpAuth from "./UserTotpAuth";
|
||||
import UserWebAuthn from "./UserWebAuthn";
|
||||
|
||||
import TelemetryIngestionKey from "./TelemetryIngestionKey";
|
||||
|
||||
|
|
@ -366,7 +367,8 @@ const AllModelTypes: Array<{
|
|||
ProbeOwnerTeam,
|
||||
ProbeOwnerUser,
|
||||
|
||||
UserTwoFactorAuth,
|
||||
UserTotpAuth,
|
||||
UserWebAuthn,
|
||||
|
||||
TelemetryIngestionKey,
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import File from "./File";
|
||||
import UserModel from "../../Models/DatabaseModels/DatabaseBaseModel/UserModel";
|
||||
import Route from "../../Types/API/Route";
|
||||
import URL from "../../Types/API/URL";
|
||||
import CompanySize from "../../Types/Company/CompanySize";
|
||||
import JobRole from "../../Types/Company/JobRole";
|
||||
import AllowAccessIfSubscriptionIsUnpaid from "../../Types/Database/AccessControl/AllowAccessIfSubscriptionIsUnpaid";
|
||||
|
|
@ -301,51 +300,6 @@ class User extends UserModel {
|
|||
})
|
||||
public twoFactorAuthEnabled?: boolean = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({ type: TableColumnType.ShortText })
|
||||
@Column({
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
nullable: true,
|
||||
unique: false,
|
||||
})
|
||||
public twoFactorSecretCode?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({ type: TableColumnType.ShortURL })
|
||||
@Column({
|
||||
type: ColumnType.ShortURL,
|
||||
length: ColumnLength.ShortURL,
|
||||
nullable: true,
|
||||
unique: false,
|
||||
transformer: URL.getDatabaseTransformer(),
|
||||
})
|
||||
public twoFactorAuthUrl?: URL = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [Permission.CurrentUser],
|
||||
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({ type: TableColumnType.Array })
|
||||
@Column({
|
||||
type: ColumnType.Array,
|
||||
nullable: true,
|
||||
unique: false,
|
||||
})
|
||||
public backupCodes?: Array<string> = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
|
|
|
|||
|
|
@ -27,19 +27,19 @@ import { Column, Entity, JoinColumn, ManyToOne } from "typeorm";
|
|||
delete: [Permission.CurrentUser],
|
||||
update: [Permission.CurrentUser],
|
||||
})
|
||||
@CrudApiEndpoint(new Route("/user-two-factor-auth"))
|
||||
@CrudApiEndpoint(new Route("/user-totp-auth"))
|
||||
@Entity({
|
||||
name: "UserTwoFactorAuth",
|
||||
name: "UserTotpAuth",
|
||||
})
|
||||
@TableMetadata({
|
||||
tableName: "UserTwoFactorAuth",
|
||||
singularName: "Two Factor Auth",
|
||||
pluralName: "Two Factor Auth",
|
||||
tableName: "UserTotpAuth",
|
||||
singularName: "TOTP Auth",
|
||||
pluralName: "TOTP Auth",
|
||||
icon: IconProp.ShieldCheck,
|
||||
tableDescription: "Two Factor Authentication for users",
|
||||
tableDescription: "TOTP Authentication for users",
|
||||
})
|
||||
@CurrentUserCanAccessRecordBy("userId")
|
||||
class UserTwoFactorAuth extends BaseModel {
|
||||
class UserTotpAuth extends BaseModel {
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [Permission.CurrentUser],
|
||||
|
|
@ -48,8 +48,8 @@ class UserTwoFactorAuth extends BaseModel {
|
|||
@TableColumn({
|
||||
type: TableColumnType.ShortText,
|
||||
canReadOnRelationQuery: true,
|
||||
title: "Two Factor Auth Name",
|
||||
description: "Name of the two factor authentication",
|
||||
title: "TOTP Auth Name",
|
||||
description: "Name of the TOTP authentication",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ShortText,
|
||||
|
|
@ -67,8 +67,8 @@ class UserTwoFactorAuth extends BaseModel {
|
|||
@TableColumn({
|
||||
type: TableColumnType.VeryLongText,
|
||||
canReadOnRelationQuery: false,
|
||||
title: "Two Factor Auth Secret",
|
||||
description: "Secret of the two factor authentication",
|
||||
title: "TOTP Auth Secret",
|
||||
description: "Secret of the TOTP authentication",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.VeryLongText,
|
||||
|
|
@ -85,8 +85,8 @@ class UserTwoFactorAuth extends BaseModel {
|
|||
@TableColumn({
|
||||
type: TableColumnType.VeryLongText,
|
||||
canReadOnRelationQuery: false,
|
||||
title: "Two Factor Auth OTP URL",
|
||||
description: "OTP URL of the two factor authentication",
|
||||
title: "TOTP Auth OTP URL",
|
||||
description: "OTP URL of the TOTP authentication",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.VeryLongText,
|
||||
|
|
@ -106,7 +106,7 @@ class UserTwoFactorAuth extends BaseModel {
|
|||
title: "Is Verified",
|
||||
isDefaultValueColumn: true,
|
||||
description:
|
||||
"Is this two factor authentication verified and validated (has user entered the tokent to verify it)",
|
||||
"Is this TOTP authentication verified and validated (has user entered the token to verify it)",
|
||||
defaultValue: false,
|
||||
})
|
||||
@Column({
|
||||
|
|
@ -171,7 +171,7 @@ class UserTwoFactorAuth extends BaseModel {
|
|||
manyToOneRelationColumn: "userId",
|
||||
type: TableColumnType.Entity,
|
||||
title: "User",
|
||||
description: "Relation to User who owns this two factor authentication",
|
||||
description: "Relation to User who owns this TOTP authentication",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
|
|
@ -207,4 +207,4 @@ class UserTwoFactorAuth extends BaseModel {
|
|||
public userId?: ObjectID = undefined;
|
||||
}
|
||||
|
||||
export default UserTwoFactorAuth;
|
||||
export default UserTotpAuth;
|
||||
244
Common/Models/DatabaseModels/UserWebAuthn.ts
Normal file
244
Common/Models/DatabaseModels/UserWebAuthn.ts
Normal file
|
|
@ -0,0 +1,244 @@
|
|||
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
|
||||
import User from "./User";
|
||||
import Route from "../../Types/API/Route";
|
||||
import AllowAccessIfSubscriptionIsUnpaid from "../../Types/Database/AccessControl/AllowAccessIfSubscriptionIsUnpaid";
|
||||
import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl";
|
||||
import TableAccessControl from "../../Types/Database/AccessControl/TableAccessControl";
|
||||
import ColumnLength from "../../Types/Database/ColumnLength";
|
||||
import ColumnType from "../../Types/Database/ColumnType";
|
||||
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
|
||||
import CurrentUserCanAccessRecordBy from "../../Types/Database/CurrentUserCanAccessRecordBy";
|
||||
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
|
||||
import TableColumn from "../../Types/Database/TableColumn";
|
||||
import TableColumnType from "../../Types/Database/TableColumnType";
|
||||
import TableMetadata from "../../Types/Database/TableMetadata";
|
||||
import IconProp from "../../Types/Icon/IconProp";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import Permission from "../../Types/Permission";
|
||||
import { Column, Entity, JoinColumn, ManyToOne } from "typeorm";
|
||||
|
||||
@EnableDocumentation({
|
||||
isMasterAdminApiDocs: true,
|
||||
})
|
||||
@AllowAccessIfSubscriptionIsUnpaid()
|
||||
@TableAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [Permission.CurrentUser],
|
||||
delete: [Permission.CurrentUser],
|
||||
update: [Permission.CurrentUser],
|
||||
})
|
||||
@CrudApiEndpoint(new Route("/user-webauthn"))
|
||||
@Entity({
|
||||
name: "UserWebAuthn",
|
||||
})
|
||||
@TableMetadata({
|
||||
tableName: "UserWebAuthn",
|
||||
singularName: "WebAuthn Credential",
|
||||
pluralName: "WebAuthn Credentials",
|
||||
icon: IconProp.ShieldCheck,
|
||||
tableDescription: "WebAuthn credentials for users (security keys)",
|
||||
})
|
||||
@CurrentUserCanAccessRecordBy("userId")
|
||||
class UserWebAuthn extends BaseModel {
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [Permission.CurrentUser],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ShortText,
|
||||
canReadOnRelationQuery: true,
|
||||
title: "Credential Name",
|
||||
description: "Name of the WebAuthn credential",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
nullable: false,
|
||||
unique: false,
|
||||
})
|
||||
public name?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.VeryLongText,
|
||||
canReadOnRelationQuery: false,
|
||||
title: "Credential ID",
|
||||
description: "Unique identifier for the WebAuthn credential",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.VeryLongText,
|
||||
nullable: false,
|
||||
unique: true,
|
||||
})
|
||||
public credentialId?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.VeryLongText,
|
||||
canReadOnRelationQuery: false,
|
||||
title: "Public Key",
|
||||
description: "Public key of the WebAuthn credential",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.VeryLongText,
|
||||
nullable: false,
|
||||
unique: false,
|
||||
})
|
||||
public publicKey?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.VeryLongText,
|
||||
canReadOnRelationQuery: false,
|
||||
title: "Counter",
|
||||
description: "Counter for the WebAuthn credential",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.VeryLongText,
|
||||
nullable: false,
|
||||
unique: false,
|
||||
})
|
||||
public counter?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.VeryLongText,
|
||||
canReadOnRelationQuery: false,
|
||||
title: "Transports",
|
||||
description: "Transports supported by the WebAuthn credential",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.VeryLongText,
|
||||
nullable: true,
|
||||
unique: false,
|
||||
})
|
||||
public transports?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.Boolean,
|
||||
canReadOnRelationQuery: true,
|
||||
title: "Is Verified",
|
||||
isDefaultValueColumn: true,
|
||||
description: "Is this WebAuthn credential verified and validated",
|
||||
defaultValue: false,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.Boolean,
|
||||
nullable: false,
|
||||
default: false,
|
||||
})
|
||||
public isVerified?: boolean = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "deletedByUserId",
|
||||
type: TableColumnType.Entity,
|
||||
title: "Deleted by User",
|
||||
modelType: User,
|
||||
description:
|
||||
"Relation to User who deleted this object (if this object was deleted by a User)",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return User;
|
||||
},
|
||||
{
|
||||
cascade: false,
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "SET NULL",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "deletedByUserId" })
|
||||
public deletedByUser?: User = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
title: "Deleted by User ID",
|
||||
description:
|
||||
"User ID who deleted this object (if this object was deleted by a User)",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: true,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public deletedByUserId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [Permission.CurrentUser],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "userId",
|
||||
type: TableColumnType.Entity,
|
||||
title: "User",
|
||||
description: "Relation to User who owns this WebAuthn credential",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return User;
|
||||
},
|
||||
{
|
||||
cascade: false,
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "CASCADE",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "userId" })
|
||||
public user?: User = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [Permission.CurrentUser],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
title: "User ID",
|
||||
description: "User ID who owns this WebAuthn credential",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: true,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public userId?: ObjectID = undefined;
|
||||
}
|
||||
|
||||
export default UserWebAuthn;
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import ObjectID from "../../Types/ObjectID";
|
||||
import UserMiddleware from "../Middleware/UserAuthorization";
|
||||
import UserTwoFactorAuthService, {
|
||||
Service as UserTwoFactorAuthServiceType,
|
||||
} from "../Services/UserTwoFactorAuthService";
|
||||
import UserTotpAuthService, {
|
||||
Service as UserTotpAuthServiceType,
|
||||
} from "../Services/UserTotpAuthService";
|
||||
import {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
|
|
@ -10,27 +10,27 @@ import {
|
|||
OneUptimeRequest,
|
||||
} from "../Utils/Express";
|
||||
import BaseAPI from "./BaseAPI";
|
||||
import UserTwoFactorAuth from "../../Models/DatabaseModels/UserTwoFactorAuth";
|
||||
import UserTotpAuth from "../../Models/DatabaseModels/UserTotpAuth";
|
||||
import BadDataException from "../../Types/Exception/BadDataException";
|
||||
import TwoFactorAuth from "../Utils/TwoFactorAuth";
|
||||
import TotpAuth from "../Utils/TotpAuth";
|
||||
import Response from "../Utils/Response";
|
||||
import User from "../../Models/DatabaseModels/User";
|
||||
import UserService from "../Services/UserService";
|
||||
|
||||
export default class UserTwoFactorAuthAPI extends BaseAPI<
|
||||
UserTwoFactorAuth,
|
||||
UserTwoFactorAuthServiceType
|
||||
export default class UserTotpAuthAPI extends BaseAPI<
|
||||
UserTotpAuth,
|
||||
UserTotpAuthServiceType
|
||||
> {
|
||||
public constructor() {
|
||||
super(UserTwoFactorAuth, UserTwoFactorAuthService);
|
||||
super(UserTotpAuth, UserTotpAuthService);
|
||||
|
||||
this.router.post(
|
||||
`${new this.entityType().getCrudApiPath()?.toString()}/validate`,
|
||||
UserMiddleware.getUserMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
const userTwoFactorAuth: UserTwoFactorAuth | null =
|
||||
await UserTwoFactorAuthService.findOneById({
|
||||
const userTotpAuth: UserTotpAuth | null =
|
||||
await UserTotpAuthService.findOneById({
|
||||
id: new ObjectID(req.body["id"]),
|
||||
select: {
|
||||
twoFactorSecret: true,
|
||||
|
|
@ -41,24 +41,24 @@ export default class UserTwoFactorAuthAPI extends BaseAPI<
|
|||
},
|
||||
});
|
||||
|
||||
if (!userTwoFactorAuth) {
|
||||
throw new BadDataException("Two factor auth not found");
|
||||
if (!userTotpAuth) {
|
||||
throw new BadDataException("TOTP auth not found");
|
||||
}
|
||||
|
||||
if (
|
||||
userTwoFactorAuth.userId?.toString() !==
|
||||
userTotpAuth.userId?.toString() !==
|
||||
(req as OneUptimeRequest).userAuthorization?.userId.toString()
|
||||
) {
|
||||
throw new BadDataException("Two factor auth not found");
|
||||
}
|
||||
|
||||
if (!userTwoFactorAuth.userId) {
|
||||
if (!userTotpAuth.userId) {
|
||||
throw new BadDataException("User not found");
|
||||
}
|
||||
|
||||
// get user email.
|
||||
const user: User | null = await UserService.findOneById({
|
||||
id: userTwoFactorAuth.userId!,
|
||||
id: userTotpAuth.userId!,
|
||||
select: {
|
||||
email: true,
|
||||
},
|
||||
|
|
@ -75,8 +75,8 @@ export default class UserTwoFactorAuthAPI extends BaseAPI<
|
|||
throw new BadDataException("User email not found");
|
||||
}
|
||||
|
||||
const isValid: boolean = TwoFactorAuth.verifyToken({
|
||||
secret: userTwoFactorAuth.twoFactorSecret || "",
|
||||
const isValid: boolean = TotpAuth.verifyToken({
|
||||
secret: userTotpAuth.twoFactorSecret || "",
|
||||
token: req.body["code"] || "",
|
||||
email: user.email!,
|
||||
});
|
||||
|
|
@ -87,8 +87,8 @@ export default class UserTwoFactorAuthAPI extends BaseAPI<
|
|||
|
||||
// update this 2fa code as verified
|
||||
|
||||
await UserTwoFactorAuthService.updateOneById({
|
||||
id: userTwoFactorAuth.id!,
|
||||
await UserTotpAuthService.updateOneById({
|
||||
id: userTotpAuth.id!,
|
||||
data: {
|
||||
isVerified: true,
|
||||
},
|
||||
103
Common/Server/API/UserWebAuthnAPI.ts
Normal file
103
Common/Server/API/UserWebAuthnAPI.ts
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
import ObjectID from "../../Types/ObjectID";
|
||||
import UserMiddleware from "../Middleware/UserAuthorization";
|
||||
import UserWebAuthnService, {
|
||||
Service as UserWebAuthnServiceType,
|
||||
} from "../Services/UserWebAuthnService";
|
||||
import {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
NextFunction,
|
||||
OneUptimeRequest,
|
||||
} from "../Utils/Express";
|
||||
import BaseAPI from "./BaseAPI";
|
||||
import UserWebAuthn from "../../Models/DatabaseModels/UserWebAuthn";
|
||||
import BadDataException from "../../Types/Exception/BadDataException";
|
||||
import Response from "../Utils/Response";
|
||||
import { JSONObject } from "../../Types/JSON";
|
||||
import CommonAPI from "./CommonAPI";
|
||||
import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
|
||||
|
||||
export default class UserWebAuthnAPI extends BaseAPI<
|
||||
UserWebAuthn,
|
||||
UserWebAuthnServiceType
|
||||
> {
|
||||
public constructor() {
|
||||
super(UserWebAuthn, UserWebAuthnService);
|
||||
|
||||
this.router.post(
|
||||
`${new this.entityType().getCrudApiPath()?.toString()}/generate-registration-options`,
|
||||
UserMiddleware.getUserMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
const userId: ObjectID = (req as OneUptimeRequest).userAuthorization!
|
||||
.userId;
|
||||
|
||||
const result: { options: any; challenge: string } =
|
||||
await UserWebAuthnService.generateRegistrationOptions({
|
||||
userId: userId,
|
||||
});
|
||||
|
||||
return Response.sendJsonObjectResponse(req, res, result);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
this.router.post(
|
||||
`${new this.entityType().getCrudApiPath()?.toString()}/verify-registration`,
|
||||
UserMiddleware.getUserMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
const data: JSONObject = req.body;
|
||||
|
||||
const databaseProps: DatabaseCommonInteractionProps =
|
||||
await CommonAPI.getDatabaseCommonInteractionProps(req);
|
||||
|
||||
const expectedChallenge: string = data["challenge"] as string;
|
||||
const credential: any = data["credential"];
|
||||
const name: string = data["name"] as string;
|
||||
|
||||
await UserWebAuthnService.verifyRegistration({
|
||||
challenge: expectedChallenge,
|
||||
credential: credential,
|
||||
name: name,
|
||||
props: databaseProps,
|
||||
});
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
this.router.post(
|
||||
`${new this.entityType().getCrudApiPath()?.toString()}/generate-authentication-options`,
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
const data: JSONObject = req.body["data"] as JSONObject;
|
||||
|
||||
if (!data) {
|
||||
throw new BadDataException("Data is required");
|
||||
}
|
||||
|
||||
const email: string | undefined = data["email"] as string | undefined;
|
||||
|
||||
if (!email) {
|
||||
throw new BadDataException("Email is required");
|
||||
}
|
||||
|
||||
const result: { options: any; challenge: string; userId: string } =
|
||||
await UserWebAuthnService.generateAuthenticationOptions({
|
||||
email: email,
|
||||
});
|
||||
|
||||
return Response.sendJsonObjectResponse(req, res, result);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class MigrationName1759175457008 implements MigrationInterface {
|
||||
public name = "MigrationName1759175457008";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "UserWebAuthn" ("_id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP WITH TIME ZONE, "version" integer NOT NULL, "name" character varying(100) NOT NULL, "credentialId" text NOT NULL, "publicKey" text NOT NULL, "counter" text NOT NULL, "transports" text, "isVerified" boolean NOT NULL DEFAULT false, "deletedByUserId" uuid, "userId" uuid, CONSTRAINT "UQ_ed9d287cb27cc360b9c3a4542e9" UNIQUE ("credentialId"), CONSTRAINT "PK_76a58e093d632ac5a9036bfac57" PRIMARY KEY ("_id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "UserWebAuthn" ADD CONSTRAINT "FK_e14966d27e4991f5f53ef54cad5" FOREIGN KEY ("deletedByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "UserWebAuthn" ADD CONSTRAINT "FK_e7a7d2869a90899c5f76ec997c0" FOREIGN KEY ("userId") REFERENCES "User"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "UserWebAuthn" DROP CONSTRAINT "FK_e7a7d2869a90899c5f76ec997c0"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "UserWebAuthn" DROP CONSTRAINT "FK_e14966d27e4991f5f53ef54cad5"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "UserWebAuthn"`);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class MigrationName1759232954703 implements MigrationInterface {
|
||||
public name = "MigrationName1759232954703";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "User" DROP COLUMN "twoFactorSecretCode"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "User" DROP COLUMN "twoFactorAuthUrl"`,
|
||||
);
|
||||
await queryRunner.query(`ALTER TABLE "User" DROP COLUMN "backupCodes"`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "User" ADD "backupCodes" text`);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "User" ADD "twoFactorAuthUrl" character varying(100)`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "User" ADD "twoFactorSecretCode" character varying(100)`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class RenameUserTwoFactorAuthToUserTotpAuth1759234532998
|
||||
implements MigrationInterface
|
||||
{
|
||||
public name = "RenameUserTwoFactorAuthToUserTotpAuth1759234532998";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.renameTable("UserTwoFactorAuth", "UserTotpAuth");
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.renameTable("UserTotpAuth", "UserTwoFactorAuth");
|
||||
}
|
||||
}
|
||||
|
|
@ -171,6 +171,9 @@ import { MigrationName1758313975491 } from "./1758313975491-MigrationName";
|
|||
import { MigrationName1758626094132 } from "./1758626094132-MigrationName";
|
||||
import { MigrationName1758629540993 } from "./1758629540993-MigrationName";
|
||||
import { MigrationName1758798730753 } from "./1758798730753-MigrationName";
|
||||
import { MigrationName1759175457008 } from "./1759175457008-MigrationName";
|
||||
import { MigrationName1759232954703 } from "./1759232954703-MigrationName";
|
||||
import { RenameUserTwoFactorAuthToUserTotpAuth1759234532998 } from "./1759234532998-MigrationName";
|
||||
|
||||
export default [
|
||||
InitialMigration,
|
||||
|
|
@ -346,4 +349,7 @@ export default [
|
|||
MigrationName1758626094132,
|
||||
MigrationName1758629540993,
|
||||
MigrationName1758798730753,
|
||||
MigrationName1759175457008,
|
||||
MigrationName1759232954703,
|
||||
RenameUserTwoFactorAuthToUserTotpAuth1759234532998,
|
||||
];
|
||||
|
|
|
|||
|
|
@ -123,7 +123,8 @@ import UserNotificationSettingService from "./UserNotificationSettingService";
|
|||
import UserOnCallLogService from "./UserOnCallLogService";
|
||||
import UserOnCallLogTimelineService from "./UserOnCallLogTimelineService";
|
||||
import UserService from "./UserService";
|
||||
import UserTwoFactorAuthService from "./UserTwoFactorAuthService";
|
||||
import UserTotpAuthService from "./UserTotpAuthService";
|
||||
import UserWebAuthnService from "./UserWebAuthnService";
|
||||
import UserSmsService from "./UserSmsService";
|
||||
import WorkflowLogService from "./WorkflowLogService";
|
||||
// Workflows.
|
||||
|
|
@ -280,7 +281,8 @@ const services: Array<BaseService> = [
|
|||
UserOnCallLogService,
|
||||
UserOnCallLogTimelineService,
|
||||
UserSmsService,
|
||||
UserTwoFactorAuthService,
|
||||
UserTotpAuthService,
|
||||
UserWebAuthnService,
|
||||
|
||||
WorkflowLogService,
|
||||
WorkflowService,
|
||||
|
|
|
|||
|
|
@ -29,8 +29,10 @@ import EmailVerificationToken from "../../Models/DatabaseModels/EmailVerificatio
|
|||
import TeamMember from "../../Models/DatabaseModels/TeamMember";
|
||||
import Model from "../../Models/DatabaseModels/User";
|
||||
import SlackUtil from "../Utils/Workspace/Slack/Slack";
|
||||
import UserTwoFactorAuth from "../../Models/DatabaseModels/UserTwoFactorAuth";
|
||||
import UserTwoFactorAuthService from "./UserTwoFactorAuthService";
|
||||
import UserTotpAuth from "../../Models/DatabaseModels/UserTotpAuth";
|
||||
import UserTotpAuthService from "./UserTotpAuthService";
|
||||
import UserWebAuthn from "../../Models/DatabaseModels/UserWebAuthn";
|
||||
import UserWebAuthnService from "./UserWebAuthnService";
|
||||
import BadDataException from "../../Types/Exception/BadDataException";
|
||||
import Name from "../../Types/Name";
|
||||
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
||||
|
|
@ -157,8 +159,8 @@ export class Service extends DatabaseService<Model> {
|
|||
});
|
||||
|
||||
for (const user of users) {
|
||||
const twoFactorAuth: UserTwoFactorAuth | null =
|
||||
await UserTwoFactorAuthService.findOneBy({
|
||||
const totpAuth: UserTotpAuth | null =
|
||||
await UserTotpAuthService.findOneBy({
|
||||
query: {
|
||||
userId: user.id!,
|
||||
isVerified: true,
|
||||
|
|
@ -171,7 +173,21 @@ export class Service extends DatabaseService<Model> {
|
|||
},
|
||||
});
|
||||
|
||||
if (!twoFactorAuth) {
|
||||
const webAuthn: UserWebAuthn | null =
|
||||
await UserWebAuthnService.findOneBy({
|
||||
query: {
|
||||
userId: user.id!,
|
||||
isVerified: true,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!totpAuth && !webAuthn) {
|
||||
throw new BadDataException(
|
||||
"Please verify two factor authentication method before you enable two factor authentication.",
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import CreateBy from "../Types/Database/CreateBy";
|
||||
import { OnCreate, OnDelete } from "../Types/Database/Hooks";
|
||||
import DatabaseService from "./DatabaseService";
|
||||
import Model from "../../Models/DatabaseModels/UserTwoFactorAuth";
|
||||
import TwoFactorAuth from "../Utils/TwoFactorAuth";
|
||||
import Model from "../../Models/DatabaseModels/UserTotpAuth";
|
||||
import TotpAuth from "../Utils/TotpAuth";
|
||||
import UserService from "./UserService";
|
||||
import BadDataException from "../../Types/Exception/BadDataException";
|
||||
import User from "../../Models/DatabaseModels/User";
|
||||
|
|
@ -43,8 +43,8 @@ export class Service extends DatabaseService<Model> {
|
|||
throw new BadDataException("User email is required");
|
||||
}
|
||||
|
||||
createBy.data.twoFactorSecret = TwoFactorAuth.generateSecret();
|
||||
createBy.data.twoFactorOtpUrl = TwoFactorAuth.generateUri({
|
||||
createBy.data.twoFactorSecret = TotpAuth.generateSecret();
|
||||
createBy.data.twoFactorOtpUrl = TotpAuth.generateUri({
|
||||
secret: createBy.data.twoFactorSecret,
|
||||
userEmail: user.email,
|
||||
});
|
||||
400
Common/Server/Services/UserWebAuthnService.ts
Normal file
400
Common/Server/Services/UserWebAuthnService.ts
Normal file
|
|
@ -0,0 +1,400 @@
|
|||
import CreateBy from "../Types/Database/CreateBy";
|
||||
import { OnCreate, OnDelete } from "../Types/Database/Hooks";
|
||||
import DatabaseService from "./DatabaseService";
|
||||
import Model from "../../Models/DatabaseModels/UserWebAuthn";
|
||||
import UserService from "./UserService";
|
||||
import BadDataException from "../../Types/Exception/BadDataException";
|
||||
import User from "../../Models/DatabaseModels/User";
|
||||
import DeleteBy from "../Types/Database/DeleteBy";
|
||||
import LIMIT_MAX, { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
|
||||
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
||||
import {
|
||||
generateRegistrationOptions,
|
||||
verifyRegistrationResponse,
|
||||
generateAuthenticationOptions,
|
||||
verifyAuthenticationResponse,
|
||||
} from "@simplewebauthn/server";
|
||||
import { Host, HttpProtocol } from "../EnvironmentConfig";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
public constructor() {
|
||||
super(Model);
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public async generateRegistrationOptions(data: {
|
||||
userId: ObjectID;
|
||||
}): Promise<{ options: any; challenge: string }> {
|
||||
const user: User | null = await UserService.findOneById({
|
||||
id: data.userId,
|
||||
select: {
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new BadDataException("User not found");
|
||||
}
|
||||
|
||||
if (!user.email) {
|
||||
throw new BadDataException("User email not found");
|
||||
}
|
||||
|
||||
// Get existing credentials for this user
|
||||
const existingCredentials: Array<Model> = await this.findBy({
|
||||
query: {
|
||||
userId: data.userId,
|
||||
},
|
||||
select: {
|
||||
credentialId: true,
|
||||
},
|
||||
limit: LIMIT_PER_PROJECT,
|
||||
skip: 0,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
const options: any = await generateRegistrationOptions({
|
||||
rpName: "OneUptime",
|
||||
rpID: Host.toString(),
|
||||
userID: new Uint8Array(Buffer.from(data.userId.toString())),
|
||||
userName: user.email.toString(),
|
||||
userDisplayName: user.name ? user.name.toString() : user.email.toString(),
|
||||
attestationType: "none",
|
||||
excludeCredentials: existingCredentials
|
||||
.filter((cred: Model) => {
|
||||
return cred.credentialId;
|
||||
})
|
||||
.map((cred: Model) => {
|
||||
return {
|
||||
id: cred.credentialId!,
|
||||
type: "public-key",
|
||||
};
|
||||
}),
|
||||
authenticatorSelection: {
|
||||
residentKey: "discouraged",
|
||||
userVerification: "preferred",
|
||||
},
|
||||
});
|
||||
|
||||
// Convert to JSON serializable format
|
||||
options.challenge = Buffer.from(options.challenge).toString("base64url");
|
||||
if (options.excludeCredentials) {
|
||||
options.excludeCredentials = options.excludeCredentials.map(
|
||||
(cred: any) => {
|
||||
return {
|
||||
...cred,
|
||||
id:
|
||||
typeof cred.id === "string"
|
||||
? cred.id
|
||||
: Buffer.from(cred.id).toString("base64url"),
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
options: options as any,
|
||||
challenge: options.challenge,
|
||||
};
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public async verifyRegistration(data: {
|
||||
challenge: string;
|
||||
credential: any;
|
||||
name: string;
|
||||
props: DatabaseCommonInteractionProps;
|
||||
}): Promise<void> {
|
||||
const expectedOrigin: string = `${HttpProtocol}${Host.toString()}`;
|
||||
|
||||
const verification: any = await verifyRegistrationResponse({
|
||||
response: data.credential,
|
||||
expectedChallenge: data.challenge,
|
||||
expectedOrigin: expectedOrigin,
|
||||
expectedRPID: Host.toString(),
|
||||
});
|
||||
|
||||
if (!verification.verified) {
|
||||
throw new BadDataException("Registration verification failed");
|
||||
}
|
||||
|
||||
const { registrationInfo } = verification;
|
||||
|
||||
if (!registrationInfo) {
|
||||
throw new BadDataException("Registration info not found");
|
||||
}
|
||||
|
||||
if (!data.props.userId) {
|
||||
throw new BadDataException("User ID not found in request");
|
||||
}
|
||||
|
||||
// Save the credential
|
||||
const userWebAuthn: Model = Model.fromJSON(
|
||||
{
|
||||
name: data.name,
|
||||
credentialId: registrationInfo.credential.id,
|
||||
publicKey: Buffer.from(registrationInfo.credential.publicKey).toString(
|
||||
"base64",
|
||||
),
|
||||
counter: "0",
|
||||
transports: JSON.stringify([]),
|
||||
isVerified: true,
|
||||
userId: data.props.userId,
|
||||
},
|
||||
Model,
|
||||
) as Model;
|
||||
|
||||
await this.create({
|
||||
data: userWebAuthn,
|
||||
props: data.props,
|
||||
});
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public async generateAuthenticationOptions(data: {
|
||||
email: string;
|
||||
}): Promise<{ options: any; challenge: string; userId: string }> {
|
||||
const user: User | null = await UserService.findOneBy({
|
||||
query: { email: data.email },
|
||||
select: {
|
||||
_id: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new BadDataException("User not found");
|
||||
}
|
||||
|
||||
// Get user's WebAuthn credentials
|
||||
const credentials: Array<Model> = await this.findBy({
|
||||
query: {
|
||||
userId: user.id!,
|
||||
isVerified: true,
|
||||
},
|
||||
select: {
|
||||
credentialId: true,
|
||||
},
|
||||
limit: LIMIT_PER_PROJECT,
|
||||
skip: 0,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (credentials.length === 0) {
|
||||
throw new BadDataException("No WebAuthn credentials found for this user");
|
||||
}
|
||||
|
||||
const options: any = await generateAuthenticationOptions({
|
||||
rpID: Host.toString(),
|
||||
allowCredentials: credentials.map((cred: Model) => {
|
||||
return {
|
||||
id: cred.credentialId!,
|
||||
type: "public-key",
|
||||
};
|
||||
}),
|
||||
userVerification: "preferred",
|
||||
});
|
||||
|
||||
// Convert to JSON serializable format
|
||||
options.challenge = Buffer.from(options.challenge).toString("base64url");
|
||||
// allowCredentials id is already base64url string
|
||||
|
||||
return {
|
||||
options: options as any,
|
||||
challenge: options.challenge,
|
||||
userId: user.id!.toString(),
|
||||
};
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public async verifyAuthentication(data: {
|
||||
userId: string;
|
||||
challenge: string;
|
||||
credential: any;
|
||||
}): Promise<User> {
|
||||
const user: User | null = await UserService.findOneById({
|
||||
id: new ObjectID(data.userId),
|
||||
select: {
|
||||
_id: true,
|
||||
email: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new BadDataException("User not found");
|
||||
}
|
||||
|
||||
// Get the credential from database
|
||||
const dbCredential: Model | null = await this.findOneBy({
|
||||
query: {
|
||||
credentialId: data.credential.id,
|
||||
userId: new ObjectID(data.userId),
|
||||
isVerified: true,
|
||||
},
|
||||
select: {
|
||||
credentialId: true,
|
||||
publicKey: true,
|
||||
counter: true,
|
||||
_id: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!dbCredential) {
|
||||
throw new BadDataException("Credential not found");
|
||||
}
|
||||
|
||||
const expectedOrigin: string = `${HttpProtocol}${Host.toString()}`;
|
||||
|
||||
const verification: any = await verifyAuthenticationResponse({
|
||||
response: data.credential,
|
||||
expectedChallenge: data.challenge,
|
||||
expectedOrigin: expectedOrigin,
|
||||
expectedRPID: Host.toString(),
|
||||
credential: {
|
||||
id: dbCredential.credentialId!,
|
||||
publicKey: Buffer.from(dbCredential.publicKey!, "base64"),
|
||||
counter: parseInt(dbCredential.counter!),
|
||||
} as any,
|
||||
});
|
||||
|
||||
if (!verification.verified) {
|
||||
throw new BadDataException("Authentication verification failed");
|
||||
}
|
||||
|
||||
// Update counter
|
||||
await this.updateOneById({
|
||||
id: dbCredential.id!,
|
||||
data: {
|
||||
counter: verification.authenticationInfo.newCounter.toString(),
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
protected override async onBeforeCreate(
|
||||
createBy: CreateBy<Model>,
|
||||
): Promise<OnCreate<Model>> {
|
||||
if (!createBy.props.userId) {
|
||||
throw new BadDataException("User id is required");
|
||||
}
|
||||
|
||||
createBy.data.userId = createBy.props.userId;
|
||||
|
||||
const user: User | null = await UserService.findOneById({
|
||||
id: createBy.data.userId,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
select: {
|
||||
email: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new BadDataException("User not found");
|
||||
}
|
||||
|
||||
if (!user.email) {
|
||||
throw new BadDataException("User email is required");
|
||||
}
|
||||
|
||||
// by default secuirty keys are always verified. You can't add an unverified security key.
|
||||
|
||||
createBy.data.isVerified = true;
|
||||
|
||||
return {
|
||||
createBy: createBy,
|
||||
carryForward: {},
|
||||
};
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
protected override async onBeforeDelete(
|
||||
deleteBy: DeleteBy<Model>,
|
||||
): Promise<OnDelete<Model>> {
|
||||
const itemsToBeDeleted: Array<Model> = await this.findBy({
|
||||
query: deleteBy.query,
|
||||
select: {
|
||||
userId: true,
|
||||
_id: true,
|
||||
isVerified: true,
|
||||
},
|
||||
limit: LIMIT_MAX,
|
||||
skip: 0,
|
||||
props: deleteBy.props,
|
||||
});
|
||||
|
||||
for (const item of itemsToBeDeleted) {
|
||||
if (item.isVerified) {
|
||||
// check if user two auth is enabled.
|
||||
|
||||
const user: User | null = await UserService.findOneById({
|
||||
id: item.userId!,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
select: {
|
||||
enableTwoFactorAuth: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new BadDataException("User not found");
|
||||
}
|
||||
|
||||
if (user.enableTwoFactorAuth) {
|
||||
// if enabled then check if this is the only verified item for this user.
|
||||
|
||||
const verifiedItems: Array<Model> = await this.findBy({
|
||||
query: {
|
||||
userId: item.userId!,
|
||||
isVerified: true,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
},
|
||||
limit: LIMIT_MAX,
|
||||
skip: 0,
|
||||
props: deleteBy.props,
|
||||
});
|
||||
|
||||
if (verifiedItems.length === 1) {
|
||||
throw new BadDataException(
|
||||
"You must have atleast one verified two factor auth. Please disable two factor auth before deleting this item.",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
deleteBy: deleteBy,
|
||||
carryForward: {},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default new Service();
|
||||
|
|
@ -3,9 +3,9 @@ import * as OTPAuth from "otpauth";
|
|||
import CaptureSpan from "./Telemetry/CaptureSpan";
|
||||
|
||||
/**
|
||||
* Utility class for handling two-factor authentication.
|
||||
* Utility class for handling TOTP authentication.
|
||||
*/
|
||||
export default class TwoFactorAuth {
|
||||
export default class TotpAuth {
|
||||
/**
|
||||
* Generates a random secret key for two-factor authentication.
|
||||
* @returns The generated secret key.
|
||||
|
|
@ -22,7 +22,12 @@ export default abstract class LoginUtil {
|
|||
if (user.timezone) {
|
||||
UserUtil.setSavedUserTimezone(user.timezone);
|
||||
}
|
||||
UserUtil.setIsMasterAdmin(user.isMasterAdmin as boolean);
|
||||
|
||||
if (user.isMasterAdmin) {
|
||||
UserUtil.setIsMasterAdmin(user.isMasterAdmin as boolean);
|
||||
} else {
|
||||
UserUtil.setIsMasterAdmin(false);
|
||||
}
|
||||
|
||||
if (user.profilePictureId) {
|
||||
UserUtil.setProfilePicId(user.profilePictureId);
|
||||
|
|
|
|||
13
Common/Utils/Base64.ts
Normal file
13
Common/Utils/Base64.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
class Base64 {
|
||||
public static base64UrlToUint8Array(base64Url: string): Uint8Array {
|
||||
const base64: string = base64Url.replace(/-/g, "+").replace(/_/g, "/");
|
||||
return Buffer.from(base64, "base64") as Uint8Array;
|
||||
}
|
||||
|
||||
public static uint8ArrayToBase64Url(uint8Array: Uint8Array): string {
|
||||
const base64: string = Buffer.from(uint8Array).toString("base64");
|
||||
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/[=]/g, "");
|
||||
}
|
||||
}
|
||||
|
||||
export default Base64;
|
||||
289
Common/package-lock.json
generated
289
Common/package-lock.json
generated
|
|
@ -33,6 +33,7 @@
|
|||
"@opentelemetry/sdk-trace-web": "^1.25.1",
|
||||
"@opentelemetry/semantic-conventions": "^1.26.0",
|
||||
"@remixicon/react": "^4.2.0",
|
||||
"@simplewebauthn/server": "^13.2.1",
|
||||
"@tippyjs/react": "^4.2.6",
|
||||
"@types/archiver": "^6.0.3",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
|
|
@ -1553,6 +1554,12 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@hexagon/base64": {
|
||||
"version": "1.1.28",
|
||||
"resolved": "https://registry.npmjs.org/@hexagon/base64/-/base64-1.1.28.tgz",
|
||||
"integrity": "sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@icons/material": {
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@icons/material/-/material-0.2.4.tgz",
|
||||
|
|
@ -2078,6 +2085,12 @@
|
|||
"url": "https://opencollective.com/js-sdsl"
|
||||
}
|
||||
},
|
||||
"node_modules/@levischuck/tiny-cbor": {
|
||||
"version": "0.2.11",
|
||||
"resolved": "https://registry.npmjs.org/@levischuck/tiny-cbor/-/tiny-cbor-0.2.11.tgz",
|
||||
"integrity": "sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@monaco-editor/loader": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.4.0.tgz",
|
||||
|
|
@ -3621,150 +3634,160 @@
|
|||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@peculiar/asn1-cms": {
|
||||
"version": "2.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.3.13.tgz",
|
||||
"integrity": "sha512-joqu8A7KR2G85oLPq+vB+NFr2ro7Ls4ol13Zcse/giPSzUNN0n2k3v8kMpf6QdGUhI13e5SzQYN8AKP8sJ8v4w==",
|
||||
"node_modules/@peculiar/asn1-android": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-android/-/asn1-android-2.5.0.tgz",
|
||||
"integrity": "sha512-t8A83hgghWQkcneRsgGs2ebAlRe54ns88p7ouv8PW2tzF1nAW4yHcL4uZKrFpIU+uszIRzTkcCuie37gpkId0A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@peculiar/asn1-schema": "^2.3.13",
|
||||
"@peculiar/asn1-x509": "^2.3.13",
|
||||
"@peculiar/asn1-x509-attr": "^2.3.13",
|
||||
"asn1js": "^3.0.5",
|
||||
"tslib": "^2.6.2"
|
||||
"@peculiar/asn1-schema": "^2.5.0",
|
||||
"asn1js": "^3.0.6",
|
||||
"tslib": "^2.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@peculiar/asn1-cms": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.5.0.tgz",
|
||||
"integrity": "sha512-p0SjJ3TuuleIvjPM4aYfvYw8Fk1Hn/zAVyPJZTtZ2eE9/MIer6/18ROxX6N/e6edVSfvuZBqhxAj3YgsmSjQ/A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@peculiar/asn1-schema": "^2.5.0",
|
||||
"@peculiar/asn1-x509": "^2.5.0",
|
||||
"@peculiar/asn1-x509-attr": "^2.5.0",
|
||||
"asn1js": "^3.0.6",
|
||||
"tslib": "^2.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@peculiar/asn1-csr": {
|
||||
"version": "2.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.3.13.tgz",
|
||||
"integrity": "sha512-+JtFsOUWCw4zDpxp1LbeTYBnZLlGVOWmHHEhoFdjM5yn4wCn+JiYQ8mghOi36M2f6TPQ17PmhNL6/JfNh7/jCA==",
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.5.0.tgz",
|
||||
"integrity": "sha512-ioigvA6WSYN9h/YssMmmoIwgl3RvZlAYx4A/9jD2qaqXZwGcNlAxaw54eSx2QG1Yu7YyBC5Rku3nNoHrQ16YsQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@peculiar/asn1-schema": "^2.3.13",
|
||||
"@peculiar/asn1-x509": "^2.3.13",
|
||||
"asn1js": "^3.0.5",
|
||||
"tslib": "^2.6.2"
|
||||
"@peculiar/asn1-schema": "^2.5.0",
|
||||
"@peculiar/asn1-x509": "^2.5.0",
|
||||
"asn1js": "^3.0.6",
|
||||
"tslib": "^2.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@peculiar/asn1-ecc": {
|
||||
"version": "2.3.14",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.3.14.tgz",
|
||||
"integrity": "sha512-zWPyI7QZto6rnLv6zPniTqbGaLh6zBpJyI46r1yS/bVHJXT2amdMHCRRnbV5yst2H8+ppXG6uXu/M6lKakiQ8w==",
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.5.0.tgz",
|
||||
"integrity": "sha512-t4eYGNhXtLRxaP50h3sfO6aJebUCDGQACoeexcelL4roMFRRVgB20yBIu2LxsPh/tdW9I282gNgMOyg3ywg/mg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@peculiar/asn1-schema": "^2.3.13",
|
||||
"@peculiar/asn1-x509": "^2.3.13",
|
||||
"asn1js": "^3.0.5",
|
||||
"tslib": "^2.6.2"
|
||||
"@peculiar/asn1-schema": "^2.5.0",
|
||||
"@peculiar/asn1-x509": "^2.5.0",
|
||||
"asn1js": "^3.0.6",
|
||||
"tslib": "^2.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@peculiar/asn1-pfx": {
|
||||
"version": "2.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.3.13.tgz",
|
||||
"integrity": "sha512-fypYxjn16BW+5XbFoY11Rm8LhZf6euqX/C7BTYpqVvLem1GvRl7A+Ro1bO/UPwJL0z+1mbvXEnkG0YOwbwz2LA==",
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.5.0.tgz",
|
||||
"integrity": "sha512-Vj0d0wxJZA+Ztqfb7W+/iu8Uasw6hhKtCdLKXLG/P3kEPIQpqGI4P4YXlROfl7gOCqFIbgsj1HzFIFwQ5s20ug==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@peculiar/asn1-cms": "^2.3.13",
|
||||
"@peculiar/asn1-pkcs8": "^2.3.13",
|
||||
"@peculiar/asn1-rsa": "^2.3.13",
|
||||
"@peculiar/asn1-schema": "^2.3.13",
|
||||
"asn1js": "^3.0.5",
|
||||
"tslib": "^2.6.2"
|
||||
"@peculiar/asn1-cms": "^2.5.0",
|
||||
"@peculiar/asn1-pkcs8": "^2.5.0",
|
||||
"@peculiar/asn1-rsa": "^2.5.0",
|
||||
"@peculiar/asn1-schema": "^2.5.0",
|
||||
"asn1js": "^3.0.6",
|
||||
"tslib": "^2.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@peculiar/asn1-pkcs8": {
|
||||
"version": "2.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.3.13.tgz",
|
||||
"integrity": "sha512-VP3PQzbeSSjPjKET5K37pxyf2qCdM0dz3DJ56ZCsol3FqAXGekb4sDcpoL9uTLGxAh975WcdvUms9UcdZTuGyQ==",
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.5.0.tgz",
|
||||
"integrity": "sha512-L7599HTI2SLlitlpEP8oAPaJgYssByI4eCwQq2C9eC90otFpm8MRn66PpbKviweAlhinWQ3ZjDD2KIVtx7PaVw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@peculiar/asn1-schema": "^2.3.13",
|
||||
"@peculiar/asn1-x509": "^2.3.13",
|
||||
"asn1js": "^3.0.5",
|
||||
"tslib": "^2.6.2"
|
||||
"@peculiar/asn1-schema": "^2.5.0",
|
||||
"@peculiar/asn1-x509": "^2.5.0",
|
||||
"asn1js": "^3.0.6",
|
||||
"tslib": "^2.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@peculiar/asn1-pkcs9": {
|
||||
"version": "2.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.3.13.tgz",
|
||||
"integrity": "sha512-rIwQXmHpTo/dgPiWqUgby8Fnq6p1xTJbRMxCiMCk833kQCeZrC5lbSKg6NDnJTnX2kC6IbXBB9yCS2C73U2gJg==",
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.5.0.tgz",
|
||||
"integrity": "sha512-UgqSMBLNLR5TzEZ5ZzxR45Nk6VJrammxd60WMSkofyNzd3DQLSNycGWSK5Xg3UTYbXcDFyG8pA/7/y/ztVCa6A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@peculiar/asn1-cms": "^2.3.13",
|
||||
"@peculiar/asn1-pfx": "^2.3.13",
|
||||
"@peculiar/asn1-pkcs8": "^2.3.13",
|
||||
"@peculiar/asn1-schema": "^2.3.13",
|
||||
"@peculiar/asn1-x509": "^2.3.13",
|
||||
"@peculiar/asn1-x509-attr": "^2.3.13",
|
||||
"asn1js": "^3.0.5",
|
||||
"tslib": "^2.6.2"
|
||||
"@peculiar/asn1-cms": "^2.5.0",
|
||||
"@peculiar/asn1-pfx": "^2.5.0",
|
||||
"@peculiar/asn1-pkcs8": "^2.5.0",
|
||||
"@peculiar/asn1-schema": "^2.5.0",
|
||||
"@peculiar/asn1-x509": "^2.5.0",
|
||||
"@peculiar/asn1-x509-attr": "^2.5.0",
|
||||
"asn1js": "^3.0.6",
|
||||
"tslib": "^2.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@peculiar/asn1-rsa": {
|
||||
"version": "2.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.3.13.tgz",
|
||||
"integrity": "sha512-wBNQqCyRtmqvXkGkL4DR3WxZhHy8fDiYtOjTeCd7SFE5F6GBeafw3EJ94PX/V0OJJrjQ40SkRY2IZu3ZSyBqcg==",
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.5.0.tgz",
|
||||
"integrity": "sha512-qMZ/vweiTHy9syrkkqWFvbT3eLoedvamcUdnnvwyyUNv5FgFXA3KP8td+ATibnlZ0EANW5PYRm8E6MJzEB/72Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@peculiar/asn1-schema": "^2.3.13",
|
||||
"@peculiar/asn1-x509": "^2.3.13",
|
||||
"asn1js": "^3.0.5",
|
||||
"tslib": "^2.6.2"
|
||||
"@peculiar/asn1-schema": "^2.5.0",
|
||||
"@peculiar/asn1-x509": "^2.5.0",
|
||||
"asn1js": "^3.0.6",
|
||||
"tslib": "^2.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@peculiar/asn1-schema": {
|
||||
"version": "2.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.3.13.tgz",
|
||||
"integrity": "sha512-3Xq3a01WkHRZL8X04Zsfg//mGaA21xlL4tlVn4v2xGT0JStiztATRkMwa5b+f/HXmY2smsiLXYK46Gwgzvfg3g==",
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.5.0.tgz",
|
||||
"integrity": "sha512-YM/nFfskFJSlHqv59ed6dZlLZqtZQwjRVJ4bBAiWV08Oc+1rSd5lDZcBEx0lGDHfSoH3UziI2pXt2UM33KerPQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asn1js": "^3.0.5",
|
||||
"pvtsutils": "^1.3.5",
|
||||
"tslib": "^2.6.2"
|
||||
"asn1js": "^3.0.6",
|
||||
"pvtsutils": "^1.3.6",
|
||||
"tslib": "^2.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@peculiar/asn1-x509": {
|
||||
"version": "2.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.3.13.tgz",
|
||||
"integrity": "sha512-PfeLQl2skXmxX2/AFFCVaWU8U6FKW1Db43mgBhShCOFS1bVxqtvusq1hVjfuEcuSQGedrLdCSvTgabluwN/M9A==",
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.5.0.tgz",
|
||||
"integrity": "sha512-CpwtMCTJvfvYTFMuiME5IH+8qmDe3yEWzKHe7OOADbGfq7ohxeLaXwQo0q4du3qs0AII3UbLCvb9NF/6q0oTKQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@peculiar/asn1-schema": "^2.3.13",
|
||||
"asn1js": "^3.0.5",
|
||||
"ipaddr.js": "^2.1.0",
|
||||
"pvtsutils": "^1.3.5",
|
||||
"tslib": "^2.6.2"
|
||||
"@peculiar/asn1-schema": "^2.5.0",
|
||||
"asn1js": "^3.0.6",
|
||||
"pvtsutils": "^1.3.6",
|
||||
"tslib": "^2.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@peculiar/asn1-x509-attr": {
|
||||
"version": "2.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.3.13.tgz",
|
||||
"integrity": "sha512-WpEos6CcnUzJ6o2Qb68Z7Dz5rSjRGv/DtXITCNBtjZIRWRV12yFVci76SVfOX8sisL61QWMhpLKQibrG8pi2Pw==",
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.5.0.tgz",
|
||||
"integrity": "sha512-9f0hPOxiJDoG/bfNLAFven+Bd4gwz/VzrCIIWc1025LEI4BXO0U5fOCTNDPbbp2ll+UzqKsZ3g61mpBp74gk9A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@peculiar/asn1-schema": "^2.3.13",
|
||||
"@peculiar/asn1-x509": "^2.3.13",
|
||||
"asn1js": "^3.0.5",
|
||||
"tslib": "^2.6.2"
|
||||
"@peculiar/asn1-schema": "^2.5.0",
|
||||
"@peculiar/asn1-x509": "^2.5.0",
|
||||
"asn1js": "^3.0.6",
|
||||
"tslib": "^2.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@peculiar/x509": {
|
||||
"version": "1.12.3",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.12.3.tgz",
|
||||
"integrity": "sha512-+Mzq+W7cNEKfkNZzyLl6A6ffqc3r21HGZUezgfKxpZrkORfOqgRXnS80Zu0IV6a9Ue9QBJeKD7kN0iWfc3bhRQ==",
|
||||
"version": "1.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.14.0.tgz",
|
||||
"integrity": "sha512-Yc4PDxN3OrxUPiXgU63c+ZRXKGE8YKF2McTciYhUHFtHVB0KMnjeFSU0qpztGhsp4P0uKix4+J2xEpIEDu8oXg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@peculiar/asn1-cms": "^2.3.13",
|
||||
"@peculiar/asn1-csr": "^2.3.13",
|
||||
"@peculiar/asn1-ecc": "^2.3.14",
|
||||
"@peculiar/asn1-pkcs9": "^2.3.13",
|
||||
"@peculiar/asn1-rsa": "^2.3.13",
|
||||
"@peculiar/asn1-schema": "^2.3.13",
|
||||
"@peculiar/asn1-x509": "^2.3.13",
|
||||
"pvtsutils": "^1.3.5",
|
||||
"@peculiar/asn1-cms": "^2.5.0",
|
||||
"@peculiar/asn1-csr": "^2.5.0",
|
||||
"@peculiar/asn1-ecc": "^2.5.0",
|
||||
"@peculiar/asn1-pkcs9": "^2.5.0",
|
||||
"@peculiar/asn1-rsa": "^2.5.0",
|
||||
"@peculiar/asn1-schema": "^2.5.0",
|
||||
"@peculiar/asn1-x509": "^2.5.0",
|
||||
"pvtsutils": "^1.3.6",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"tslib": "^2.7.0",
|
||||
"tsyringe": "^4.8.0"
|
||||
"tslib": "^2.8.1",
|
||||
"tsyringe": "^4.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@pkgjs/parseargs": {
|
||||
|
|
@ -3983,6 +4006,25 @@
|
|||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@simplewebauthn/server": {
|
||||
"version": "13.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@simplewebauthn/server/-/server-13.2.1.tgz",
|
||||
"integrity": "sha512-Inmfye5opZXe3HI0GaksqBnQiM7glcNySoG6DH1GgkO1Lh9dvuV4XSV9DK02DReUVX39HpcDob9nxHELjECoQw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@hexagon/base64": "^1.1.27",
|
||||
"@levischuck/tiny-cbor": "^0.2.2",
|
||||
"@peculiar/asn1-android": "^2.3.10",
|
||||
"@peculiar/asn1-ecc": "^2.3.8",
|
||||
"@peculiar/asn1-rsa": "^2.3.8",
|
||||
"@peculiar/asn1-schema": "^2.3.8",
|
||||
"@peculiar/asn1-x509": "^2.3.8",
|
||||
"@peculiar/x509": "^1.13.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@sinclair/typebox": {
|
||||
"version": "0.24.51",
|
||||
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz",
|
||||
|
|
@ -4032,9 +4074,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@testing-library/dom": {
|
||||
"version": "10.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz",
|
||||
"integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==",
|
||||
"version": "10.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
|
||||
"integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
|
|
@ -4043,9 +4085,9 @@
|
|||
"@babel/runtime": "^7.12.5",
|
||||
"@types/aria-query": "^5.0.1",
|
||||
"aria-query": "5.3.0",
|
||||
"chalk": "^4.1.0",
|
||||
"dom-accessibility-api": "^0.5.9",
|
||||
"lz-string": "^1.5.0",
|
||||
"picocolors": "1.1.1",
|
||||
"pretty-format": "^27.0.2"
|
||||
},
|
||||
"engines": {
|
||||
|
|
@ -5021,6 +5063,13 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/trusted-types": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-1.0.6.tgz",
|
||||
"integrity": "sha512-230RC8sFeHoT6sSUlRO6a8cAnclO06eeiq1QDfiv2FGCLWFvvERWgwIQD4FWqD9A69BN7Lzee4OXwoMVnnsWDw==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@types/unist": {
|
||||
"version": "2.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz",
|
||||
|
|
@ -5506,14 +5555,14 @@
|
|||
}
|
||||
},
|
||||
"node_modules/asn1js": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.5.tgz",
|
||||
"integrity": "sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ==",
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.6.tgz",
|
||||
"integrity": "sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"pvtsutils": "^1.3.2",
|
||||
"pvtsutils": "^1.3.6",
|
||||
"pvutils": "^1.1.3",
|
||||
"tslib": "^2.4.0"
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
|
|
@ -9026,15 +9075,6 @@
|
|||
"url": "https://opencollective.com/ioredis"
|
||||
}
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz",
|
||||
"integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/is-alphabetical": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz",
|
||||
|
|
@ -12300,11 +12340,14 @@
|
|||
}
|
||||
},
|
||||
"node_modules/monaco-editor": {
|
||||
"version": "0.52.0",
|
||||
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.0.tgz",
|
||||
"integrity": "sha512-OeWhNpABLCeTqubfqLMXGsqf6OmPU6pHM85kF3dhy6kq5hnhuVS1p3VrEW/XhWHc71P2tHyS5JFySD8mgs1crw==",
|
||||
"version": "0.53.0",
|
||||
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.53.0.tgz",
|
||||
"integrity": "sha512-0WNThgC6CMWNXXBxTbaYYcunj08iB5rnx4/G56UOPeL9UVIUGGHA1GR0EWIh9Ebabj7NpCRawQ5b0hfN1jQmYQ==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/trusted-types": "^1.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mri": {
|
||||
"version": "1.2.0",
|
||||
|
|
@ -13340,12 +13383,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/pvtsutils": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.5.tgz",
|
||||
"integrity": "sha512-ARvb14YB9Nm2Xi6nBq1ZX6dAM0FsJnuk+31aUp4TrcZEdKUlSqOqsxJHUPJDNE3qiIp+iUPEIeR6Je/tgV7zsA==",
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz",
|
||||
"integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.6.1"
|
||||
"tslib": "^2.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/pvutils": {
|
||||
|
|
@ -15919,9 +15962,9 @@
|
|||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/tsyringe": {
|
||||
"version": "4.8.0",
|
||||
"resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.8.0.tgz",
|
||||
"integrity": "sha512-YB1FG+axdxADa3ncEtRnQCFq/M0lALGLxSZeVNbTU8NqhOVc51nnv2CISTcvc1kyv6EGPtXVr0v6lWeDxiijOA==",
|
||||
"version": "4.10.0",
|
||||
"resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.10.0.tgz",
|
||||
"integrity": "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^1.9.3"
|
||||
|
|
@ -16185,9 +16228,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.6.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
|
||||
"integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
|
||||
"version": "5.9.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
|
||||
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@
|
|||
"@opentelemetry/sdk-trace-web": "^1.25.1",
|
||||
"@opentelemetry/semantic-conventions": "^1.26.0",
|
||||
"@remixicon/react": "^4.2.0",
|
||||
"@simplewebauthn/server": "^13.2.1",
|
||||
"@tippyjs/react": "^4.2.6",
|
||||
"@types/archiver": "^6.0.3",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
|
|
|
|||
|
|
@ -7,5 +7,5 @@
|
|||
"ignore": ["./node_modules/**", "./public/**", "./bin/**", "./build/**"],
|
||||
"watchOptions": {"useFsEvents": false, "interval": 500},
|
||||
"env": {"TS_NODE_TRANSPILE_ONLY": "1", "TS_NODE_FILES": "false"},
|
||||
"exec": "node --inspect=0.0.0.0:9229 -r ts-node/register/transpile-only Index.ts"
|
||||
"exec": "node -r ts-node/register/transpile-only Index.ts"
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
#
|
||||
|
||||
# Pull base image nodejs image.
|
||||
FROM public.ecr.aws/docker/library/node:23.8-alpine3.21
|
||||
FROM public.ecr.aws/docker/library/node:24.9-alpine3.21
|
||||
RUN mkdir /tmp/npm && chmod 2777 /tmp/npm && chown 1000:1000 /tmp/npm && npm config set cache /tmp/npm --global
|
||||
|
||||
RUN npm config set fetch-retries 5
|
||||
|
|
|
|||
|
|
@ -7,8 +7,9 @@ import Route from "Common/Types/API/Route";
|
|||
import Page from "Common/UI/Components/Page/Page";
|
||||
import React, { FunctionComponent, ReactElement } from "react";
|
||||
import UserUtil from "Common/UI/Utils/User";
|
||||
import UserTwoFactorAuth from "Common/Models/DatabaseModels/UserTwoFactorAuth";
|
||||
import { ButtonStyleType } from "Common/UI/Components/Button/Button";
|
||||
import UserTotpAuth from "Common/Models/DatabaseModels/UserTotpAuth";
|
||||
import UserWebAuthn from "Common/Models/DatabaseModels/UserWebAuthn";
|
||||
import Button, { ButtonStyleType } from "Common/UI/Components/Button/Button";
|
||||
import IconProp from "Common/Types/Icon/IconProp";
|
||||
import FieldType from "Common/UI/Components/Types/FieldType";
|
||||
import FormFieldSchemaType from "Common/UI/Components/Forms/Types/FormFieldSchemaType";
|
||||
|
|
@ -26,10 +27,11 @@ import { CustomElementProps } from "Common/UI/Components/Forms/Types/Field";
|
|||
import CardModelDetail from "Common/UI/Components/ModelDetail/CardModelDetail";
|
||||
import User from "Common/Models/DatabaseModels/User";
|
||||
import OneUptimeDate from "Common/Types/Date";
|
||||
import Base64 from "Common/Utils/Base64";
|
||||
|
||||
const Home: FunctionComponent<PageComponentProps> = (): ReactElement => {
|
||||
const [selectedTwoFactorAuth, setSelectedTwoFactorAuth] =
|
||||
React.useState<UserTwoFactorAuth | null>(null);
|
||||
const [selectedTotpAuth, setSelectedTotpAuth] =
|
||||
React.useState<UserTotpAuth | null>(null);
|
||||
const [showVerificationModal, setShowVerificationModal] =
|
||||
React.useState<boolean>(false);
|
||||
const [verificationError, setVerificationError] = React.useState<
|
||||
|
|
@ -42,6 +44,13 @@ const Home: FunctionComponent<PageComponentProps> = (): ReactElement => {
|
|||
OneUptimeDate.getCurrentDate().toString(),
|
||||
);
|
||||
|
||||
const [showWebAuthnRegistrationModal, setShowWebAuthnRegistrationModal] =
|
||||
React.useState<boolean>(false);
|
||||
const [webAuthnRegistrationError, setWebAuthnRegistrationError] =
|
||||
React.useState<string | null>(null);
|
||||
const [webAuthnRegistrationLoading, setWebAuthnRegistrationLoading] =
|
||||
React.useState<boolean>(false);
|
||||
|
||||
return (
|
||||
<Page
|
||||
title={"User Profile"}
|
||||
|
|
@ -66,11 +75,11 @@ const Home: FunctionComponent<PageComponentProps> = (): ReactElement => {
|
|||
sideMenu={<SideMenu />}
|
||||
>
|
||||
<div>
|
||||
<ModelTable<UserTwoFactorAuth>
|
||||
modelType={UserTwoFactorAuth}
|
||||
name="Two Factor Authentication"
|
||||
id="two-factor-auth-table"
|
||||
userPreferencesKey="user-two-factor-auth-table"
|
||||
<ModelTable<UserTotpAuth>
|
||||
modelType={UserTotpAuth}
|
||||
name="Authenticator Based TOTP Authentication"
|
||||
id="totp-auth-table"
|
||||
userPreferencesKey="user-totp-auth-table"
|
||||
isDeleteable={true}
|
||||
refreshToggle={tableRefreshToggle}
|
||||
filters={[]}
|
||||
|
|
@ -82,25 +91,28 @@ const Home: FunctionComponent<PageComponentProps> = (): ReactElement => {
|
|||
isCreateable={true}
|
||||
isViewable={false}
|
||||
cardProps={{
|
||||
title: "Two Factor Authentication",
|
||||
description: "Manage your two factor authentication settings here.",
|
||||
title: "Authenticator Based Two Factor Authentication",
|
||||
description:
|
||||
"Manage your authenticator based two factor authentication settings here.",
|
||||
}}
|
||||
noItemsMessage={"No two factor authentication found."}
|
||||
singularName="Two Factor Authentication"
|
||||
pluralName="Two Factor Authentications"
|
||||
noItemsMessage={
|
||||
"No authenticator based two factor authentication found."
|
||||
}
|
||||
singularName="Authenticator Based Two Factor Authentication"
|
||||
pluralName="Authenticator Based Two Factor Authentications"
|
||||
actionButtons={[
|
||||
{
|
||||
title: "Verify",
|
||||
buttonStyleType: ButtonStyleType.NORMAL,
|
||||
icon: IconProp.Check,
|
||||
isVisible: (item: UserTwoFactorAuth) => {
|
||||
isVisible: (item: UserTotpAuth) => {
|
||||
return !item.isVerified;
|
||||
},
|
||||
onClick: async (
|
||||
item: UserTwoFactorAuth,
|
||||
item: UserTotpAuth,
|
||||
onCompleteAction: VoidFunction,
|
||||
) => {
|
||||
setSelectedTwoFactorAuth(item);
|
||||
setSelectedTotpAuth(item);
|
||||
setShowVerificationModal(true);
|
||||
onCompleteAction();
|
||||
},
|
||||
|
|
@ -137,9 +149,67 @@ const Home: FunctionComponent<PageComponentProps> = (): ReactElement => {
|
|||
},
|
||||
]}
|
||||
/>
|
||||
{showVerificationModal && selectedTwoFactorAuth ? (
|
||||
|
||||
<div>
|
||||
<ModelTable<UserWebAuthn>
|
||||
modelType={UserWebAuthn}
|
||||
name="Security Key-Based Two-Factor Authentication"
|
||||
id="webauthn-table"
|
||||
userPreferencesKey="user-webauthn-table"
|
||||
isDeleteable={true}
|
||||
refreshToggle={tableRefreshToggle}
|
||||
filters={[]}
|
||||
query={{
|
||||
userId: UserUtil.getUserId(),
|
||||
}}
|
||||
isEditable={false}
|
||||
showRefreshButton={true}
|
||||
isCreateable={false}
|
||||
isViewable={false}
|
||||
cardProps={{
|
||||
title: "Security Key-Based Two-Factor Authentication",
|
||||
description:
|
||||
"Manage your security keys for two-factor authentication.",
|
||||
rightElement: (
|
||||
<div className="flex justify-end mb-4">
|
||||
<Button
|
||||
title="Add Security Key"
|
||||
buttonStyle={ButtonStyleType.NORMAL}
|
||||
icon={IconProp.Add}
|
||||
onClick={() => {
|
||||
setWebAuthnRegistrationLoading(false);
|
||||
setWebAuthnRegistrationError(null);
|
||||
return setShowWebAuthnRegistrationModal(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
noItemsMessage={"No security keys found."}
|
||||
singularName="Security Key"
|
||||
pluralName="Security Keys"
|
||||
columns={[
|
||||
{
|
||||
field: {
|
||||
name: true,
|
||||
},
|
||||
title: "Name",
|
||||
type: FieldType.Text,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
isVerified: true,
|
||||
},
|
||||
title: "Is Verified?",
|
||||
type: FieldType.Boolean,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{showVerificationModal && selectedTotpAuth ? (
|
||||
<BasicFormModal
|
||||
title={`Verify ${selectedTwoFactorAuth.name}`}
|
||||
title={`Verify ${selectedTotpAuth.name}`}
|
||||
description={`Please scan this QR code with your authenticator app and enter the code below. This code works with Google Authenticator.`}
|
||||
formProps={{
|
||||
error: verificationError || undefined,
|
||||
|
|
@ -162,7 +232,7 @@ const Home: FunctionComponent<PageComponentProps> = (): ReactElement => {
|
|||
}
|
||||
return (
|
||||
<QRCodeElement
|
||||
text={selectedTwoFactorAuth.twoFactorOtpUrl || ""}
|
||||
text={selectedTotpAuth.twoFactorOtpUrl || ""}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
|
@ -183,7 +253,7 @@ const Home: FunctionComponent<PageComponentProps> = (): ReactElement => {
|
|||
onClose={() => {
|
||||
setShowVerificationModal(false);
|
||||
setVerificationError(null);
|
||||
setSelectedTwoFactorAuth(null);
|
||||
setSelectedTotpAuth(null);
|
||||
}}
|
||||
isLoading={verificationLoading}
|
||||
onSubmit={async (values: JSONObject) => {
|
||||
|
|
@ -195,17 +265,17 @@ const Home: FunctionComponent<PageComponentProps> = (): ReactElement => {
|
|||
| HTTPResponse<EmptyResponseData>
|
||||
| HTTPErrorResponse = await API.post({
|
||||
url: URL.fromString(APP_API_URL.toString()).addRoute(
|
||||
`/user-two-factor-auth/validate`,
|
||||
`/user-totp-auth/validate`,
|
||||
),
|
||||
data: {
|
||||
code: values["code"],
|
||||
id: selectedTwoFactorAuth.id?.toString(),
|
||||
id: selectedTotpAuth.id?.toString(),
|
||||
},
|
||||
});
|
||||
if (response.isSuccess()) {
|
||||
setShowVerificationModal(false);
|
||||
setVerificationError(null);
|
||||
setSelectedTwoFactorAuth(null);
|
||||
setSelectedTotpAuth(null);
|
||||
setVerificationLoading(false);
|
||||
}
|
||||
|
||||
|
|
@ -227,6 +297,124 @@ const Home: FunctionComponent<PageComponentProps> = (): ReactElement => {
|
|||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
||||
{showWebAuthnRegistrationModal ? (
|
||||
<BasicFormModal
|
||||
title="Add Security Key"
|
||||
description="Register a new security key for two factor authentication."
|
||||
formProps={{
|
||||
error: webAuthnRegistrationError || undefined,
|
||||
fields: [
|
||||
{
|
||||
field: {
|
||||
name: true,
|
||||
},
|
||||
title: "Name",
|
||||
description:
|
||||
"Give your security key a name (e.g., YubiKey, Titan Key)",
|
||||
fieldType: FormFieldSchemaType.Text,
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
}}
|
||||
submitButtonText="Register Security Key"
|
||||
onClose={() => {
|
||||
setShowWebAuthnRegistrationModal(false);
|
||||
setWebAuthnRegistrationError(null);
|
||||
setWebAuthnRegistrationLoading(false);
|
||||
}}
|
||||
isLoading={webAuthnRegistrationLoading}
|
||||
onSubmit={async (values: JSONObject) => {
|
||||
try {
|
||||
setWebAuthnRegistrationLoading(true);
|
||||
setWebAuthnRegistrationError("");
|
||||
|
||||
// Generate registration options
|
||||
const response: HTTPResponse<JSONObject> | HTTPErrorResponse =
|
||||
await API.post({
|
||||
url: URL.fromString(APP_API_URL.toString()).addRoute(
|
||||
`/user-webauthn/generate-registration-options`,
|
||||
),
|
||||
data: {},
|
||||
});
|
||||
|
||||
if (response instanceof HTTPErrorResponse) {
|
||||
throw response;
|
||||
}
|
||||
|
||||
const data: any = response.data as any;
|
||||
|
||||
// Convert base64url strings back to Uint8Array
|
||||
data.options.challenge = Base64.base64UrlToUint8Array(
|
||||
data.options.challenge,
|
||||
);
|
||||
if (data.options.excludeCredentials) {
|
||||
data.options.excludeCredentials.forEach((cred: any) => {
|
||||
cred.id = Base64.base64UrlToUint8Array(cred.id);
|
||||
});
|
||||
}
|
||||
if (data.options.user && data.options.user.id) {
|
||||
data.options.user.id = Base64.base64UrlToUint8Array(
|
||||
data.options.user.id,
|
||||
);
|
||||
}
|
||||
|
||||
// Use WebAuthn API
|
||||
const credential: PublicKeyCredential =
|
||||
(await navigator.credentials.create({
|
||||
publicKey: data.options,
|
||||
})) as PublicKeyCredential;
|
||||
|
||||
const attestationResponse: AuthenticatorAttestationResponse =
|
||||
credential.response as AuthenticatorAttestationResponse;
|
||||
|
||||
// Verify registration
|
||||
const verifyResponse:
|
||||
| HTTPResponse<EmptyResponseData>
|
||||
| HTTPErrorResponse = await API.post({
|
||||
url: URL.fromString(APP_API_URL.toString()).addRoute(
|
||||
`/user-webauthn/verify-registration`,
|
||||
),
|
||||
data: {
|
||||
challenge: data.challenge,
|
||||
name: values["name"],
|
||||
credential: {
|
||||
id: credential.id,
|
||||
rawId: Base64.uint8ArrayToBase64Url(
|
||||
new Uint8Array(credential.rawId),
|
||||
),
|
||||
response: {
|
||||
attestationObject: Base64.uint8ArrayToBase64Url(
|
||||
new Uint8Array(attestationResponse.attestationObject),
|
||||
),
|
||||
clientDataJSON: Base64.uint8ArrayToBase64Url(
|
||||
new Uint8Array(attestationResponse.clientDataJSON),
|
||||
),
|
||||
},
|
||||
type: credential.type,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (verifyResponse instanceof HTTPErrorResponse) {
|
||||
throw verifyResponse;
|
||||
}
|
||||
|
||||
setShowWebAuthnRegistrationModal(false);
|
||||
setWebAuthnRegistrationError(null);
|
||||
setTableRefreshToggle(
|
||||
OneUptimeDate.getCurrentDate().toString(),
|
||||
);
|
||||
setWebAuthnRegistrationLoading(false);
|
||||
} catch (err) {
|
||||
setWebAuthnRegistrationError(API.getFriendlyMessage(err));
|
||||
setWebAuthnRegistrationLoading(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
<CardModelDetail<User>
|
||||
cardProps={{
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
#
|
||||
|
||||
# Pull base image nodejs image.
|
||||
FROM public.ecr.aws/docker/library/node:23.8-alpine3.21
|
||||
FROM public.ecr.aws/docker/library/node:24.9-alpine3.21
|
||||
RUN mkdir /tmp/npm && chmod 2777 /tmp/npm && chown 1000:1000 /tmp/npm && npm config set cache /tmp/npm --global
|
||||
|
||||
RUN npm config set fetch-retries 5
|
||||
|
|
|
|||
|
|
@ -8,5 +8,5 @@
|
|||
"./build/**",
|
||||
"greenlock.d/*"
|
||||
],
|
||||
"exec": "node --inspect=0.0.0.0:9229 --require ts-node/register Index.ts"
|
||||
"exec": "node --require ts-node/register Index.ts"
|
||||
}
|
||||
|
|
@ -4,5 +4,5 @@
|
|||
"ignore": ["./node_modules/**", "./public/**", "./bin/**", "./build/**"],
|
||||
"watchOptions": {"useFsEvents": false, "interval": 500},
|
||||
"env": {"TS_NODE_TRANSPILE_ONLY": "1", "TS_NODE_FILES": "false"},
|
||||
"exec": "node --inspect=0.0.0.0:9229 -r ts-node/register/transpile-only Index.ts"
|
||||
"exec": "node -r ts-node/register/transpile-only Index.ts"
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
#
|
||||
|
||||
# Pull base image nodejs image.
|
||||
FROM public.ecr.aws/docker/library/node:23.8-alpine3.21
|
||||
FROM public.ecr.aws/docker/library/node:24.9-alpine3.21
|
||||
RUN mkdir /tmp/npm && chmod 2777 /tmp/npm && chown 1000:1000 /tmp/npm && npm config set cache /tmp/npm --global
|
||||
|
||||
RUN npm config set fetch-retries 5
|
||||
|
|
|
|||
|
|
@ -10,5 +10,5 @@
|
|||
],
|
||||
"watchOptions": {"useFsEvents": false, "interval": 500},
|
||||
"env": {"TS_NODE_TRANSPILE_ONLY": "1", "TS_NODE_FILES": "false"},
|
||||
"exec": "node --inspect=0.0.0.0:9229 -r ts-node/register/transpile-only Index.ts"
|
||||
"exec": "node -r ts-node/register/transpile-only Index.ts"
|
||||
}
|
||||
|
|
@ -4,5 +4,5 @@
|
|||
"ignore": ["./node_modules/**", "./public/**", "./bin/**", "./build/**"],
|
||||
"watchOptions": {"useFsEvents": false, "interval": 500},
|
||||
"env": {"TS_NODE_TRANSPILE_ONLY": "1", "TS_NODE_FILES": "false"},
|
||||
"exec": "node --inspect=0.0.0.0:9229 -r ts-node/register/transpile-only Index.ts"
|
||||
"exec": "node -r ts-node/register/transpile-only Index.ts"
|
||||
}
|
||||
|
|
@ -4,5 +4,5 @@
|
|||
"ignore": ["./node_modules/**", "./public/**", "./bin/**", "./build/**"],
|
||||
"watchOptions": {"useFsEvents": false, "interval": 500},
|
||||
"env": {"TS_NODE_TRANSPILE_ONLY": "1", "TS_NODE_FILES": "false"},
|
||||
"exec": "node --inspect=0.0.0.0:9229 -r ts-node/register/transpile-only Index.ts"
|
||||
"exec": "node -r ts-node/register/transpile-only Index.ts"
|
||||
}
|
||||
|
|
@ -4,5 +4,5 @@
|
|||
"ignore": ["./node_modules/**", "./public/**", "./bin/**", "./build/**"],
|
||||
"watchOptions": {"useFsEvents": false, "interval": 500},
|
||||
"env": {"TS_NODE_TRANSPILE_ONLY": "1", "TS_NODE_FILES": "false"},
|
||||
"exec": "node --inspect=0.0.0.0:9229 -r ts-node/register/transpile-only Index.ts"
|
||||
"exec": "node -r ts-node/register/transpile-only Index.ts"
|
||||
}
|
||||
|
|
@ -7,5 +7,5 @@
|
|||
"ignore": ["./node_modules/**", "./public/**", "./bin/**", "./build/**"],
|
||||
"watchOptions": {"useFsEvents": false, "interval": 500},
|
||||
"env": {"TS_NODE_TRANSPILE_ONLY": "1", "TS_NODE_FILES": "false"},
|
||||
"exec": "node --inspect=0.0.0.0:9229 -r ts-node/register/transpile-only Index.ts"
|
||||
"exec": "node -r ts-node/register/transpile-only Index.ts"
|
||||
}
|
||||
|
|
@ -4,5 +4,5 @@
|
|||
"ignore": ["./node_modules/**", "./public/**", "./bin/**", "./build/**"],
|
||||
"watchOptions": {"useFsEvents": false, "interval": 500},
|
||||
"env": {"TS_NODE_TRANSPILE_ONLY": "1", "TS_NODE_FILES": "false"},
|
||||
"exec": "node --inspect=0.0.0.0:9229 -r ts-node/register/transpile-only Index.ts"
|
||||
"exec": "node -r ts-node/register/transpile-only Index.ts"
|
||||
}
|
||||
|
|
@ -4,5 +4,5 @@
|
|||
"ignore": ["./node_modules/**", "./public/**", "./bin/**", "./build/**"],
|
||||
"watchOptions": {"useFsEvents": false, "interval": 500},
|
||||
"env": {"TS_NODE_TRANSPILE_ONLY": "1", "TS_NODE_FILES": "false"},
|
||||
"exec": "node --inspect=0.0.0.0:9229 -r ts-node/register/transpile-only Index.ts"
|
||||
"exec": "node -r ts-node/register/transpile-only Index.ts"
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
#
|
||||
|
||||
# Pull base image nodejs image.
|
||||
FROM public.ecr.aws/docker/library/node:23.8-alpine3.21
|
||||
FROM public.ecr.aws/docker/library/node:24.9-alpine3.21
|
||||
RUN mkdir /tmp/npm && chmod 2777 /tmp/npm && chown 1000:1000 /tmp/npm && npm config set cache /tmp/npm --global
|
||||
|
||||
RUN npm config set fetch-retries 5
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
#
|
||||
|
||||
# Pull base image nodejs image.
|
||||
FROM public.ecr.aws/docker/library/node:23.8-alpine3.21
|
||||
FROM public.ecr.aws/docker/library/node:24.9-alpine3.21
|
||||
RUN mkdir /tmp/npm && chmod 2777 /tmp/npm && chown 1000:1000 /tmp/npm && npm config set cache /tmp/npm --global
|
||||
|
||||
RUN npm config set fetch-retries 5
|
||||
|
|
|
|||
|
|
@ -4,5 +4,5 @@
|
|||
"ignore": ["./node_modules/**", "./public/**", "./bin/**", "./build/**"],
|
||||
"watchOptions": {"useFsEvents": false, "interval": 500},
|
||||
"env": {"TS_NODE_TRANSPILE_ONLY": "1", "TS_NODE_FILES": "false"},
|
||||
"exec": "node --inspect=0.0.0.0:9229 -r ts-node/register/transpile-only Index.ts"
|
||||
"exec": "node -r ts-node/register/transpile-only Index.ts"
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
FROM public.ecr.aws/docker/library/node:23.8-alpine3.21
|
||||
FROM public.ecr.aws/docker/library/node:24.9-alpine3.21
|
||||
RUN mkdir /tmp/npm && chmod 2777 /tmp/npm && chown 1000:1000 /tmp/npm && npm config set cache /tmp/npm --global
|
||||
|
||||
RUN npm config set fetch-retries 5
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
#
|
||||
|
||||
# Pull base image nodejs image.
|
||||
FROM public.ecr.aws/docker/library/node:23.8-alpine3.21
|
||||
FROM public.ecr.aws/docker/library/node:24.9-alpine3.21
|
||||
RUN mkdir /tmp/npm && chmod 2777 /tmp/npm && chown 1000:1000 /tmp/npm && npm config set cache /tmp/npm --global
|
||||
|
||||
RUN npm config set fetch-retries 5
|
||||
|
|
|
|||
|
|
@ -16,5 +16,5 @@
|
|||
"TS_NODE_TRANSPILE_ONLY": "1",
|
||||
"TS_NODE_FILES": "false"
|
||||
},
|
||||
"exec": "node --inspect=0.0.0.0:9229 -r ts-node/register/transpile-only Index.ts"
|
||||
"exec": "node -r ts-node/register/transpile-only Index.ts"
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
#
|
||||
|
||||
# Pull base image nodejs image.
|
||||
FROM public.ecr.aws/docker/library/node:23.8-alpine3.21
|
||||
FROM public.ecr.aws/docker/library/node:24.9-alpine3.21
|
||||
RUN mkdir /tmp/npm && chmod 2777 /tmp/npm && chown 1000:1000 /tmp/npm && npm config set cache /tmp/npm --global
|
||||
|
||||
RUN npm config set fetch-retries 5
|
||||
|
|
|
|||
|
|
@ -10,5 +10,5 @@
|
|||
],
|
||||
"watchOptions": {"useFsEvents": false, "interval": 500},
|
||||
"env": {"TS_NODE_TRANSPILE_ONLY": "1", "TS_NODE_FILES": "false"},
|
||||
"exec": "node --inspect=0.0.0.0:9229 -r ts-node/register/transpile-only Index.ts"
|
||||
"exec": "node -r ts-node/register/transpile-only Index.ts"
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue