session actions

This commit is contained in:
peintnermax
2024-09-03 10:24:05 +02:00
parent e4794f2632
commit f3300cdbb4
8 changed files with 117 additions and 331 deletions

View File

@@ -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();
}
}

View File

@@ -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");
}

View File

@@ -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);
}

View File

@@ -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<string | null>(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 (

View File

@@ -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 {

View File

@@ -157,8 +157,6 @@ export default function RegisterPasskey({
}
}
const { errors } = formState;
return (
<form className="w-full">
{error && (

View File

@@ -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<string | null>(null);
return (
<Link
prefetch={false}

View File

@@ -195,9 +195,9 @@ export type SessionWithChallenges = Session & {
export async function setSessionAndUpdateCookie(
recentCookie: CustomCookieData,
checks: Checks,
challenges: RequestChallenges | undefined,
authRequestId: string | undefined,
checks?: Checks,
challenges?: RequestChallenges,
authRequestId?: string,
) {
return setSession(
recentCookie.id,