combined otp form

This commit is contained in:
peintnermax
2024-04-17 14:14:25 +02:00
parent 57db64f6bb
commit 20a589cea2
5 changed files with 154 additions and 57 deletions

View File

@@ -1,6 +1,6 @@
import { getBrandingSettings, getLoginSettings, server } from "#/lib/zitadel";
import DynamicTheme from "#/ui/DynamicTheme";
import TOTPForm from "#/ui/TOTPForm";
import LoginOTP from "#/ui/LoginOTP";
import VerifyU2F from "#/ui/VerifyU2F";
export default async function Page({
@@ -15,8 +15,6 @@ export default async function Page({
const { method } = params;
console.log(method);
const branding = await getBrandingSettings(server, organization);
return (
@@ -37,15 +35,13 @@ export default async function Page({
)}
{method && ["time-based", "sms", "email"].includes(method) ? (
<TOTPForm
<LoginOTP
loginName={loginName}
sessionId={sessionId}
code={code}
method={method}
authRequestId={authRequestId}
organization={organization}
submit={submit === "true"}
/>
method={method}
></LoginOTP>
) : (
<VerifyU2F
loginName={loginName}

View File

@@ -1,4 +1,9 @@
import { server, deleteSession, listHumanAuthFactors } from "#/lib/zitadel";
import {
server,
deleteSession,
listHumanAuthFactors,
getSession,
} from "#/lib/zitadel";
import {
SessionCookie,
getMostRecentSessionCookie,
@@ -67,11 +72,10 @@ export async function PUT(request: NextRequest) {
loginName,
sessionId,
organization,
password,
webAuthN,
checks,
authRequestId,
challenges,
} = body;
const challenges: RequestChallenges = body.challenges;
const recentPromise: Promise<SessionCookie> = sessionId
? getSessionCookieById(sessionId).catch((error) => {
@@ -93,16 +97,6 @@ export async function PUT(request: NextRequest) {
return recentPromise
.then((recent) => {
const checks: Checks = {};
if (password) {
checks.password = {
password,
};
}
if (webAuthN) {
checks.webAuthN = webAuthN;
}
return setSessionAndUpdateCookie(
recent,
checks,
@@ -111,7 +105,7 @@ export async function PUT(request: NextRequest) {
).then(async (session) => {
// if password, check if user has MFA methods
let authFactors;
if (password && session.factors?.user?.id) {
if (checks.password && session.factors?.user?.id) {
const response = await listHumanAuthFactors(
server,
session.factors?.user?.id

View File

@@ -1,35 +1,44 @@
"use client";
import { useEffect, useState } from "react";
import { Button, ButtonVariants } from "./Button";
import { TextInput } from "./Input";
import { useForm } from "react-hook-form";
import { useEffect, useRef, useState } from "react";
import { useRouter } from "next/navigation";
import { Spinner } from "./Spinner";
import { coerceToArrayBuffer, coerceToBase64Url } from "#/utils/base64";
import { Button, ButtonVariants } from "./Button";
import Alert from "./Alert";
import { Spinner } from "./Spinner";
import { Checks } from "@zitadel/server";
import { useForm } from "react-hook-form";
import { TextInput } from "./Input";
// either loginName or sessionId must be provided
type Props = {
loginName?: string;
sessionId?: string;
authRequestId?: string;
organization?: string;
method?: string;
code?: string;
};
type Inputs = {
code: string;
};
type Props = {
loginName: string | undefined;
sessionId: string | undefined;
code: string | undefined;
method: string;
authRequestId?: string;
organization?: string;
submit: boolean;
};
export default function TOTPForm({
export default function LoginOTP({
loginName,
code,
method,
sessionId,
authRequestId,
organization,
submit,
method,
code,
}: Props) {
const [error, setError] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false);
const router = useRouter();
const initialized = useRef(false);
const { register, handleSubmit, formState } = useForm<Inputs>({
mode: "onBlur",
defaultValues: {
@@ -37,10 +46,93 @@ export default function TOTPForm({
},
});
const router = useRouter();
useEffect(() => {
if (!initialized.current) {
initialized.current = true;
setLoading(true);
updateSessionForOTPChallenge();
// .then((response) => {
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string>("");
// setLoading(false);
// })
// .catch((error) => {
// setError(error);
// setLoading(false);
// });
}
}, []);
async function updateSessionForOTPChallenge() {
setLoading(true);
const res = await fetch("/api/session", {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
loginName,
sessionId,
organization,
challenges:
method === "email"
? {
otpEmail: true,
}
: method === "sms"
? { otpSms: true }
: {},
authRequestId,
}),
});
setLoading(false);
if (!res.ok) {
const error = await res.json();
throw error.details.details;
}
return res.json();
}
// async function submitLogin(inputs: Inputs) {
// setLoading(true);
// const checks: Checks = {};
// if (method === "email") {
// checks.otpEmail = {
// code: inputs.code,
// };
// }
// if (method === "sms") {
// checks.otpSms = {
// code: inputs.code,
// };
// }
// const res = await fetch("/api/session", {
// method: "PUT",
// headers: {
// "Content-Type": "application/json",
// },
// body: JSON.stringify({
// loginName,
// sessionId,
// organization,
// authRequestId,
// checks,
// }),
// });
// const response = await res.json();
// setLoading(false);
// if (!res.ok) {
// setError(response.details);
// return Promise.reject(response.details);
// }
// return response;
// }
async function submitCode(values: Inputs, organization?: string) {
setLoading(true);
@@ -58,12 +150,29 @@ export default function TOTPForm({
body.authRequestId = authRequestId;
}
const res = await fetch("/api/otp/verify", {
const checks: Checks = {};
if (method === "sms") {
checks.otpSms = { code: values.code };
}
if (method === "email") {
checks.otpEmail = { code: values.code };
}
if (method === "time-based") {
checks.totp = { code: values.code };
}
const res = await fetch("/api/session", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(body),
body: JSON.stringify({
loginName,
sessionId,
organization,
checks,
authRequestId,
}),
});
setLoading(false);
@@ -112,13 +221,6 @@ export default function TOTPForm({
const { errors } = formState;
useEffect(() => {
if (submit && code) {
// When we navigate to this page, we always want to be redirected if submit is true and the parameters are valid.
setCodeAndContinue({ code }, organization);
}
}, []);
return (
<form className="w-full">
<div className="">

View File

@@ -6,6 +6,7 @@ import { coerceToArrayBuffer, coerceToBase64Url } from "#/utils/base64";
import { Button, ButtonVariants } from "./Button";
import Alert from "./Alert";
import { Spinner } from "./Spinner";
import { Checks } from "@zitadel/server";
// either loginName or sessionId must be provided
type Props = {
@@ -100,7 +101,9 @@ export default function LoginPasskey({
loginName,
sessionId,
organization,
webAuthN: { credentialAssertionData: data },
checks: {
webAuthN: { credentialAssertionData: data },
} as Checks,
authRequestId,
}),
});

View File

@@ -7,7 +7,7 @@ import { useForm } from "react-hook-form";
import { useRouter } from "next/navigation";
import { Spinner } from "./Spinner";
import Alert from "./Alert";
import { LoginSettings, AuthFactor } from "@zitadel/server";
import { LoginSettings, AuthFactor, Checks } from "@zitadel/server";
type Inputs = {
password: string;
@@ -52,7 +52,9 @@ export default function PasswordForm({
body: JSON.stringify({
loginName,
organization,
password: values.password,
checks: {
password: { password: values.password },
} as Checks,
authRequestId,
}),
});