otp methods

This commit is contained in:
peintnermax
2024-04-16 15:33:14 +02:00
parent 80e2f3ee71
commit 57db64f6bb
16 changed files with 398 additions and 144 deletions

View File

@@ -1,3 +1,25 @@
export default function Page() {
return <div className="flex flex-col items-center space-y-4">mfa</div>;
import { getBrandingSettings, server } from "#/lib/zitadel";
import DynamicTheme from "#/ui/DynamicTheme";
export default async function Page({
searchParams,
}: {
searchParams: Record<string | number | symbol, string | undefined>;
}) {
const { loginName, authRequestId, sessionId, organization, code, submit } =
searchParams;
const branding = await getBrandingSettings(server, organization);
return (
<DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4">
<h1>Verify 2-Factor</h1>
<p className="ztdl-p">Choose one of the following second factors.</p>
<div></div>
</div>
</DynamicTheme>
);
}

View File

@@ -1,34 +1,36 @@
"use client";
import { getBrandingSettings, server } from "#/lib/zitadel";
import { Button, ButtonVariants } from "#/ui/Button";
import DynamicTheme from "#/ui/DynamicTheme";
import { TextInput } from "#/ui/Input";
import UserAvatar from "#/ui/UserAvatar";
import { useRouter } from "next/navigation";
export default function Page() {
const router = useRouter();
export default async function Page({
searchParams,
}: {
searchParams: Record<string | number | symbol, string | undefined>;
}) {
const { loginName, authRequestId, sessionId, organization, code, submit } =
searchParams;
const branding = await getBrandingSettings(server, organization);
return (
<div className="flex flex-col items-center space-y-4">
<h1>Second Factor</h1>
<p className="ztdl-p mb-6 block">Please select a second factor.</p>
<DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4">
<h1>Verify 2-Factor</h1>
<UserAvatar
showDropdown
displayName="Max Peintner"
loginName="max@zitadel.com"
></UserAvatar>
<div className="w-full">
<TextInput type="password" label="Password" />
<p className="ztdl-p">Choose one of the following second factors.</p>
<UserAvatar
showDropdown
displayName="Max Peintner"
loginName="max@zitadel.com"
></UserAvatar>
<div className="w-full">
<TextInput type="password" label="Password" />
</div>
</div>
<div className="flex w-full flex-row items-center justify-between">
<Button
onClick={() => router.back()}
variant={ButtonVariants.Secondary}
>
back
</Button>
<Button variant={ButtonVariants.Primary}>continue</Button>
</div>
</div>
</DynamicTheme>
);
}

View File

@@ -0,0 +1,61 @@
import { getBrandingSettings, getLoginSettings, server } from "#/lib/zitadel";
import DynamicTheme from "#/ui/DynamicTheme";
import TOTPForm from "#/ui/TOTPForm";
import VerifyU2F from "#/ui/VerifyU2F";
export default async function Page({
searchParams,
params,
}: {
searchParams: Record<string | number | symbol, string | undefined>;
params: Record<string | number | symbol, string | undefined>;
}) {
const { loginName, authRequestId, sessionId, organization, code, submit } =
searchParams;
const { method } = params;
console.log(method);
const branding = await getBrandingSettings(server, organization);
return (
<DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4">
<h1>Verify 2-Factor</h1>
{method === "time-based" && (
<p className="ztdl-p">Enter the code from your authenticator app.</p>
)}
{method === "sms" && (
<p className="ztdl-p">Enter the code you got on your phone.</p>
)}
{method === "email" && (
<p className="ztdl-p">Enter the code you got via your email.</p>
)}
{method === "u2f" && (
<p className="ztdl-p">Verify your account with your device.</p>
)}
{method && ["time-based", "sms", "email"].includes(method) ? (
<TOTPForm
loginName={loginName}
sessionId={sessionId}
code={code}
method={method}
authRequestId={authRequestId}
organization={organization}
submit={submit === "true"}
/>
) : (
<VerifyU2F
loginName={loginName}
sessionId={sessionId}
authRequestId={authRequestId}
organization={organization}
submit={submit === "true"}
></VerifyU2F>
)}
</div>
</DynamicTheme>
);
}

View File

@@ -0,0 +1,76 @@
import {
addOTPEmail,
addOTPSMS,
getBrandingSettings,
getSession,
registerTOTP,
server,
} from "#/lib/zitadel";
import DynamicTheme from "#/ui/DynamicTheme";
import TOTPRegister from "#/ui/TOTPRegister";
import { getMostRecentCookieWithLoginname } from "#/utils/cookies";
export default async function Page({
searchParams,
params,
}: {
searchParams: Record<string | number | symbol, string | undefined>;
params: Record<string | number | symbol, string | undefined>;
}) {
const { loginName, organization } = searchParams;
const { method } = params;
const branding = await getBrandingSettings(server, organization);
const totpResponse = await loadSession(loginName, organization).then(
({ session, token }) => {
if (session && session.factors?.user?.id) {
if (method === "time-based") {
return registerTOTP(session.factors.user.id, token);
} else if (method === "sms") {
return addOTPSMS(session.factors.user.id);
} else if (method === "email") {
return addOTPEmail(session.factors.user.id);
} else {
throw new Error("Invalid method");
}
} else {
throw new Error("No session found");
}
}
);
async function loadSession(loginName?: string, organization?: string) {
const recent = await getMostRecentCookieWithLoginname(
loginName,
organization
);
return getSession(server, recent.id, recent.token).then((response) => {
return { session: response?.session, token: recent.token };
});
}
return (
<DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4">
<h1>Register TOTP</h1>
<p className="ztdl-p">
Scan the QR Code or navigate to the URL manually.
</p>
<div>
{/* {auth && <div>{auth.to}</div>} */}
{totpResponse &&
"uri" in totpResponse &&
"secret" in totpResponse && (
<TOTPRegister
uri={totpResponse.uri as string}
secret={totpResponse.secret as string}
></TOTPRegister>
)}
</div>
</div>
</DynamicTheme>
);
}

View File

@@ -1,32 +0,0 @@
import { getBrandingSettings, getLoginSettings, server } from "#/lib/zitadel";
import DynamicTheme from "#/ui/DynamicTheme";
import TOTPForm from "#/ui/TOTPForm";
export default async function Page({
searchParams,
}: {
searchParams: Record<string | number | symbol, string | undefined>;
}) {
const { loginName, authRequestId, sessionId, organization, code, submit } =
searchParams;
const branding = await getBrandingSettings(server, organization);
return (
<DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4">
<h1>Verify 2-Factor</h1>
<p className="ztdl-p">Enter the code from your authenticator app. </p>
<TOTPForm
loginName={loginName}
sessionId={sessionId}
code={code}
authRequestId={authRequestId}
organization={organization}
submit={submit === "true"}
/>
</div>
</DynamicTheme>
);
}

View File

@@ -1,46 +0,0 @@
import {
addMyAuthFactorOTP,
getBrandingSettings,
getLoginSettings,
getSession,
server,
} from "#/lib/zitadel";
import DynamicTheme from "#/ui/DynamicTheme";
import TOTPRegister from "#/ui/TOTPRegister";
import { getMostRecentCookieWithLoginname } from "#/utils/cookies";
export default async function Page({
searchParams,
}: {
searchParams: Record<string | number | symbol, string | undefined>;
}) {
const { loginName, organization } = searchParams;
const branding = await getBrandingSettings(server, organization);
const auth = await getMostRecentCookieWithLoginname(
loginName,
organization
).then((cookie) => {
if (cookie) {
return addMyAuthFactorOTP(cookie.token);
} else {
throw new Error("No cookie found");
}
});
return (
<DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4">
<h1>Register TOTP</h1>
<p className="ztdl-p">
Scan the QR Code or navigate to the URL manually.
</p>
<div>
{auth && <div>{auth.url}</div>}
<TOTPRegister></TOTPRegister>
</div>
</div>
</DynamicTheme>
);
}

View File

@@ -5,13 +5,15 @@ import {
getSessionCookieByLoginName,
} from "#/utils/cookies";
import { setSessionAndUpdateCookie } from "#/utils/session";
import { Checks } from "@zitadel/server";
import { NextRequest, NextResponse, userAgent } from "next/server";
export async function POST(request: NextRequest) {
const body = await request.json();
if (body) {
const { loginName, sessionId, organization, authRequestId, code } = body;
const { loginName, sessionId, organization, authRequestId, code, method } =
body;
const recentPromise: Promise<SessionCookie> = sessionId
? getSessionCookieById(sessionId).catch((error) => {
@@ -27,12 +29,26 @@ export async function POST(request: NextRequest) {
return recentPromise
.then((recent) => {
const checks: Checks = {};
if (method === "time-based") {
checks.totp = {
code,
};
} else if (method === "sms") {
checks.otpSms = {
code,
};
} else if (method === "email") {
checks.otpEmail = {
code,
};
}
return setSessionAndUpdateCookie(
recent,
checks,
undefined,
undefined,
undefined,
code,
authRequestId
).then((session) => {
return NextResponse.json({

View File

@@ -0,0 +1,70 @@
import {
SessionCookie,
getMostRecentSessionCookie,
getSessionCookieById,
getSessionCookieByLoginName,
} from "#/utils/cookies";
import { setSessionAndUpdateCookie } from "#/utils/session";
import { Checks } from "@zitadel/server";
import { NextRequest, NextResponse, userAgent } from "next/server";
export async function POST(request: NextRequest) {
const body = await request.json();
if (body) {
const { loginName, sessionId, organization, authRequestId, code, method } =
body;
const recentPromise: Promise<SessionCookie> = sessionId
? getSessionCookieById(sessionId).catch((error) => {
return Promise.reject(error);
})
: loginName
? getSessionCookieByLoginName(loginName, organization).catch((error) => {
return Promise.reject(error);
})
: getMostRecentSessionCookie().catch((error) => {
return Promise.reject(error);
});
return recentPromise
.then((recent) => {
const checks: Checks = {};
if (method === "time-based") {
checks.totp = {
code,
};
} else if (method === "sms") {
checks.otpSms = {
code,
};
} else if (method === "email") {
checks.otpEmail = {
code,
};
}
return setSessionAndUpdateCookie(
recent,
checks,
undefined,
authRequestId
).then((session) => {
return NextResponse.json({
sessionId: session.id,
factors: session.factors,
challenges: session.challenges,
});
});
})
.catch((error) => {
return NextResponse.json({ details: error }, { status: 500 });
});
} else {
return NextResponse.json(
{ details: "Request body is missing" },
{ status: 400 }
);
}
}

View File

@@ -11,7 +11,7 @@ import {
createSessionForIdpAndUpdateCookie,
setSessionAndUpdateCookie,
} from "#/utils/session";
import { RequestChallenges } from "@zitadel/server";
import { Checks, RequestChallenges } from "@zitadel/server";
import { NextRequest, NextResponse } from "next/server";
export async function POST(request: NextRequest) {
@@ -93,12 +93,20 @@ 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,
password,
webAuthN,
checks,
challenges,
undefined,
authRequestId
).then(async (session) => {
// if password, check if user has MFA methods

View File

@@ -1,3 +1,4 @@
import { RegisterTOTPResponse } from "@zitadel/server";
import {
LegalAndSupportSettings,
PasswordComplexitySettings,
@@ -45,6 +46,8 @@ import {
TextQueryMethod,
ListHumanAuthFactorsResponse,
AddHumanUserRequest,
AddOTPEmailResponse,
AddOTPSMSResponse,
} from "@zitadel/server";
export const zitadelConfig: ZitadelServerOptions = {
@@ -90,19 +93,45 @@ export async function verifyMyAuthFactorOTP(
return authService.verifyMyAuthFactorOTP({ code }, {});
}
export async function addMyAuthFactorOTP(
token: string
): Promise<AddMyAuthFactorOTPResponse> {
const zitadelConfig: ZitadelServerOptions = {
name: "zitadel login",
apiUrl: process.env.ZITADEL_API_URL ?? "",
token: token,
};
export async function addOTPEmail(
userId: string
): Promise<AddOTPEmailResponse | undefined> {
const userService = user.getUser(server);
return userService.addOTPEmail(
{
userId,
},
{}
);
}
const server: ZitadelServer = initializeServer(zitadelConfig);
export async function addOTPSMS(
userId: string
): Promise<AddOTPSMSResponse | undefined> {
const userService = user.getUser(server);
return userService.addOTPSMS({ userId }, {});
}
const authService = auth.getAuth(server);
return authService.addMyAuthFactorOTP({}, {});
export async function registerTOTP(
userId: string,
token?: string
): Promise<RegisterTOTPResponse | undefined> {
let userService;
if (token) {
const authConfig: ZitadelServerOptions = {
name: "zitadel login",
apiUrl: process.env.ZITADEL_API_URL ?? "",
token: token,
};
console.log(token);
const sessionUser = initializeServer(authConfig);
userService = user.getUser(sessionUser);
} else {
userService = user.getUser(server);
}
return userService.registerTOTP({ userId }, {});
}
export async function getGeneralSettings(

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 } from "@zitadel/server";
import { LoginSettings, AuthFactor } from "@zitadel/server";
type Inputs = {
password: string;
@@ -68,11 +68,11 @@ export default function PasswordForm({
}
function submitPasswordAndContinue(value: Inputs): Promise<boolean | void> {
return submitPassword(value).then((resp: any) => {
return submitPassword(value).then((resp) => {
// if user has mfa -> /totp
// if mfa is forced -> /mfa/set
// if no passwordless -> /passkey/add
if (resp.authFactors?.length >= 1) {
if (resp.authFactors?.length == 1) {
const params = new URLSearchParams({
loginName: resp.factors.user.loginName,
});
@@ -85,7 +85,32 @@ export default function PasswordForm({
params.append("organization", organization);
}
return router.push(`/mfa/set?` + params);
let method;
if ((resp.authFactors as AuthFactor[])[0].otp) {
method = "time-based";
} else if ((resp.authFactors as AuthFactor[])[0].otpSms) {
method = "sms";
} else if ((resp.authFactors as AuthFactor[])[0].otpEmail) {
method = "email";
} else if ((resp.authFactors as AuthFactor[])[0].u2f) {
method = "u2f";
}
return router.push(`/otp/${method}?` + params);
} else if (resp.authFactors?.length >= 1) {
const params = new URLSearchParams({
loginName: resp.factors.user.loginName,
});
if (authRequestId) {
params.append("authRequest", authRequestId);
}
if (organization) {
params.append("organization", organization);
}
return router.push(`/mfa?` + params);
} else if (
resp.factors &&
!resp.factors.passwordless && // if session was not verified with a passkey

View File

@@ -16,6 +16,7 @@ type Props = {
loginName: string | undefined;
sessionId: string | undefined;
code: string | undefined;
method: string;
authRequestId?: string;
organization?: string;
submit: boolean;
@@ -24,6 +25,7 @@ type Props = {
export default function TOTPForm({
loginName,
code,
method,
authRequestId,
organization,
submit,
@@ -45,6 +47,7 @@ export default function TOTPForm({
let body: any = {
code: values.code,
method,
};
if (organization) {
@@ -55,7 +58,7 @@ export default function TOTPForm({
body.authRequestId = authRequestId;
}
const res = await fetch("/api/totp/verify", {
const res = await fetch("/api/otp/verify", {
method: "POST",
headers: {
"Content-Type": "application/json",

View File

@@ -1,3 +1,11 @@
export default function TOTPRegister() {
return <div></div>;
import { RegisterTOTPResponse } from "@zitadel/server";
export default function TOTPRegister({
uri,
secret,
}: {
uri: string;
secret: string;
}) {
return <div>{uri}</div>;
}

View File

@@ -0,0 +1,16 @@
type Props = {
loginName: string | undefined;
sessionId: string | undefined;
authRequestId?: string;
organization?: string;
submit: boolean;
};
export default function VerifyU2F({
loginName,
authRequestId,
organization,
submit,
}: Props) {
return <div>Verify U2F</div>;
}

View File

@@ -188,24 +188,10 @@ export type SessionWithChallenges = Session & {
export async function setSessionAndUpdateCookie(
recentCookie: SessionCookie,
password: string | undefined,
webAuthN: { credentialAssertionData: any } | undefined,
checks: Checks,
challenges: RequestChallenges | undefined,
totpCode: string | undefined,
authRequestId: string | undefined
): Promise<SessionWithChallenges> {
const checks: Checks = {};
if (password) {
checks.password = { password };
}
if (webAuthN) {
checks.webAuthN = webAuthN;
}
if (totpCode) {
checks.totp = { code: totpCode };
}
return setSession(
server,
recentCookie.id,

View File

@@ -86,13 +86,23 @@ export {
RetrieveIdentityProviderIntentResponse,
ListUsersRequest,
ListUsersResponse,
AddOTPEmailResponse,
AddOTPEmailRequest,
AddOTPSMSResponse,
AddOTPSMSRequest,
RegisterTOTPRequest,
RegisterTOTPResponse,
} from "./proto/server/zitadel/user/v2beta/user_service";
export { AuthFactor } from "./proto/server/zitadel/user";
export {
SetHumanPasswordResponse,
SetHumanPasswordRequest,
GetOrgByDomainGlobalResponse,
ListHumanAuthFactorsResponse,
} from "./proto/server/zitadel/management";
export * from "./proto/server/zitadel/idp";
export { type LegalAndSupportSettings } from "./proto/server/zitadel/settings/v2beta/legal_settings";
export { type PasswordComplexitySettings } from "./proto/server/zitadel/settings/v2beta/password_settings";