From f3300cdbb49f45851c31adce96a350b5c607b0c5 Mon Sep 17 00:00:00 2001 From: peintnermax Date: Tue, 3 Sep 2024 10:24:05 +0200 Subject: [PATCH] session actions --- apps/login/src/app/api/session/route.ts | 196 ------------------------ apps/login/src/lib/server/loginname.ts | 6 +- apps/login/src/lib/server/session.ts | 34 +++- apps/login/src/ui/IdpSignin.tsx | 73 ++++----- apps/login/src/ui/LoginPasskey.tsx | 106 ++++++------- apps/login/src/ui/RegisterPasskey.tsx | 2 - apps/login/src/ui/SessionItem.tsx | 25 ++- apps/login/src/utils/session.ts | 6 +- 8 files changed, 117 insertions(+), 331 deletions(-) delete mode 100644 apps/login/src/app/api/session/route.ts diff --git a/apps/login/src/app/api/session/route.ts b/apps/login/src/app/api/session/route.ts deleted file mode 100644 index 4c9e7d4a449..00000000000 --- a/apps/login/src/app/api/session/route.ts +++ /dev/null @@ -1,196 +0,0 @@ -import { - deleteSession, - getSession, - getUserByID, - listAuthenticationMethodTypes, -} from "@/lib/zitadel"; -import { - getMostRecentSessionCookie, - getSessionCookieById, - getSessionCookieByLoginName, - removeSessionFromCookie, -} from "@zitadel/next"; -import { - createSessionAndUpdateCookie, - createSessionForIdpAndUpdateCookie, - setSessionAndUpdateCookie, -} from "@/utils/session"; -import { toJson } from "@zitadel/client"; -import { NextRequest, NextResponse } from "next/server"; -import { SessionSchema } from "@zitadel/proto/zitadel/session/v2/session_pb"; - -export async function POST(request: NextRequest) { - const body = await request.json(); - if (body) { - const { - userId, - idpIntent, - loginName, - password, - organization, - authRequestId, - } = body; - - if (userId && idpIntent) { - return createSessionForIdpAndUpdateCookie( - userId, - idpIntent, - organization, - authRequestId, - ).then((session) => { - return NextResponse.json(toJson(SessionSchema, session)); - }); - } else { - return createSessionAndUpdateCookie( - loginName, - password, - undefined, - organization, - authRequestId, - ).then((session) => { - return NextResponse.json(toJson(SessionSchema, session)); - }); - } - } else { - return NextResponse.json( - { details: "Session could not be created" }, - { status: 500 }, - ); - } -} - -/** - * - * @param request password for the most recent session - * @returns the updated most recent Session with the added password - */ -export async function PUT(request: NextRequest) { - const body = await request.json(); - - if (body) { - const { - loginName, - sessionId, - organization, - checks, - authRequestId, - challenges, - } = body; - - const recentPromise = 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); - }); - - const domain: string = request.nextUrl.hostname; - - if (challenges && challenges.webAuthN && !challenges.webAuthN.domain) { - challenges.webAuthN.domain = domain; - } - - return recentPromise - .then(async (recent) => { - if ( - challenges && - (challenges.otpEmail === "" || challenges.otpSms === "") - ) { - const sessionResponse = await getSession(recent.id, recent.token); - - if (sessionResponse && sessionResponse.session?.factors?.user?.id) { - const userResponse = await getUserByID( - sessionResponse.session.factors.user.id, - ); - const humanUser = - userResponse.user?.type.case === "human" - ? userResponse.user?.type.value - : undefined; - - if (challenges.otpEmail === "" && humanUser?.email?.email) { - challenges.otpEmail = humanUser?.email?.email; - } - - if (challenges.otpSms === "" && humanUser?.phone?.phone) { - challenges.otpSms = humanUser?.phone?.phone; - } - } - } - - return setSessionAndUpdateCookie( - recent, - checks, - challenges, - authRequestId, - ).then(async (session) => { - // if password, check if user has MFA methods - let authMethods; - if (checks && checks.password && session.factors?.user?.id) { - const response = await listAuthenticationMethodTypes( - session.factors?.user?.id, - ); - if (response.authMethodTypes && response.authMethodTypes.length) { - authMethods = response.authMethodTypes; - } - } - - return NextResponse.json({ - sessionId: session.id, - factors: session.factors, - challenges: session.challenges, - authMethods, - }); - }); - }) - .catch((error) => { - console.error(error); - return NextResponse.json({ details: error }, { status: 500 }); - }); - } else { - return NextResponse.json( - { details: "Request body is missing" }, - { status: 400 }, - ); - } -} - -/** - * - * @param request id of the session to be deleted - */ -export async function DELETE(request: NextRequest) { - const { searchParams } = new URL(request.url); - const sessionId = searchParams.get("id"); - if (sessionId) { - const session = await getSessionCookieById({ sessionId }); - - return deleteSession(session.id, session.token) - .then(() => { - return removeSessionFromCookie(session) - .then(() => { - return NextResponse.json({}); - }) - .catch((error) => { - return NextResponse.json( - { details: "could not set cookie" }, - { status: 500 }, - ); - }); - }) - .catch((error) => { - return NextResponse.json( - { details: "could not delete session" }, - { status: 500 }, - ); - }); - } else { - return NextResponse.error(); - } -} diff --git a/apps/login/src/lib/server/loginname.ts b/apps/login/src/lib/server/loginname.ts index 0e2699d29dd..933bafcb246 100644 --- a/apps/login/src/lib/server/loginname.ts +++ b/apps/login/src/lib/server/loginname.ts @@ -18,8 +18,6 @@ export type SendLoginnameCommand = { organization?: string; }; -export const UserNotFound = Error("Could not find user"); - export async function sendLoginname(options: SendLoginnameCommand) { const { loginName, authRequestId, organization } = options; const users = await listUsers({ @@ -94,7 +92,7 @@ export async function sendLoginname(options: SendLoginnameCommand) { } }); } else { - throw UserNotFound; + throw Error("Could not find user"); } } else if ( loginSettings?.allowRegister && @@ -116,5 +114,5 @@ export async function sendLoginname(options: SendLoginnameCommand) { return redirect(registerUrl.toString()); } - throw UserNotFound; + throw Error("Could not find user"); } diff --git a/apps/login/src/lib/server/session.ts b/apps/login/src/lib/server/session.ts index f226b655480..b07e7693186 100644 --- a/apps/login/src/lib/server/session.ts +++ b/apps/login/src/lib/server/session.ts @@ -29,12 +29,12 @@ type CreateNewSessionCommand = { userId: string; idpIntent: { idpIntentId: string; - idpIntentType: string; + idpIntentToken: string; }; - loginName: string; - password: string; - organization: string; - authRequestId: string; + loginName?: string; + password?: string; + organization?: string; + authRequestId?: string; }; export async function createNewSession(options: CreateNewSessionCommand) { @@ -54,7 +54,7 @@ export async function createNewSession(options: CreateNewSessionCommand) { organization, authRequestId, ); - } else { + } else if (loginName) { return createSessionAndUpdateCookie( loginName, password, @@ -62,6 +62,8 @@ export async function createNewSession(options: CreateNewSessionCommand) { organization, authRequestId, ); + } else { + throw new Error("No userId or loginName provided"); } } @@ -69,7 +71,7 @@ export type UpdateSessionCommand = { loginName?: string; sessionId?: string; organization?: string; - checks: Checks; + checks?: Checks; authRequestId?: string; challenges?: RequestChallenges; }; @@ -178,3 +180,21 @@ export async function clearSession(options: ClearSessionOptions) { return removeSessionFromCookie(session); } } + +type CleanupSessionCommand = { + sessionId: string; +}; +export async function cleanupSession({ sessionId }: CleanupSessionCommand) { + const sessionCookie = await getSessionCookieById({ sessionId }); + + const deleteResponse = await deleteSession( + sessionCookie.id, + sessionCookie.token, + ); + + if (!deleteResponse) { + throw new Error("Could not delete session"); + } + + return removeSessionFromCookie(sessionCookie); +} diff --git a/apps/login/src/ui/IdpSignin.tsx b/apps/login/src/ui/IdpSignin.tsx index 72391d1b118..a11bf72a1a3 100644 --- a/apps/login/src/ui/IdpSignin.tsx +++ b/apps/login/src/ui/IdpSignin.tsx @@ -4,6 +4,7 @@ import { useEffect, useState } from "react"; import { Spinner } from "./Spinner"; import Alert from "./Alert"; import { useRouter } from "next/navigation"; +import { createNewSession } from "@/lib/server/session"; type Props = { userId: string; @@ -15,66 +16,54 @@ type Props = { authRequestId?: string; }; -export default function IdpSignin(props: Props) { +export default function IdpSignin({ + userId, + idpIntent: { idpIntentId, idpIntentToken }, + authRequestId, +}: Props) { const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const router = useRouter(); - async function createSessionForIdp() { - setLoading(true); - const res = await fetch("/api/session", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - userId: props.userId, - idpIntent: props.idpIntent, - authRequestId: props.authRequestId, - // organization: props.organization, - }), - }); - - if (!res.ok) { - const error = await res.json(); - throw error.details.details; - } - return res.json(); - } - useEffect(() => { - createSessionForIdp() + createNewSession({ + userId, + idpIntent: { + idpIntentId, + idpIntentToken, + }, + authRequestId, + // organization: props.organization, + }) .then((session) => { - setLoading(false); - if (props.authRequestId && session && session.sessionId) { + if (authRequestId && session && session.id) { return router.push( `/login?` + new URLSearchParams({ - sessionId: session.sessionId, - authRequest: props.authRequestId, + sessionId: session.id, + authRequest: authRequestId, }), ); } else { - return router.push( - `/signedin?` + - new URLSearchParams( - props.authRequestId - ? { - loginName: session.factors.user.loginName, - authRequestId: props.authRequestId, - } - : { - loginName: session.factors.user.loginName, - }, - ), - ); + const params = new URLSearchParams({}); + if (session.factors?.user?.loginName) { + params.set("loginName", session.factors?.user?.loginName); + } + + if (authRequestId) { + params.set("authRequestId", authRequestId); + } + + return router.push(`/signedin?` + params); } }) .catch((error) => { - setLoading(false); setError(error.message); + return; }); + + setLoading(false); }, []); return ( diff --git a/apps/login/src/ui/LoginPasskey.tsx b/apps/login/src/ui/LoginPasskey.tsx index fce8ecd8ecb..4c6dc122968 100644 --- a/apps/login/src/ui/LoginPasskey.tsx +++ b/apps/login/src/ui/LoginPasskey.tsx @@ -8,6 +8,12 @@ import Alert from "./Alert"; import { Spinner } from "./Spinner"; import BackButton from "./BackButton"; import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; +import { updateSession } from "@/lib/server/session"; +import { + RequestChallengesSchema, + UserVerificationRequirement, +} from "@zitadel/proto/zitadel/session/v2/challenge_pb"; +import { create } from "@zitadel/client"; // either loginName or sessionId must be provided type Props = { @@ -42,8 +48,8 @@ export default function LoginPasskey({ updateSessionForChallenge() .then((response) => { const pK = - response.challenges.webAuthN.publicKeyCredentialRequestOptions - .publicKey; + response?.challenges?.webAuthN?.publicKeyCredentialRequestOptions + ?.publicKey; if (pK) { submitLoginAndContinue(pK) .then(() => { @@ -66,65 +72,46 @@ export default function LoginPasskey({ }, []); async function updateSessionForChallenge( - userVerificationRequirement: number = login ? 1 : 3, + userVerificationRequirement: number = login + ? UserVerificationRequirement.REQUIRED + : UserVerificationRequirement.DISCOURAGED, ) { setLoading(true); - const res = await fetch("/api/session", { - method: "PUT", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - loginName, - sessionId, - organization, - challenges: { - webAuthN: { - domain: "", - // USER_VERIFICATION_REQUIREMENT_UNSPECIFIED = 0; - // USER_VERIFICATION_REQUIREMENT_REQUIRED = 1; - passkey login - // USER_VERIFICATION_REQUIREMENT_PREFERRED = 2; - // USER_VERIFICATION_REQUIREMENT_DISCOURAGED = 3; - mfa - userVerificationRequirement: userVerificationRequirement, - }, + const session = await updateSession({ + loginName, + sessionId, + organization, + challenges: create(RequestChallengesSchema, { + webAuthN: { + domain: "", + userVerificationRequirement, }, - authRequestId, }), + authRequestId, + }).catch((error: Error) => { + setError(error.message); }); - setLoading(false); - if (!res.ok) { - const error = await res.json(); - throw error.details.details; - } - return res.json(); + + return session; } async function submitLogin(data: any) { setLoading(true); - const res = await fetch("/api/session", { - method: "PUT", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - loginName, - sessionId, - organization, - checks: { - webAuthN: { credentialAssertionData: data }, - } as Checks, - authRequestId, - }), + const response = await updateSession({ + loginName, + sessionId, + organization, + checks: { + webAuthN: { credentialAssertionData: data }, + } as Checks, + authRequestId, + }).catch((error: Error) => { + setError(error.message); }); - const response = await res.json(); - setLoading(false); - if (!res.ok) { - setError(response.details); - return Promise.reject(response.details); - } + return response; } @@ -183,19 +170,16 @@ export default function LoginPasskey({ }), ); } else { - return router.push( - `/signedin?` + - new URLSearchParams( - authRequestId - ? { - loginName: resp.factors.user.loginName, - authRequestId, - } - : { - loginName: resp.factors.user.loginName, - }, - ), - ); + const params = new URLSearchParams({}); + + if (authRequestId) { + params.set("authRequestId", authRequestId); + } + if (resp?.factors?.user?.loginName) { + params.set("loginName", resp.factors.user.loginName); + } + + return router.push(`/signedin?` + params); } }); } else { diff --git a/apps/login/src/ui/RegisterPasskey.tsx b/apps/login/src/ui/RegisterPasskey.tsx index 68f77490514..b6dbd3ef614 100644 --- a/apps/login/src/ui/RegisterPasskey.tsx +++ b/apps/login/src/ui/RegisterPasskey.tsx @@ -157,8 +157,6 @@ export default function RegisterPasskey({ } } - const { errors } = formState; - return (
{error && ( diff --git a/apps/login/src/ui/SessionItem.tsx b/apps/login/src/ui/SessionItem.tsx index 27bceeb4c39..7ef0c4238c1 100644 --- a/apps/login/src/ui/SessionItem.tsx +++ b/apps/login/src/ui/SessionItem.tsx @@ -7,6 +7,8 @@ import moment from "moment"; import { XCircleIcon } from "@heroicons/react/24/outline"; import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; import { timestampDate } from "@zitadel/client"; +import { deleteSession } from "@/lib/zitadel"; +import { cleanupSession } from "@/lib/server/session"; export default function SessionItem({ session, @@ -21,25 +23,14 @@ export default function SessionItem({ async function clearSession(id: string) { setLoading(true); - const res = await fetch("/api/session?" + new URLSearchParams({ id }), { - method: "DELETE", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - id: id, - }), + const response = await cleanupSession({ + sessionId: id, + }).catch((error) => { + setError(error.message); }); - const response = await res.json(); - setLoading(false); - if (!res.ok) { - // setError(response.details); - return Promise.reject(response); - } else { - return response; - } + return response; } const validPassword = session?.factors?.password?.verifiedAt; @@ -51,6 +42,8 @@ export default function SessionItem({ const validDate = validPassword || validPasskey; const validUser = (validPassword || validPasskey) && stillValid; + const [error, setError] = useState(null); + return (