mirror of
https://github.com/zitadel/zitadel.git
synced 2025-12-12 06:42:59 +00:00
register totp, login catch expired session
This commit is contained in:
@@ -17,7 +17,7 @@ export default async function Page({
|
|||||||
searchParams: Record<string | number | symbol, string | undefined>;
|
searchParams: Record<string | number | symbol, string | undefined>;
|
||||||
params: Record<string | number | symbol, string | undefined>;
|
params: Record<string | number | symbol, string | undefined>;
|
||||||
}) {
|
}) {
|
||||||
const { loginName, organization } = searchParams;
|
const { loginName, organization, sessionId, authRequestId } = searchParams;
|
||||||
const { method } = params;
|
const { method } = params;
|
||||||
|
|
||||||
const branding = await getBrandingSettings(server, organization);
|
const branding = await getBrandingSettings(server, organization);
|
||||||
@@ -67,6 +67,10 @@ export default async function Page({
|
|||||||
<TOTPRegister
|
<TOTPRegister
|
||||||
uri={totpResponse.uri as string}
|
uri={totpResponse.uri as string}
|
||||||
secret={totpResponse.secret as string}
|
secret={totpResponse.secret as string}
|
||||||
|
loginName={loginName}
|
||||||
|
sessionId={sessionId}
|
||||||
|
authRequestId={authRequestId}
|
||||||
|
organization={organization}
|
||||||
></TOTPRegister>
|
></TOTPRegister>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -86,7 +86,8 @@ export async function GET(request: NextRequest) {
|
|||||||
if (authRequestId) {
|
if (authRequestId) {
|
||||||
console.log(`Login with authRequest: ${authRequestId}`);
|
console.log(`Login with authRequest: ${authRequestId}`);
|
||||||
const { authRequest } = await getAuthRequest(server, { authRequestId });
|
const { authRequest } = await getAuthRequest(server, { authRequestId });
|
||||||
let organization;
|
|
||||||
|
let organization = "";
|
||||||
|
|
||||||
if (authRequest?.scope) {
|
if (authRequest?.scope) {
|
||||||
const orgScope = authRequest.scope.find((s: string) =>
|
const orgScope = authRequest.scope.find((s: string) =>
|
||||||
@@ -112,6 +113,18 @@ export async function GET(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const gotoAccounts = () => {
|
||||||
|
const accountsUrl = new URL("/accounts", request.url);
|
||||||
|
if (authRequest?.id) {
|
||||||
|
accountsUrl.searchParams.set("authRequestId", authRequest?.id);
|
||||||
|
}
|
||||||
|
if (organization) {
|
||||||
|
accountsUrl.searchParams.set("organization", organization);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.redirect(accountsUrl);
|
||||||
|
};
|
||||||
|
|
||||||
if (authRequest && authRequest.prompt.includes(Prompt.PROMPT_CREATE)) {
|
if (authRequest && authRequest.prompt.includes(Prompt.PROMPT_CREATE)) {
|
||||||
const registerUrl = new URL("/register", request.url);
|
const registerUrl = new URL("/register", request.url);
|
||||||
if (authRequest?.id) {
|
if (authRequest?.id) {
|
||||||
@@ -128,15 +141,7 @@ export async function GET(request: NextRequest) {
|
|||||||
if (authRequest && sessions.length) {
|
if (authRequest && sessions.length) {
|
||||||
// if some accounts are available for selection and select_account is set
|
// if some accounts are available for selection and select_account is set
|
||||||
if (authRequest.prompt.includes(Prompt.PROMPT_SELECT_ACCOUNT)) {
|
if (authRequest.prompt.includes(Prompt.PROMPT_SELECT_ACCOUNT)) {
|
||||||
const accountsUrl = new URL("/accounts", request.url);
|
gotoAccounts();
|
||||||
if (authRequest?.id) {
|
|
||||||
accountsUrl.searchParams.set("authRequestId", authRequest?.id);
|
|
||||||
}
|
|
||||||
if (organization) {
|
|
||||||
accountsUrl.searchParams.set("organization", organization);
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.redirect(accountsUrl);
|
|
||||||
} else if (authRequest.prompt.includes(Prompt.PROMPT_LOGIN)) {
|
} else if (authRequest.prompt.includes(Prompt.PROMPT_LOGIN)) {
|
||||||
// if prompt is login
|
// if prompt is login
|
||||||
const loginNameUrl = new URL("/loginname", request.url);
|
const loginNameUrl = new URL("/loginname", request.url);
|
||||||
@@ -200,22 +205,16 @@ export async function GET(request: NextRequest) {
|
|||||||
authRequestId,
|
authRequestId,
|
||||||
session,
|
session,
|
||||||
});
|
});
|
||||||
return NextResponse.redirect(callbackUrl);
|
if (callbackUrl) {
|
||||||
} else {
|
return NextResponse.redirect(callbackUrl);
|
||||||
const accountsUrl = new URL("/accounts", request.url);
|
} else {
|
||||||
accountsUrl.searchParams.set("authRequestId", authRequestId);
|
gotoAccounts();
|
||||||
if (organization) {
|
|
||||||
accountsUrl.searchParams.set("organization", organization);
|
|
||||||
}
|
}
|
||||||
return NextResponse.redirect(accountsUrl);
|
} else {
|
||||||
|
gotoAccounts();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const accountsUrl = new URL("/accounts", request.url);
|
gotoAccounts();
|
||||||
accountsUrl.searchParams.set("authRequestId", authRequestId);
|
|
||||||
if (organization) {
|
|
||||||
accountsUrl.searchParams.set("organization", organization);
|
|
||||||
}
|
|
||||||
return NextResponse.redirect(accountsUrl);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
24
apps/login/lib/server-actions.ts
Normal file
24
apps/login/lib/server-actions.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import { getMostRecentCookieWithLoginname } from "#/utils/cookies";
|
||||||
|
import { getSession, server, verifyTOTPRegistration } from "./zitadel";
|
||||||
|
|
||||||
|
export async function verifyTOTP(
|
||||||
|
code: string,
|
||||||
|
loginName?: string,
|
||||||
|
organization?: string
|
||||||
|
) {
|
||||||
|
return getMostRecentCookieWithLoginname(loginName, organization)
|
||||||
|
.then((recent) => {
|
||||||
|
return getSession(server, recent.id, recent.token).then((response) => {
|
||||||
|
return { session: response?.session, token: recent.token };
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(({ session, token }) => {
|
||||||
|
if (session?.factors?.user?.id) {
|
||||||
|
return verifyTOTPRegistration(code, session.factors.user.id, token);
|
||||||
|
} else {
|
||||||
|
throw Error("No user id found in session.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,4 +1,8 @@
|
|||||||
import { GetUserByIDResponse, RegisterTOTPResponse } from "@zitadel/server";
|
import {
|
||||||
|
GetUserByIDResponse,
|
||||||
|
RegisterTOTPResponse,
|
||||||
|
VerifyTOTPRegistrationResponse,
|
||||||
|
} from "@zitadel/server";
|
||||||
import {
|
import {
|
||||||
LegalAndSupportSettings,
|
LegalAndSupportSettings,
|
||||||
PasswordComplexitySettings,
|
PasswordComplexitySettings,
|
||||||
@@ -50,6 +54,8 @@ import {
|
|||||||
AddOTPSMSResponse,
|
AddOTPSMSResponse,
|
||||||
} from "@zitadel/server";
|
} from "@zitadel/server";
|
||||||
|
|
||||||
|
const SESSION_LIFETIME_S = 3000;
|
||||||
|
|
||||||
export const zitadelConfig: ZitadelServerOptions = {
|
export const zitadelConfig: ZitadelServerOptions = {
|
||||||
name: "zitadel login",
|
name: "zitadel login",
|
||||||
apiUrl: process.env.ZITADEL_API_URL ?? "",
|
apiUrl: process.env.ZITADEL_API_URL ?? "",
|
||||||
@@ -124,8 +130,6 @@ export async function registerTOTP(
|
|||||||
token: token,
|
token: token,
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log(token);
|
|
||||||
|
|
||||||
const sessionUser = initializeServer(authConfig);
|
const sessionUser = initializeServer(authConfig);
|
||||||
userService = user.getUser(sessionUser);
|
userService = user.getUser(sessionUser);
|
||||||
} else {
|
} else {
|
||||||
@@ -185,7 +189,7 @@ export async function createSessionFromChecks(
|
|||||||
checks: checks,
|
checks: checks,
|
||||||
challenges,
|
challenges,
|
||||||
lifetime: {
|
lifetime: {
|
||||||
seconds: 300,
|
seconds: SESSION_LIFETIME_S,
|
||||||
nanos: 0,
|
nanos: 0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -302,6 +306,27 @@ export async function addHumanUser(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function verifyTOTPRegistration(
|
||||||
|
code: string,
|
||||||
|
userId: string,
|
||||||
|
token?: string
|
||||||
|
): Promise<VerifyTOTPRegistrationResponse> {
|
||||||
|
let userService;
|
||||||
|
if (token) {
|
||||||
|
const authConfig: ZitadelServerOptions = {
|
||||||
|
name: "zitadel login",
|
||||||
|
apiUrl: process.env.ZITADEL_API_URL ?? "",
|
||||||
|
token: token,
|
||||||
|
};
|
||||||
|
|
||||||
|
const sessionUser = initializeServer(authConfig);
|
||||||
|
userService = user.getUser(sessionUser);
|
||||||
|
} else {
|
||||||
|
userService = user.getUser(server);
|
||||||
|
}
|
||||||
|
return userService.verifyTOTPRegistration({ code, userId }, {});
|
||||||
|
}
|
||||||
|
|
||||||
export async function getUserByID(
|
export async function getUserByID(
|
||||||
userId: string
|
userId: string
|
||||||
): Promise<GetUserByIDResponse> {
|
): Promise<GetUserByIDResponse> {
|
||||||
|
|||||||
@@ -40,10 +40,12 @@
|
|||||||
"@zitadel/react": "workspace:*",
|
"@zitadel/react": "workspace:*",
|
||||||
"@zitadel/server": "workspace:*",
|
"@zitadel/server": "workspace:*",
|
||||||
"clsx": "1.2.1",
|
"clsx": "1.2.1",
|
||||||
|
"copy-to-clipboard": "^3.3.3",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"next": "13.4.12",
|
"next": "13.4.12",
|
||||||
"next-themes": "^0.2.1",
|
"next-themes": "^0.2.1",
|
||||||
"nice-grpc": "2.0.1",
|
"nice-grpc": "2.0.1",
|
||||||
|
"qrcode.react": "^3.1.0",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-hook-form": "7.39.5",
|
"react-hook-form": "7.39.5",
|
||||||
|
|||||||
41
apps/login/ui/CopyToClipboard.tsx
Normal file
41
apps/login/ui/CopyToClipboard.tsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
ClipboardDocumentCheckIcon,
|
||||||
|
ClipboardIcon,
|
||||||
|
} from "@heroicons/react/20/solid";
|
||||||
|
import copy from "copy-to-clipboard";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function CopyToClipboard({ value }: Props) {
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (copied) {
|
||||||
|
copy(value);
|
||||||
|
const to = setTimeout(setCopied, 1000, false);
|
||||||
|
return () => clearTimeout(to);
|
||||||
|
}
|
||||||
|
}, [copied]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-row items-center px-2">
|
||||||
|
<button
|
||||||
|
id="tooltip-ctc"
|
||||||
|
type="button"
|
||||||
|
className=" text-primary-light-500 dark:text-primary-dark-500"
|
||||||
|
onClick={() => setCopied(true)}
|
||||||
|
>
|
||||||
|
{!copied ? (
|
||||||
|
<ClipboardIcon className="h-5 w-5" />
|
||||||
|
) : (
|
||||||
|
<ClipboardDocumentCheckIcon className="h-5 w-5" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -95,47 +95,6 @@ export default function LoginOTP({
|
|||||||
return res.json();
|
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) {
|
async function submitCode(values: Inputs, organization?: string) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,128 @@
|
|||||||
import { RegisterTOTPResponse } from "@zitadel/server";
|
"use client";
|
||||||
|
import { QRCodeSVG } from "qrcode.react";
|
||||||
|
import Alert, { AlertType } from "./Alert";
|
||||||
|
import Link from "next/link";
|
||||||
|
import CopyToClipboard from "./CopyToClipboard";
|
||||||
|
import { TextInput } from "./Input";
|
||||||
|
import { Button, ButtonVariants } from "./Button";
|
||||||
|
import { Spinner } from "./Spinner";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { verifyTOTP } from "#/lib/server-actions";
|
||||||
|
import { login } from "@zitadel/server";
|
||||||
|
|
||||||
|
type Inputs = {
|
||||||
|
code: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
uri: string;
|
||||||
|
secret: string;
|
||||||
|
loginName?: string;
|
||||||
|
sessionId?: string;
|
||||||
|
authRequestId?: string;
|
||||||
|
organization?: string;
|
||||||
|
};
|
||||||
export default function TOTPRegister({
|
export default function TOTPRegister({
|
||||||
uri,
|
uri,
|
||||||
secret,
|
secret,
|
||||||
}: {
|
loginName,
|
||||||
uri: string;
|
sessionId,
|
||||||
secret: string;
|
authRequestId,
|
||||||
}) {
|
organization,
|
||||||
return <div>{uri}</div>;
|
}: Props) {
|
||||||
|
const [error, setError] = useState<string>("");
|
||||||
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const { register, handleSubmit, formState } = useForm<Inputs>({
|
||||||
|
mode: "onBlur",
|
||||||
|
defaultValues: {
|
||||||
|
code: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
async function continueWithCode(values: Inputs) {
|
||||||
|
return verifyTOTP(values.code, loginName, organization)
|
||||||
|
.then((response) => {
|
||||||
|
if (authRequestId && sessionId) {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
sessionId: sessionId,
|
||||||
|
authRequest: authRequestId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (organization) {
|
||||||
|
params.append("organization", organization);
|
||||||
|
}
|
||||||
|
|
||||||
|
return router.push(`/login?` + params);
|
||||||
|
} else if (loginName) {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
loginName,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (authRequestId) {
|
||||||
|
params.append("authRequestId", authRequestId);
|
||||||
|
}
|
||||||
|
if (organization) {
|
||||||
|
params.append("organization", organization);
|
||||||
|
}
|
||||||
|
|
||||||
|
return router.push(`/signedin?` + params);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
setError(e.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center ">
|
||||||
|
{uri && (
|
||||||
|
<>
|
||||||
|
<QRCodeSVG
|
||||||
|
className="rounded-md w-40 h-40 p-2 bg-white my-4"
|
||||||
|
value={uri}
|
||||||
|
/>
|
||||||
|
<div className="mb-4 w-96 flex text-sm my-2 border rounded-lg px-4 py-2 pr-2 border-divider-light dark:border-divider-dark">
|
||||||
|
<Link href={uri} target="_blank" className="flex-1 overflow-x-auto">
|
||||||
|
{uri}
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<CopyToClipboard value={uri}></CopyToClipboard>
|
||||||
|
</div>
|
||||||
|
<form className="w-full">
|
||||||
|
<div className="">
|
||||||
|
<TextInput
|
||||||
|
type="text"
|
||||||
|
{...register("code", { required: "This field is required" })}
|
||||||
|
label="Code"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="py-4">
|
||||||
|
<Alert>{error}</Alert>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mt-8 flex w-full flex-row items-center">
|
||||||
|
<span className="flex-grow"></span>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className="self-end"
|
||||||
|
variant={ButtonVariants.Primary}
|
||||||
|
disabled={loading || !formState.isValid}
|
||||||
|
onClick={handleSubmit(continueWithCode)}
|
||||||
|
>
|
||||||
|
{loading && <Spinner className="h-5 w-5 mr-2" />}
|
||||||
|
continue
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,6 +92,8 @@ export {
|
|||||||
AddOTPSMSRequest,
|
AddOTPSMSRequest,
|
||||||
RegisterTOTPRequest,
|
RegisterTOTPRequest,
|
||||||
RegisterTOTPResponse,
|
RegisterTOTPResponse,
|
||||||
|
VerifyTOTPRegistrationRequest,
|
||||||
|
VerifyTOTPRegistrationResponse,
|
||||||
} from "./proto/server/zitadel/user/v2beta/user_service";
|
} from "./proto/server/zitadel/user/v2beta/user_service";
|
||||||
|
|
||||||
export { AuthFactor } from "./proto/server/zitadel/user";
|
export { AuthFactor } from "./proto/server/zitadel/user";
|
||||||
|
|||||||
8331
pnpm-lock.yaml
generated
8331
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user