From 20a589cea2209e810b3222f607124eda1802a4ab Mon Sep 17 00:00:00 2001 From: peintnermax Date: Wed, 17 Apr 2024 14:14:25 +0200 Subject: [PATCH] combined otp form --- apps/login/app/(login)/otp/[method]/page.tsx | 12 +- apps/login/app/api/session/route.ts | 24 +-- apps/login/ui/{TOTPForm.tsx => LoginOTP.tsx} | 164 +++++++++++++++---- apps/login/ui/LoginPasskey.tsx | 5 +- apps/login/ui/PasswordForm.tsx | 6 +- 5 files changed, 154 insertions(+), 57 deletions(-) rename apps/login/ui/{TOTPForm.tsx => LoginOTP.tsx} (54%) diff --git a/apps/login/app/(login)/otp/[method]/page.tsx b/apps/login/app/(login)/otp/[method]/page.tsx index 4a0fc343b1a..6d87d070f26 100644 --- a/apps/login/app/(login)/otp/[method]/page.tsx +++ b/apps/login/app/(login)/otp/[method]/page.tsx @@ -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) ? ( - + method={method} + > ) : ( = 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 diff --git a/apps/login/ui/TOTPForm.tsx b/apps/login/ui/LoginOTP.tsx similarity index 54% rename from apps/login/ui/TOTPForm.tsx rename to apps/login/ui/LoginOTP.tsx index 365c3a3bd9d..3264f8e0409 100644 --- a/apps/login/ui/TOTPForm.tsx +++ b/apps/login/ui/LoginOTP.tsx @@ -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(""); + const [loading, setLoading] = useState(false); + + const router = useRouter(); + + const initialized = useRef(false); + const { register, handleSubmit, formState } = useForm({ 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(false); - const [error, setError] = useState(""); + // 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 (
diff --git a/apps/login/ui/LoginPasskey.tsx b/apps/login/ui/LoginPasskey.tsx index 7ea0524a6c8..fb8a024214b 100644 --- a/apps/login/ui/LoginPasskey.tsx +++ b/apps/login/ui/LoginPasskey.tsx @@ -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, }), }); diff --git a/apps/login/ui/PasswordForm.tsx b/apps/login/ui/PasswordForm.tsx index d528c814079..2d3ccad0d5d 100644 --- a/apps/login/ui/PasswordForm.tsx +++ b/apps/login/ui/PasswordForm.tsx @@ -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, }), });